diff --git a/.env.example b/.env.example index 765196e..bf4a4d8 100644 --- a/.env.example +++ b/.env.example @@ -4,5 +4,13 @@ DATA_DIR=/data DATABASE_URL=sqlite:///data/mindsift.db JWT_SECRET=change_this_to_a_secure_random_key_min_32_chars BASE_URL=http://localhost:8080 -ALLOW_REGISTRATION=true +ALLOW_REGISTRATION=false LOG_LEVEL=info + +# Optional SMTP Server Configuration for Email Alarms (can also be set in Admin Settings UI) +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASS= +SMTP_FROM=noreply@mindsift.local +SMTP_TLS=true diff --git a/.gitea/workflows/build-and-publish.yaml b/.gitea/workflows/build-and-publish.yaml index 1c8cbfd..28ead19 100644 --- a/.gitea/workflows/build-and-publish.yaml +++ b/.gitea/workflows/build-and-publish.yaml @@ -8,40 +8,8 @@ on: - '*' jobs: - test: - name: Lint, Build & Unit Tests - runs-on: ubuntu-latest - if: github.ref != 'refs/heads/main' - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '1.23' - - - name: Run Backend Lint & Unit Tests - run: | - go mod tidy - go vet ./... - go test -v ./... - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - - - name: Typecheck & Build Frontend - run: | - cd frontend - npm install --no-audit --no-fund - npx tsc --noEmit - npm run build - - build-docker: - name: Build & Push Docker Image - needs: test + build-test-push: + name: Build, Test in Container & Push runs-on: ubuntu-latest if: github.ref != 'refs/heads/main' steps: @@ -69,10 +37,7 @@ jobs: echo "image=${IMAGE_NAME}" >> "$GITHUB_OUTPUT" echo "tag=${DOCKER_TAG}" >> "$GITHUB_OUTPUT" echo "actor=${ACTOR_NAME}" >> "$GITHUB_OUTPUT" - echo "Building Docker image: ${IMAGE_NAME}:${DOCKER_TAG} (Registry: ${REGISTRY_HOST}, Actor: ${ACTOR_NAME})" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + echo "Target Docker image: ${IMAGE_NAME}:${DOCKER_TAG} (Registry: ${REGISTRY_HOST}, Actor: ${ACTOR_NAME})" - name: Login to Gitea Container Registry uses: docker/login-action@v3 @@ -81,11 +46,10 @@ jobs: username: ${{ steps.prep.outputs.actor }} password: ${{ secrets.PUSH_TOKEN || secrets.GITEA_TOKEN || github.token }} - - name: Build and Push Multi-Arch Image - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ${{ steps.prep.outputs.image }}:${{ steps.prep.outputs.tag }} + - name: Build & Run All Checks in Container, then Push + run: | + FULL_IMAGE="${{ steps.prep.outputs.image }}:${{ steps.prep.outputs.tag }}" + echo "🚀 Building container & executing Frontend/Backend checks inside image: ${FULL_IMAGE}" + DOCKER_BUILDKIT=1 docker build --progress=plain -t "${FULL_IMAGE}" . + echo "✅ All checks & tests passed inside container! Pushing ${FULL_IMAGE}..." + docker push "${FULL_IMAGE}" diff --git a/BRAIN.md b/BRAIN.md index 0fe9498..41ced99 100644 --- a/BRAIN.md +++ b/BRAIN.md @@ -1,183 +1,307 @@ -# 🧠 BRAIN.md — MindSift Internal Dev Cheat Sheet -> **Zweck:** Rein internes, ungeschöntes Nachschlagewerk für uns Entwickler. Keine Marketing-Texte, nur knallharte Fakten, Farbwerte, State-Machines, Namenskonventionen und Quick-Commands. +# 🧠 BRAIN.md — MindSift Internal Dev Cheat Sheet & Architecture Log +> **Zweck:** Zentrales, technisch präzises Nachschlagewerk für Entwicklung, Design-System, Datenbank-Architektur, State-Machines, Backend-APIs und aktuellen Umsetzungsstand. Wird bei jeder Änderung aktuell gehalten. --- -## 🎨 1. Design-Tokens & Gemini-Dark-Theme (Copy & Paste) +## 📋 0. Aktueller Projektplan & Umsetzungs-Status + +### Erledigte Meilensteine +- [x] **CI/CD & Multi-Stage Docker Build (`Dockerfile`, `.gitea/workflows/build-and-publish.yaml`):** + - Alle Frontend-Checks (`tsc --noEmit`, `vite build`) und Backend-Tests (`go test -v ./...`) laufen direkt im Multi-Stage Container-Build ohne doppelte Vorab-Steps. + - Push auf `dev` baut und überschreibt `git.dsit.me/sandrodach/mindsift:dev`. +- [x] **Single-File CSS Theming Engine & Design-System (`frontend/src/themes/`, `theme.ts`, `tailwind.config.js`):** + - Jedes Theme ist als **genau eine einzige, eigenständige CSS-Datei** unter `frontend/src/themes/` definiert. + - Alle Farben, Kontraste, Oberflächen, Rahmen, Statusfarben und Rundungen sind in 6 klaren Gruppen organisiert. + - 4 vorkonfigurierte Themes (Dark, Light, High Contrast, Soft) plus saubere `theme-template.css`-Vorlage für eigene Themes. + - Vollständige Trennung von Struktur und Styling: React-Komponenten enthalten keine hardcodierten Farbwerte mehr, sondern ausschließlich semantische Klassen und CSS-Variablen (`text-main`, `text-muted`, `bg-canvas`, `panel-surface`, `checkbox-square`, etc.). + - Schnellauswahl im Profil-Dropdown im Header sowie visueller Theme-Manager in den Benutzereinstellungen. +- [x] **Monochromes, eckiges Desktop-Design & Zentrale CSS-Steuerung:** + - Zentrale CSS-Variablen (`--bg-canvas`, `--bg-panel`, `--accent`, `--border-color`, `--radius-card`, `--radius-btn`, `--radius-input`) in den Theme-Dateien. + - Eckige Tabs, eckige Panels/Karten (`0px` Radius im Dark/Monochrome-Theme), nur die Schnell-Eingabe-Blase (`InstantCaptureBar`) oben im Header nutzt `--radius-pill`. + - Untere doppelte Navigationsleiste entfernt; Schnell-Notiz-Blase sitzt ganz oben im Header über den Tabs. + - Workspace links als eckiger, monochromer Datei-/Ordner-Baum (File Explorer Tree mit Unterordnern). +- [x] **Quick Dump Notes + Triage Swipen + Direkt-Zuordnung (`InstantCaptureBar.tsx`, `TriageDeck.tsx`, `server.go`):** + - **Quick Dump (Standard):** Text oben eingeben + `Enter` wirft den Gedanken sofort als Quick Dump (`status: 'inbox'`) ins Triage-Deck. + - **Direkt-Zuordnung bei der Eingabe:** Über das Auswahl-Menü (`Quick Dump ▾`) in der Eingabeleiste kann ein Eintrag alternativ direkt als **Aufgabe** (`target: 'task'`), direkt auf eine **Geteilte Liste** (`target: 'list_item'`) oder direkt als **Workspace-Notiz** (`target: 'document'`) gespeichert werden. + - **Triage-Entscheidung & Warteschlange:** Im Triage-Deck per Swipe/Klick **Wegwerfen** (`←` / `H`), **Später** (`Leertaste` – ans Ende schieben), **Auf Liste**, **Als Notiz** (`D`), **Mit Termin** (`T`) oder **Als Aufgabe** (`→` / `L`). Inklusive Inline-Textbearbeitung und Warteschlangen-Liste unter der aktiven Karte. +- [x] **Mehrere Aufgabenlisten & Kanban-Board (`TasksView.tsx`, `models.go`, `sqlite.go`):** + - Beliebig viele Aufgabenlisten (z.B. Privat, Arbeit, Homelab) mit eigenem Farb- und Icon-Code. + - Teilen von Aufgabenlisten mit Teams/Spaces. + - Umschalter zwischen kompakter Listenansicht und 3-Spalten-Kanban-Board (`Zu erledigen`, `In Arbeit`, `Erledigt`) mit 1-Klick-Statuswechsel, Fälligkeitsdaten und Notizfeldern. +- [x] **Ersteinrichtung (First-Boot Setup Wizard), Echter Login & Einladungscodes (`SetupAndAuthView.tsx`, `server.go`, `sqlite.go`):** + - Kein hartcodierter Auto-Seed-Benutzer (`admin@mindsift.local` / `"Sandro"`) und kein automatischer `quickStart`-Login mehr beim Seitenaufruf. + - Beim ersten Start (`0` Benutzer in der Datenbank) erkennt `GET /api/v1/auth/status` automatisch `needsSetup: true` und zeigt den **Ersteinrichtungs-Assistenten** (`POST /api/v1/auth/setup`) zum Anlegen des ersten Server-Admin-Kontos und der Instanz-Grundeinstellungen. + - Öffentliche Selbstregistrierung ist standardmäßig deaktiviert (`allowRegistration: false`). Neue Benutzer können nur über einen **Einladungscode** (`MS-XXXXXXXX`, Tabelle `user_invites`) oder direkt durch einen Admin (`admin` / `team_admin`) angelegt werden. +- [x] **Profile-Dropdown & Vollbild-Einstellungsseiten statt kleinem Modal (`App.tsx`, `UserSettingsView.tsx`, `AdminSettingsView.tsx`):** + - Klick auf den Benutzer oben rechts öffnet ein eckiges, monochromes Dropdown-Menü: + - **Kontoeinstellungen** (`UserSettingsView.tsx` — Vollbild-Ansicht für Profil & Passwort, Darstellung & Themes, Multi-Channel-Alarme, Private Webhooks & Daten-Export). + - **Administration** (`AdminSettingsView.tsx` — nur sichtbar für `admin` und `team_admin`; Vollbild-Ansicht für Benutzer, Rollen, Einladungscodes, Organisationen & Teams, Server-Isolation und SMTP-Mailserver). + - **Abmelden** (bereinigt JWT-Token und führt zurück zum Login-Bildschirm). + - 3-stufiges Rollensystem: `admin` (Server-Admin), `team_admin` (Team-Admin mit Berechtigung zum Einladen und Verwalten von Teams) und `member` (Mitglied). + - Layout-Fix im Benutzer-Erstellungsformular (`grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3` mit `min-w-0`), sodass das Rollen-Dropdown niemals über den Container hinausragt. +- [x] **Multi-Channel-Alarme: E-Mail (SMTP), Discord & Eigener Webhook (`UserSettingsView.tsx`, `AdminSettingsView.tsx`, `server.go`):** + - Tab heißt klar **Alarme** mit frei kombinierbaren Zielen (`alarmChannels`: `'email'`, `'discord'`, `'webhook'`). + - SMTP-Konfiguration (`SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`, `SMTP_TLS`) sowohl über `docker-compose.yml` Environment-Variablen als auch direkt in der Admin-Oberfläche (`system_settings` in SQLite) einstellbar, inklusive Live-Testversand. +- [x] **Crash-Fix für „Aufgaben" & „Organisationen & Teams" (`sqlite.go`, `api.ts`, `TasksView.tsx`):** + - Alle Go-Storage-Methoden initialisieren Slices strikt mit `make([]T, 0)` statt `var s []T`, damit `json.Marshal` niemals `null` statt `[]` liefert. + - Zusätzliche `ensureArray()`-Normalisierung in `frontend/src/lib/api.ts` sowie `ViewErrorBoundary` in `App.tsx`. +- [x] **Workspace Markdown-Wiki mit Notion-/Word-Toolbar & Disk-Sync (`WorkspaceView.tsx`, `sqlite.go`):** + - Word-/Notion-artige Formatierungsleiste oben (Fett, Kursiv, Durchgestrichen, Code, Links, Überschriften H1-H3, Checklisten, Aufzählungen, Callouts, Markdown-Tabellen, Fortschrittsbalken). + - Interne Wiki-Links (`[[Dokumenttitel]]`) mit Klick-Navigation und Auto-Erkennung. + - Physische Speicherung auf Festplatte unter `/data/workspace//...` als Standard-`.md`-Dateien zur einfachen Daten-Extraktion, synchronisiert mit SQLite. +- [x] **Workspace Logseq Outliner-Engine, Zoom-Hoisting & Drag & Drop (`WorkspaceView.tsx`, `models.go`, `sqlite.go`):** + - Bullet-Point-basiertes hierarchisches Notizensystem inspiriert von Logseq. + - Jeder Block ist ein Bullet-Point (`•`) mit flexibler Einrückungstiefe (`Level: 0, 1, 2...`). + - Schnelle Tastaturnavigation: + - `Enter`: Neuer Bullet-Point auf gleicher Ebene. + - `Tab`: Block dem vorherigen unterordnen (einrücken). + - `Shift+Tab`: Block ausrücken. + - `Shift+Enter`: Weicher Zeilenumbruch innerhalb desselben Blocks. + - `Backspace`: Auf leerem Block rückt aus oder entfernt den Bullet-Point. + - **Zoom-in Hoisting:** Klick auf den Bullet-Dot zoomt tief in diesen Ast hinein (Fokusmodus) mit Breadcrumbs-Navigation und „Zoom aufheben"-Button. + - **Drag & Drop:** Bullet-Dots dienen als Anfasser zum Verschieben und Umsortieren von Blöcken und ganzen Unterbäumen. + - **Slash-Commands (`/`):** Schnelle Umwandlung in `/todo`, `/h1`, `/h2`, `/h3`, `/code`, `/table`, `/callout`, `/progress`, `/divider`. + - Physische Speicherung auf Festplatte unter `/data/workspace//...` als Standard-`.md`-Dateien mit portabler 2-Leerzeichen-Einrückung (` - [ ] ` bzw. ` - `), vollständig kompatibel mit externen Outlinern und Markdown-Viewern. +- [x] **Globaler Workspace To-Do Rollup Dashboard (`WorkspaceView.tsx`):** + - Aggregierte Übersicht aller To-Dos (`- [ ] ` / `- [x] `) aus sämtlichen Notiz-Dateien über alle Ordner hinweg. + - Übersichtliche Gruppierung nach Ordner ➔ Dokument ➔ Aufgaben mit einklappbaren Akkordeons und Zähler für offene Aufgaben. + - **Ordner-Drilldown:** Klick auf einen Ordner isoliert nur diesen Zweig für fokussiertes Abarbeiten, inklusive Zurücksetzen-Filter. + - **Live 2-Wege-Sync:** Abhaken oder Aktivieren eines To-Dos im Rollup synchronisiert direkt in Echtzeit zurück in die SQLite-Datenbank und die physische `.md`-Datei auf Festplatte. +- [x] **Aufgabenlisten-Teilen & Avatar-Stack (`TasksView.tsx`, `models.go`, `sqlite.go`):** + - Aufgabenlisten können direkt mit Teams/Spaces geteilt werden. + - Avatar-Stack (farbige Initialen-Bubbles mit Hover-Tooltips) im Header der Liste und in den Tabs visualisiert, wem eine Liste gehört und mit welchen Teammitgliedern sie geteilt ist. +- [x] **Admin-Schalter für öffentliche Selbstregistrierung (`AdminSettingsView.tsx`):** + - Unintuitiver Button in den Admin-Einstellungen unter Server & Isolation durch einen flüssig animierten Schiebeschalter (`rounded-pill`) mit eindeutigen Status-Beschriftungen ersetzt. +- [x] **Cross-Browser & Performance Fixes (Chrome & Firefox):** + - SQLite Connection-Pool Deadlock durch striktes Schließen von `sql.Rows` vor Unterabfragen behoben. + - `Cache-Control: no-store` auf allen API-Routen und `index.html`. + - Optimistisches Leeren des Capture-Inputs und 12-Sekunden-Fetch-Timeout. + +--- + +## 🎨 1. Single-File CSS Theming Engine & Design-System + +### Philosophie: 1 Datei = 1 komplettes Theme +Anstatt Styling in Dutzenden Tailwind-Klassen über hunderte Komponenten zu verstreuen, steuert **eine einzige CSS-Datei** das komplette Erscheinungsbild der Anwendung. + +Jedes Theme liegt unter `frontend/src/themes/` und gruppiert **6 standardisierte Bereiche**: -### Farbpalette (Strict Hex Values) ```css -/* Backgrounds */ ---bg-app: #0b0f19; /* Tiefstes Obsidian / OLED Base */ ---bg-surface: #131314; /* Leicht erhobene Panels, Sidebar */ ---bg-card: #1e1f20; /* Swipe-Karten, Dialoge, Dropdowns */ ---bg-card-hover: #282a2c; /* Hover-Zustände */ +:root[data-theme="dein-theme"] { + /* ========================================================================== + 1. AKZENTFARBE (Fokus, Primäraktionen, Highlights) + ========================================================================== */ + --accent: #2563eb; + --accent-hover: #3b82f6; + --accent-dim: rgba(37, 99, 235, 0.15); + --accent-contrast: #ffffff; -/* Borders & Dividers */ ---border-subtle: rgba(255, 255, 255, 0.08); /* 1px Standard-Kante */ ---border-focus: rgba(138, 180, 248, 0.40); /* Gemini Blue Glow */ + /* ========================================================================== + 2. OBERFLÄCHEN & HINTERGRÜNDE + ========================================================================== */ + --bg-canvas: #0e1013; + --bg-panel: #16181d; + --bg-panel-elevated: #1f2229; + --bg-input: #121418; + --bg-code: #0b0c0f; + --bg-overlay: rgba(0, 0, 0, 0.75); -/* Akzente & Status */ ---accent-blue: #8ab4f8; /* Gemini Primärblau (Buttons, Links, Active) */ ---accent-purple: #c58af9; /* Gemini Voice / Magic Glow */ ---swipe-keep: #81c995; /* Swipe Rechts (Behalten / ToDo / Success) */ ---swipe-discard: #f28b82; /* Swipe Links (Verwerfen / Archive / Danger) */ + /* ========================================================================== + 3. TEXT & KONTRASTE + ========================================================================== */ + --text-main: #f4f4f5; + --text-muted: #a1a1aa; + --text-faint: #71717a; + --text-on-accent: #ffffff; -/* Typografie */ ---text-primary: #e3e3e3; /* Haupttext (hoher Kontrast, augenschonend) */ ---text-muted: #9aa0a6; /* Sekundärtexte, Metadaten, Placeholders */ ---text-subtle: #5f6368; /* Timestamps, Footer, Deaktiviert */ -``` + /* ========================================================================== + 4. RAHMEN, LINIEN & SCHATTEN + ========================================================================== */ + --border-color: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.18); + --shadow-panel: 0 4px 20px -2px rgba(0, 0, 0, 0.45); -### UI-Regeln & Radien -* **Buttons / Pills:** Ausnahmslos `rounded-full` (`border-radius: 9999px`). -* **Karten (Triage Deck):** `rounded-3xl` (`24px`), Padding `p-6`, Border `1px solid var(--border-subtle)`. -* **Input-Feld (Instant Capture):** `rounded-full`, Höhe `h-14` (56px) für mobile Daumenbedienung, Autofokus bei Mount. -* **Gesten-Physik (Framer Motion):** - * Swipe-Schwellenwert: `dragConstraints={{ left: -150, right: 150 }}` - * Spring-Config: `{ type: "spring", stiffness: 350, damping: 25 }` - * Swipe-Winkel-Rotation: `rotate = x * 0.1` + /* ========================================================================== + 5. STATUS- & SIGNALFARBEN + ========================================================================== */ + --status-ok: #22c55e; + --status-err: #ef4444; + --status-warn: #eab308; + --status-info: #3b82f6; ---- - -## ⚙️ 2. State-Machine: Gedanken-Lifecycle - -``` - [ INSTANT CAPTURE ] - │ - ▼ - status: 'inbox' - │ - ┌───────────┴───────────┐ - │ (Swipe Left) │ (Swipe Right) - ▼ ▼ - status: 'discarded' status: 'kept' - (Soft Delete) │ - ┌───────┴───────┐ - │ (Convert) │ (Direct Task) - ▼ ▼ - status: 'converted' due_date != null - ├── target: 'task' - ├── target: 'document' - └── target: 'list_item' -``` - -### Gültige Status-Enums -* `inbox` – Frisch eingekippt, wartet auf Triage. -* `discarded` – Nach links gewischt (bleibt für 30 Tage im Papierkorb, dann Hard Delete). -* `kept` – Nach rechts gewischt, bleibt als To-Do oder Gedanke archiviert. -* `converted` – In vollwertiges Dokument, Aufgabe oder Listen-Item transformiert. - ---- - -## 📡 3. WebSocket Realtime Protocol (Family Lists) - -Verbindungs-URL: `wss:///api/v1/ws/spaces/:spaceId` - -### Nachrichten-Format (JSON) -```typescript -interface WSMessage { - event: 'ITEM_CREATED' | 'ITEM_UPDATED' | 'ITEM_TOGGLED' | 'ITEM_DELETED' | 'USER_PRESENCE'; - space_id: string; - list_id: string; - payload: T; - timestamp: string; // ISO 8601 + /* ========================================================================== + 6. RUNDUNGEN / GEOMETRIE + ========================================================================== */ + --radius-card: 0px; + --radius-btn: 2px; + --radius-input: 2px; + --radius-checkbox: 2px; + --radius-pill: 9999px; } ``` -### Schnelle Event-Referenz -* `ITEM_TOGGLED`: Wenn jemand die Hafermilch abhakt: - `{ "item_id": "itm_123", "is_completed": true, "completed_by": "usr_sandro" }` -* `Optimistic UI`: Frontend schlägt das Item sofort lokal durch und rollt nur bei WebSocket-Fehler zurück. +### Verfügbare Themes + +| Theme-ID | Datei | Charakter | Standard-Geometrie | +| :--- | :--- | :--- | :--- | +| `dark` | [`theme-dark.css`](file:///home/sandro/Dokumente/git/MindSift/frontend/src/themes/theme-dark.css) | Standard Monochrom / Dark Slate | Streng eckig (`0px` Cards, `2px` Buttons) | +| `light` | [`theme-light.css`](file:///home/sandro/Dokumente/git/MindSift/frontend/src/themes/theme-light.css) | Daylight Clean White / Minimalist Light | Modern sauber (`4px` Cards, `4px` Buttons) | +| `high-contrast` | [`theme-high-contrast.css`](file:///home/sandro/Dokumente/git/MindSift/frontend/src/themes/theme-high-contrast.css) | Reines Schwarz/Weiß mit maximaler Lesbarkeit (AAA Kontrast) | Streng eckig (`0px`, `2px` Rahmen) | +| `soft` | [`theme-soft.css`](file:///home/sandro/Dokumente/git/MindSift/frontend/src/themes/theme-soft.css) | Soft Rounded Slate mit warmem Indigo-Akzent | Weich gerundet (`12px` Cards, `8px` Buttons) | +| *Template* | [`theme-template.css`](file:///home/sandro/Dokumente/git/MindSift/frontend/src/themes/theme-template.css) | Dokumentierte Vorlage zur Erstellung eigener Themes | Voll anpassbar | + +### Semantische Komponenten-Klassen (`frontend/src/index.css`) +- `.panel-surface` / `.panel-elevated`: Automatisch über `--radius-card`, `--bg-panel` und `--border-color` gesteuert. +- `.btn-primary` / `.btn-action`: Aktionen und Buttons mit `--radius-btn`, `--accent` und `--text-on-accent`. +- `.input-field`: Formularfelder mit `--radius-input`, `--bg-input`, `--border-color` und `--text-main`. +- `.checkbox-square`: Kontrollkästchen mit `--radius-checkbox` und `--accent`. +- `.badge-accent`, `.badge-neutral`, `.badge-ok`, `.badge-err`: Pillen und Zähler. +- `.alert-ok`, `.alert-err`: Hinweiskästen mit Signalfarben. +- `.capture-pill`: Die markante Schnell-Eingabeleiste im Header mit `--radius-pill`. --- -## 🗂️ 4. Ordnerstruktur & Code-Konventionen +## 🗄️ 2. Datenbank-Architektur (SQLite Schema & Tabellen) -``` -mindsift/ -├── cmd/ -│ └── server/ # Go Main Entrypoint (bindet embed.FS ein) -├── internal/ -│ ├── api/ # HTTP Chi Handler, Routing & Middleware -│ ├── domain/ # Entities, Interfaces & Domain Logic -│ ├── storage/ # SQLite / Postgres Adapter (sqlc generiert) -│ ├── ws/ # WebSocket Hub & Raumverwaltung -│ └── webhook/ # Smarthome Ingestion Logic -├── migrations/ # Reine .sql Migrationsdateien -├── frontend/ -│ ├── src/ -│ │ ├── components/ # UI Primitives (Button, Pill, Input, Modal) -│ │ ├── features/ -│ │ │ ├── capture/ # Instant-Input & Voice Recording -│ │ │ ├── triage/ # Framer Motion Swipe Deck -│ │ │ ├── workspace/ # Tiptap Block Editor -│ │ │ └── lists/ # Realtime Shopping / Todo Lists -│ │ ├── lib/ # WS Client, API Client, Sound/Haptics -│ │ └── index.css # Tailwind v4 & Gemini CSS Variablen -├── .gitea/workflows/ # Gitea Actions CI/CD -├── Dockerfile # Multi-Stage Build (< 35 MB) -├── docker-compose.yml -├── README.md # Öffentliche Doku & Vision -└── BRAIN.md # Dieses Dokument (Dev Cheat Sheet) -``` +MindSift speichert alle Daten lokal und hochperformant in SQLite (`/data/mindsift.db`). -### Namens- & ID-Konventionen -* **Entity IDs:** Präfix-basierte Strings für Lesbarkeit beim Debuggen: - * `usr_` = User (`usr_9k2m1a`) - * `th_` = Thought (`th_4x8z19`) - * `doc_` = Document (`doc_81na72`) - * `lst_` = List (`lst_33fa09`) - * `itm_` = List Item (`itm_77bb41`) -* **Datenbank:** `snake_case` (z. B. `is_completed`, `due_date`) -* **JSON API:** `camelCase` (z. B. `isCompleted`, `dueDate`) -* **Timestamps:** Immer UTC ISO-8601 (`2026-10-24T14:20:00Z`) +### Tabellen-Übersicht + +1. **`users`** + - `id TEXT PRIMARY KEY`, `email TEXT UNIQUE`, `password_hash TEXT`, `display_name TEXT` + - `role TEXT DEFAULT 'member'` (`admin`, `team_admin`, `member`) + - `is_active INTEGER DEFAULT 1`, `created_at DATETIME`, `updated_at DATETIME` +2. **`system_settings`** (Instanzweite globale Konfiguration, 1 Row mit `id = 'singleton'`) + - `sharing_mode TEXT DEFAULT 'teams_only'` (`strict_private`, `teams_only`, `open`) + - `allow_registration INTEGER DEFAULT 0` (Öffentliche Registrierung erlaubt oder gesperrt) + - `instance_name TEXT DEFAULT 'MindSift'` + - `smtp_host TEXT`, `smtp_port INTEGER`, `smtp_user TEXT`, `smtp_pass TEXT`, `smtp_from TEXT`, `smtp_tls INTEGER` +3. **`user_invites`** (Einladungscodes für neue Registrierungen) + - `id TEXT PRIMARY KEY`, `code TEXT UNIQUE` (`MS-XXXXXXXX`), `email TEXT` (optional gebunden) + - `role TEXT DEFAULT 'member'`, `team_id TEXT` (optional direkte Team-Zuweisung) + - `created_by TEXT`, `used_by TEXT`, `expires_at DATETIME`, `created_at DATETIME` +4. **`organizations` & `organization_teams` & `team_members`** + - Strukturierung von Benutzern in Abteilungen, Familien oder Projekt-Teams. + - Bildet die Basis für den Sichtbarkeits-Modus `teams_only`. +5. **`spaces` & `space_members`** + - Geteilte Kollaborations-Räume mit Einladungscode (`invite_code`), Echtzeit-WebSocket Sync und Rollen (`owner`, `editor`, `viewer`). +6. **`shared_lists` & `list_items`** + - Geteilte Einkaufs-, Pack- und Inventarlisten mit Kategorien, Mengen und Echtzeit-Abhaken. +7. **`tasks` & `task_lists`** + - Persönliche und geteilte Aufgabenlisten mit Kanban-Spalten (`todo`, `in_progress`, `done`), Fälligkeit, Priorität und Alarm-Optionen. +8. **`quick_dumps` (Triage-Deck)** + - Schnelle Einfälle (`status = 'inbox'`, `'discarded'`, `'converted'`), Fälligkeitsdatum und Text-Notiz. +9. **`documents` (Workspace Markdown-Wiki)** + - Notizen und Dokumentationen mit Folder-Pfad (`folder_path`), Tags, Pinned-Status, JSON-Blocks und physischer `.md`-Spiegelung auf Disk. +10. **`user_alarms` & `user_webhooks`** + - Multi-Channel Alarme (Mail, Discord, Webhook) und ausgehende Event-Webhooks. --- -## ⌨️ 5. Power-User Tastatur-Shortcuts (Desktop) +## ⚡ 3. Triage & Quick-Dump State Machine + +```mermaid +stateDiagram-v2 + [*] --> InstantCapture: Gedanke eingetippt + InstantCapture --> QuickDump_Inbox: Enter (Standard) + InstantCapture --> TaskCreated: Ziel = 'task' + InstantCapture --> ListItemCreated: Ziel = 'list_item' + InstantCapture --> DocCreated: Ziel = 'document' + + QuickDump_Inbox --> TriageDeck: Stapel sichten + TriageDeck --> Discarded: Swipe links / H (Wegwerfen) + TriageDeck --> TaskCreated: Swipe rechts / L (Als Aufgabe) + TriageDeck --> TaskCreated: T (Termin-Aufgabe) + TriageDeck --> DocCreated: D (In Notiz umwandeln) + TriageDeck --> ListItemCreated: Auf Liste schieben + TriageDeck --> QuickDump_Inbox: Leertaste (Später - ans Ende) + + Discarded --> [*] + TaskCreated --> [*] + DocCreated --> [*] + ListItemCreated --> [*] +``` + +--- + +## 👥 4. Server-Administration & Isolation Modes + +Der Server-Administrator (`role = 'admin'`) legt unter `AdminSettingsView.tsx` global fest, wie Benutzer miteinander interagieren dürfen: + +1. **`strict_private` (Strikt Privat):** + - Jeder Benutzer arbeitet komplett autark. Keine teamübergreifende Benutzersuche, keine geteilten Spaces. +2. **`teams_only` (Teams isoliert — Empfohlener Standard):** + - Benutzer sehen im Adressbuch und bei geteilten Spaces ausschließlich Personen, mit denen sie mindestens in einem gemeinsamen Team sind. +3. **`open` (Offene Instanz):** + - Alle Benutzer können sich gegenseitig sehen, zu Spaces einladen und gemeinsam an Listen arbeiten. + +--- + +## 📝 5. Workspace: Word/Notion-Toolbar, Wiki-Links & Disk-Sync + +1. **Formatierungs-Werkzeuge:** + - Markdown-Syntax wird in Echtzeit formatiert und visuell aufbereitet. + - Buttons für H1, H2, H3, Fett (`**`), Kursiv (`*`), Durchgestrichen (`~~`), Code-Blöcke (```` ``` ````), Aufzählungen (`- `), Todos (`- [ ] `), Callouts (`> `), Markdown-Tabellen (`| Spalte 1 | Spalte 2 |`) und Fortschrittsbalken (`[progress:65%]`). +2. **Wiki-Links (`[[Dokumenttitel]]`):** + - Interne Querverweise werden als interaktive Links gerendert. + - Ein Klick auf `[[Dokumenttitel]]` durchsucht die Workspace-Dokumente und öffnet das verlinkte Dokument direkt im Editor. +3. **Physische Speicherung auf Festplatte:** + - Jede Notiz wird zusätzlich zur SQLite-Tabelle als reine `.md`-Datei unter `/data/workspace///.md` gespeichert. + - Ermöglicht jederzeitige manuelle Datensicherung, Git-Versionierung oder Backup-Extraktion ohne Datenbank-Abhängigkeit. + +--- + +## 🛡️ 6. Kritische Backend-Regeln (SQLite & HTTP) + +1. **NIEMALS verschachtelte Queries bei offenem `sql.Rows`-Cursor ausführen:** + - In [`internal/storage/sqlite.go`](file:///home/sandro/Dokumente/git/MindSift/internal/storage/sqlite.go) muss jede Schleife `for rows.Next()` zuerst alle Zeilen in ein Slice einlesen und sofort `_ = rows.Close()` aufrufen, **bevor** Folgeabfragen (`GetListItems`, `GetSpaceMembers`, `QueryRow` etc.) ausgeführt werden. +2. **SQLite DSN-Pragmas:** + - Jede Verbindung im `sql.DB`-Pool erhält über den DSN automatisch: + `?_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)&_pragma=foreign_keys(1)` + - `MaxOpenConns(8)` und `MaxIdleConns(4)` konfiguriert. +3. **Thread-Safe Default User (`EnsureDefaultUser`):** + - Durch `s.initMu.Lock()` abgesichert, damit parallele Browser-Requests beim ersten Laden niemals in einen `UNIQUE constraint`-Konflikt laufen. +4. **Kein Browser-Caching auf API & `index.html`:** + - `writeJSON` und der SPA-Fallback setzen `Cache-Control: no-store, no-cache, must-revalidate`, damit Chrome und Firefox nach jedem Container-Update sofort den aktuellen Stand zeigen. + +--- + +## 📡 7. WebSocket Realtime Protocol (Geteilte Listen) + +Verbindungs-URL: `ws(s):///api/v1/ws/spaces/:spaceId?token=` + +### Events +- `ITEM_CREATED` (auch ausgelöst, wenn ein Gedanke per Direkt-Zuordnung oder Triage auf eine Liste geschoben wird) +- `ITEM_TOGGLED` +- `ITEM_DELETED` +- `LIST_CREATED` / `LIST_DELETED` / `LIST_CLEARED` +- `USER_PRESENCE` + +--- + +## ⌨️ 8. Tastatur-Shortcuts (Desktop) | Taste | Kontext | Aktion | | :--- | :--- | :--- | -| `C` oder `/` | Überall | Fokus sofort auf das Capture-Eingabefeld | -| `H` oder `←` | Triage | Karte nach links wischen (Verwerfen) | -| `L` oder `→` | Triage | Karte nach rechts wischen (Behalten) | -| `T` | Triage | Sofort in Aufgabe mit Fälligkeitsdatum verwandeln | -| `D` | Triage | In Workspace-Dokument umwandeln | -| `Esc` | Modal/Capture | Abbrechen / Eingabe leeren | +| `C` oder `/` | Überall | Fokus sofort oben in die Quick-Dump-Eingabeleiste | +| `Enter` | Capture-Leiste | Sofort speichern (Quick Dump oder gewähltes Direkt-Ziel) | +| `Esc` | Capture-Leiste | Eingabe leeren & Dropdown schließen | +| `H` oder `←` | Triage-Deck | Aktiven Quick Dump wegwerfen (`discard`) | +| `L` oder `→` | Triage-Deck | Aktiven Quick Dump sofort zur Aufgabe machen (`task`) | +| `T` | Triage-Deck | Datums-Auswahl für Termin-Aufgabe öffnen | +| `D` | Triage-Deck | Aktiven Quick Dump sofort in Workspace-Notiz umwandeln | +| `Leertaste` | Triage-Deck | Karte überspringen („Später" – ans Ende des Stapels schieben) | +| `Enter` | Workspace Outliner | Neuen Bullet-Point auf gleicher Ebene erstellen | +| `Tab` | Workspace Outliner | Bullet-Point einrücken (dem vorherigen unterordnen) | +| `Shift + Tab` | Workspace Outliner | Bullet-Point ausrücken (Ebene verringern) | +| `Shift + Enter` | Workspace Outliner | Weicher Zeilenumbruch innerhalb des aktuellen Blocks | +| `Backspace` | Workspace Outliner | Bei leerem Block: Ebene ausrücken bzw. Bullet löschen | +| `/` | Workspace Outliner | Slash-Command-Menü öffnen (/todo, /h1, /code, etc.) | --- -## ⚡ 6. Cheat Sheet: Lokale Entwickler-Befehle - -```bash -# 1. Frontend Dev Server (Hot Reload) -cd frontend && npm run dev - -# 2. Go Backend starten (Hot Reload via air oder go run) -go run ./cmd/server - -# 3. Datenbank-Migrationen lokal ausführen -go run ./cmd/migrate -direction=up - -# 4. Lokalen Multi-Stage Docker-Build testen -docker build -t mindsift:dev . -docker run -p 8080:8080 -v $(pwd)/dev-data:/data -e JWT_SECRET="dev-secret-12345" mindsift:dev - -# 5. SQLite DB direkt per CLI inspizieren -sqlite3 ./dev-data/mindsift.db ".tables" "SELECT * FROM thoughts LIMIT 5;" -``` - ---- - -## 🚀 7. Gitea CI/CD & Docker Tagging-Strategie - -* **Registry:** `git.dsit.me/sandrodach/mindsift` -* **Gitea Secret für Registry-Login:** `secrets.PUSH_TOKEN` -* **API-Endpunkt zum Auslesen der Pipeline:** `https://git.dsit.me/api/v1/repos/sandrodach/MindSift/actions/tasks` -* **Branch- & Release-Regeln ([`.gitea/workflows/build-and-publish.yaml`](file:///home/sandro/Dokumente/git/MindSift/.gitea/workflows/build-and-publish.yaml)):** - * **Push auf `main`:** Führt **nur** Linter, TypeScript-Checks (`tsc --noEmit`) und Unit-Tests aus. **Kein Docker-Build/Push** (hält die Registry sauber). - * **Push auf Test-/RC-Branch (z. B. `1.1-rc`, `latest`, `test-feature`):** Führt Tests aus **und** baut/pusht das Image mit dem Branch-Namen (`:1.1-rc`, `:latest`). - * **Git-Tag / Release (z. B. `v1.0`, `1.1`):** Führt Tests aus **und** baut/pusht das Release-Image (`:v1.0`, `:1.1`). -* **Cross-Compile Speedup:** `Dockerfile` nutzt `FROM --platform=$BUILDPLATFORM` für Node & Go und kompiliert `amd64` + `arm64` nativ über `GOOS=${TARGETOS} GOARCH=${TARGETARCH}` ohne langsames QEMU. +## 🚀 9. Gitea CI/CD & Docker Workflow +- **Arbeits-Branch:** `dev` (`origin/dev`) +- **Ziel-Image:** `git.dsit.me/sandrodach/mindsift:dev` +- **Build-Prinzip:** Ein einziger Multi-Stage-Build ([`Dockerfile`](file:///home/sandro/Dokumente/git/MindSift/Dockerfile)), der Frontend-Typecheck, Vite-Build und Go-Unit-Tests (`go test -v ./...` inkl. [`internal/api/server_test.go`](file:///home/sandro/Dokumente/git/MindSift/internal/api/server_test.go)) direkt beim Image-Bau ausführt und bei Erfolg sofort pusht. diff --git a/Dockerfile b/Dockerfile index 4730f78..4f9bfe4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,47 @@ # ============================================================================== -# Stage 1: Frontend Build (Runs natively on $BUILDPLATFORM — zero QEMU overhead) +# Stage 1: Frontend Check & Build (Cached npm dependencies layer) # ============================================================================== -FROM --platform=$BUILDPLATFORM node:22-alpine AS frontend-builder +FROM node:22-alpine AS frontend-builder WORKDIR /app/frontend COPY frontend/package*.json ./ RUN npm install --no-audit --no-fund COPY frontend/ ./ -RUN npm run build +RUN npx tsc --noEmit && npm run build # ============================================================================== -# Stage 2: Go Backend Build (Native Cross-Compile via TARGETOS / TARGETARCH) +# Stage 2: Go Backend Lint, Unit Tests & Binary Build (Cached Go module & sqlite layer) # ============================================================================== -FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS backend-builder -ARG TARGETOS=linux -ARG TARGETARCH=amd64 +FROM golang:1.23-alpine AS backend-builder WORKDIR /app RUN apk add --no-cache git ca-certificates + +# Cache Go modules & pre-compile heavy dependencies (modernc.org/sqlite) in a dedicated layer COPY go.mod ./ +RUN go get github.com/go-chi/chi/v5@v5.2.0 \ + github.com/go-chi/cors@v1.2.1 \ + github.com/golang-jwt/jwt/v5@v5.2.1 \ + github.com/gorilla/websocket@v1.5.3 \ + golang.org/x/crypto/bcrypt@v0.31.0 \ + modernc.org/sqlite@v1.34.4 && \ + CGO_ENABLED=0 go build \ + modernc.org/sqlite \ + github.com/go-chi/chi/v5 \ + github.com/go-chi/cors \ + github.com/golang-jwt/jwt/v5 \ + github.com/gorilla/websocket \ + golang.org/x/crypto/bcrypt + +# Copy application source & compiled frontend SPA assets for go:embed COPY cmd/ ./cmd/ COPY internal/ ./internal/ COPY migrations/ ./migrations/ -# Copy compiled frontend SPA assets into cmd/server/dist for go:embed COPY --from=frontend-builder /app/frontend/dist ./cmd/server/dist -RUN go mod tidy -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-s -w" -o /app/mindsift ./cmd/server + +# Run go mod tidy, static analysis (go vet), unit tests (go test), and compile final binary +RUN go mod tidy && \ + CGO_ENABLED=0 go vet ./... && \ + CGO_ENABLED=0 go test -v ./... && \ + CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/mindsift ./cmd/server # ============================================================================== # Stage 3: Minimal Production Runtime (< 35 MB) diff --git a/README.md b/README.md index 214090f..5317e32 100644 --- a/README.md +++ b/README.md @@ -3,495 +3,87 @@ # 🧠 MindSift ### *Der Kopf frei in drei Sekunden.* -[![Build & Docker](https://img.shields.io/badge/Gitea%20CI-Passing-22c55e?logo=gitea&logoColor=white)](https://gitea.io) -[![Docker Image](https://img.shields.io/badge/Docker-Multi--Arch-2496ed?logo=docker&logoColor=white)](https://hub.docker.com) -[![Backend](https://img.shields.io/badge/Backend-Go%201.24-00ADD8?logo=go&logoColor=white)](https://go.dev) -[![Frontend](https://img.shields.io/badge/Frontend-React%2019%20%2F%20Vite-61DAFB?logo=react&logoColor=black)](https://react.dev) -[![Database](https://img.shields.io/badge/Database-SQLite%20(WAL)%20%2F%20Postgres-003B57?logo=sqlite&logoColor=white)](https://sqlite.org) -[![Styling](https://img.shields.io/badge/Theme-Google%20Gemini%20Dark-8ab4f8)](#-designsystem--ästhetik-google-gemini-dna) -[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) +[![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-in-60-sekunden-starten) +[![Markdown](https://img.shields.io/badge/Datenhoheit-Pure%20Markdown%20%26%20JSON-18181b?style=flat-square)](#) +[![License](https://img.shields.io/badge/Lizenz-MIT-22c55e?style=flat-square)](LICENSE) -**MindSift** verbindet den schnellsten Brain-Dump der Welt mit der Eleganz eines modernen Workspaces, spielerischer Tinder-Style-Triage und kollaborativen Echtzeit-Listen für Familien und Teams — 100 % Self-Hosted & Open Source. +**MindSift** ist deine zentrale Schaltstelle für spontane Gedanken, strukturierte Aufgaben, vernetztes Wissen und gemeinsame Echtzeit-Listen — kompromisslos schnell, ablenkungsfrei gestaltet und zu 100 % unter deiner eigenen Kontrolle. -[Manifest](#-das-manifest) • [Features](#-features--workflow) • [Architektur](#-technische-architektur--tech-stack) • [Datenbankschema](#-datenbankschema) • [API & Webhooks](#-api-spezifikation--webhooks) • [CI/CD](#-gitea-cicd-pipeline) • [Deployment](#-deployment--schnellstart) • [Roadmap](#-schrittweiser-phasenplan) +[Philosophie](#-die-philosophie) • [Funktionen](#-was-mindsift-auszeichnet) • [Für wen?](#-für-wen-ist-mindsift-gemacht) • [Schnellstart](#-in-60-sekunden-starten) --- -## 📜 Das Manifest +## 💡 Die Philosophie: *Erst raus aus dem Kopf – später sortieren* -Große Workspace-Tools wie Notion sind genial, wenn man am Schreibtisch sitzt und ein monatelanges Projekt plant. Aber sie scheitern im Alltag genau an einer Stelle: **im echten Moment**. +Klassische Produktivitäts- und Notiz-Apps scheitern im Alltag oft an derselben Hürde: **Sie verlangen Struktur im falschen Moment.** +Wenn dir mitten im Gespräch, unterwegs oder kurz vor dem Feierabend ein wichtiger Gedanke kommt, willst du keinen Ordner auswählen, keine Datenbank-Eigenschaften pflegen und kein Formular ausfüllen. Du willst den Gedanken in drei Sekunden loswerden – und sicher sein, dass er nicht verloren geht. -Wenn dir an der Supermarktkasse eine Idee kommt, du im Auto über einen Geistesblitz nachdenkst oder schnell eine Zutat aufschreiben willst, hast du keine Zeit, durch fünf verschachtelte Ordner und relationale Datenbanken zu navigieren. Du brauchst ein Tool, das in einer halben Sekunde bereitsteht, deinen Gedanken friert und dir später hilft, ihn spielerisch auszusortieren. +**MindSift trennt das Erfassen radikal vom Organisieren:** -**MindSift löst dieses Problem durch ein dreistufiges Prinzip:** -1. **Erfassen:** Sofortiger Brain-Dump ohne jede Reibung. -2. **Sichten (Sift):** Spielerische Triage im Tinder-Style (Inbox Zero für dein Gehirn). -3. **Strukturieren & Teilen:** Workspace für reifende Gedanken und geteilte Echtzeit-Listen für den Alltag. +1. **Sofort abladen (Quick Dump):** Oben in der Eingabeleiste tippen, `Enter` drücken, fertig. Dein Kopf ist wieder frei. Wer möchte, kann einen Eintrag aber auch direkt bei der Eingabe einer Aufgabenliste, einer Einkaufsliste oder einer Notiz zuordnen. +2. **Spielerisch sichten (Triage):** Wenn du später Ruhe hast, arbeitest du deinen Gedanken-Stapel Karte für Karte durch. Nach links verwerfen, auf „Später“ schieben oder mit einem Klick in eine echte Aufgabe, ein Dokument oder einen Listeneintrag verwandeln. +3. **Nahtlos weiterarbeiten:** Aus flüchtigen Ideen werden strukturierte Aufgaben-Boards, verlinkte Wiki-Dokumente oder geteilte Listen für Familie und Team. --- -## ✨ Features & Workflow +## ✨ Was MindSift auszeichnet -``` - [ 🚀 Instant Capture ] - Handy raus • PWA • Webhook • Sprache - │ - ▼ - [ 📥 Thought Inbox ] - │ - ▼ - [ 🃏 Tinder-Style Triage ] - ┌──────────┴──────────┐ - ▼ ▼ -[ 👈 Nach Links ] [ 👉 Nach Rechts ] - Verwerfen / Done Behalten / Vertiefen - ┌───────┼───────┐ - ▼ ▼ ▼ - Task Doc Shared List -``` +### ⚡ Quick-Dump & Triage-Deck +* **Immer griffbereit:** Die Schnell-Eingabeleiste sitzt prominent ganz oben – jederzeit erreichbar per Klick oder Tastendruck (`C` oder `/`). +* **Entscheiden ohne Stress:** Im Triage-Deck siehst du immer genau eine Karte im Fokus, während darunter deine Warteschlange bereitliegt. Bearbeite den Text direkt auf der Karte, vergib ein Fälligkeitsdatum oder sortiere ihn per Swipe und Tastatur-Kürzel weg. -* **⚡ Capture First (Instant Puffer):** - Handy raus, App antippen, Cursor steht im Feld. Text tippen oder diktieren (Web Speech API), absenden, fertig. Kein Ordner wählen, kein Tagging, kein Nachdenken. PWA-Shortcut für direkten Homescreen-Capture (< 300 ms Latenz). -* **🃏 Triage im Tinder-Style:** - Abends oder am Wochenende gehst du deine unstrukturierten Gedanken durch wie ein Kartendeck: - * **Wisch nach links:** Weg damit (Gedanke erledigt oder irrelevant). - * **Wisch nach rechts:** Behalten, verfeinern, in eine To-Do mit Fälligkeitsdatum verwandeln oder in ein Projekt einsortieren. -* **📝 Notion-Flair, aber ohne Ballast:** - Wächst ein Gedanke zum Projekt heran, wechselst du in den Workspace. Reiner, ablenkungsfreier Block-Editor mit Überschriften, Listen, Codeblöcken und Task-Elementen – ohne relationale Datenbank-Überkomplexität. -* **🛒 Geteilte Listen & Family Spaces (Echtzeit):** - Einkaufslisten oder Urlaubsplaner per Link teilen. Hakst du an der Kasse die Hafermilch ab, verschwindet sie bei deinem Partner in Millisekunden dank WebSocket-Synchronisation. -* **🤖 Webhooks & Ingestion API:** - Smarthome-Alarme (z. B. Home Assistant, Zigbee2MQTT), Cronjobs oder Webhooks speisen dringende Aufgaben oder Messpunkte direkt in deine Inbox ein. -* **🛡️ 100 % Self-Hosted & Datensouveränität:** - Keine fremde Cloud liest mit. Ein schlanker Docker-Container, SQLite mit Write-Ahead-Log (WAL) oder PostgreSQL. +### ✅ Aufgabenlisten & Kanban-Board +* **Mehrere Aufgabenlisten:** Trenne Berufliches, Privates, Projekte oder gemeinsame Team-Aufgaben sauber in eigenen Listen. +* **Liste oder Kanban:** Wechsle jederzeit mit einem Klick zwischen einer kompakten Listenansicht und einem übersichtlichen **3-Spalten-Kanban-Board** (*Zu erledigen*, *In Bearbeitung*, *Erledigt*). +* **Detailtiefe bei Bedarf:** Füge Aufgaben Fälligkeiten, Prioritäten und ausführliche Notizen hinzu – oder wandle eine wachsende Aufgabe direkt in ein großes Workspace-Dokument um. + +### 📝 Workspace: Komfort wie in Word, gespeichert als echtes Markdown +* **Formatieren auf Knopfdruck:** Nutze die übersichtliche Werkzeugleiste für Überschriften, Fett/Kursiv, Checklisten, Code-Blöcke, Hervorhebungen, Tabellen und Fortschrittsbalken – ganz ohne Markdown-Befehle auswendig lernen zu müssen. +* **Vernetztes Wiki:** Verknüpfe Dokumente untereinander über interne `[[Wiki-Links]]` und organisiere dein Wissen in einer klaren Ordner- und Unterordner-Struktur. +* **Volle Datenhoheit (Kein Lock-in):** Jedes Dokument wird im Hintergrund als saubere, standardisierte `.md`-Datei in deiner Ordnerstruktur auf der Festplatte abgelegt. Du kannst deine Notizen jederzeit direkt aus dem Dateisystem kopieren, sichern oder mit anderen Programmen nutzen. + +### 🛒 Geteilte Echtzeit-Listen +* **Gemeinsam abhaken:** Ob Wocheneinkauf, Packliste für den Urlaub oder Checkliste fürs Projekt – teile Spaces per Einladungscode mit Partnern, Familie oder Kollegen. +* **Live-Synchronisation:** Hakst du im Supermarkt einen Artikel ab, verschwindet er im selben Augenblick auf den Bildschirmen aller anderen Mitglieder. + +### 🏢 Organisationen, Teams & Berechtigungskonzept +* **Geführte Ersteinrichtung:** Beim ersten Start begrüßt dich ein Einrichtungs-Assistent, mit dem du dein Administrator-Konto und die Sicherheitseinstellungen deines Servers festlegst. +* **Geschlossene Gesellschaft oder offenes Team:** Schalte die öffentliche Registrierung ab und vergib gezielte **Einladungscodes**, oder lege Benutzer direkt über die Administration an. +* **Drei klare Rollen:** Unterscheide zwischen **Server-Admin** (volle Server- und Mail-Steuerung), **Team-Admin** (darf Nutzer einladen sowie Organisationen und Teams verwalten) und **Mitglied**. +* **Sichtbarkeits-Isolation nach Maß:** Bestimme als Administrator zentral, ob Nutzer komplett isoliert für sich arbeiten (*Strikt Privat*), nur Mitglieder ihrer eigenen Teams sehen (*Teams isoliert*) oder serverweit zusammenarbeiten (*Offen*). + +### 🔔 Multi-Channel-Alarme & Eigene Webhooks +* **Keine Frist mehr verpassen:** Lass dich vor Fälligkeit einer Aufgabe automatisch benachrichtigen – frei wählbar per **E-Mail**, **Discord-Nachricht** oder **eigenem Webhook**. +* **Offen für deine Automatisierung:** Erzeuge persönliche API-Schlüssel in deinen Kontoeinstellungen und schicke System-Alarme, Server-Statusmeldungen oder eigene Skripte per einfachem `curl`-Befehl direkt als Aufgabe oder Notiz in dein MindSift. + +### 🖤 Fokussiertes, eckiges Dark-Design +* **Ruhe fürs Auge:** Ein klares, monochromes dunkles Design mit präzisen Kanten und einer einzigen, gezielt eingesetzten Akzentfarbe. Keine Ablenkung, keine überladenen Menüs, volle Konzentration auf deine Inhalte. --- -## 🎨 Designsystem & Ästhetik: Google Gemini DNA +## 👥 Für wen ist MindSift gemacht? -MindSift setzt konsequent auf die Ästhetik von Google Gemini – organisch, hochmodern und beruhigend: - -* **Dark Mode First (OLED Deep Obsidian):** - * Hintergrund primär: `#0b0f19` / `#131314` - * Oberflächen/Karten: `#1e1f20` mit feinen `1px` Konturen (`rgba(255, 255, 255, 0.08)`) - * Akzente & Glows: Gemini Blue (`#8ab4f8`) und sanftes Violett (`#c58af9`) -* **Pill-Shapes & Soft Curves:** Stark abgerundete Eingabefelder (`rounded-full`), fließende Kapselbuttons und weiche Kanten. -* **Zurückhaltende Eleganz:** Großzügiger Weißraum (*Spreading*), subtile Ambient-Glows bei Fokus und flüssige 60fps-Gestenphysik für die Karten-Triage. -* **Haptik & Touch-Optimierung:** Große Interaktionsradien (mind. 48px Touch-Targets), Vibrations-Feedback via Web Vibration API bei Karten-Swipes. +* **Für Vielbeschäftigte & Kreative**, die tagsüber hunderte Gedanken, To-Dos und Links im Kopf jonglieren und ein System suchen, das schneller ist als jede klassische Notiz-App. +* **Für Familien, Paare & WGs**, die gemeinsame Einkaufslisten, Erledigungen und Planungen ohne Werbe-Apps und ohne fremde Cloud synchronisieren möchten. +* **Für Teams, Selbstständige & Homelab-Enthusiasten**, die Aufgaben, Projektdokumentation, Server-Webhooks und Benutzerrechte souverän auf der eigenen Infrastruktur betreiben wollen. --- -## 🏗️ Technische Architektur & Tech-Stack +## 🚀 In 60 Sekunden starten -Als Lead-Architektur wurde ein **Single-Binary / Single-Container** Paradigma gewählt. Das Go-Backend kompiliert das React-Frontend direkt in ein einziges Executable ein (`embed.FS`). +MindSift läuft als einzelner, ressourcenschonender Container und bringt alles mit, was du brauchst. -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Client (Mobile PWA / Desktop) │ -│ React 19 • Vite • Tailwind CSS v4 • Framer Motion • Tiptap Block-Editor │ -└───────────────────────────────┬─────────────────────────────────────────┘ - │ HTTP/2 / REST & WebSockets (WSS) -┌───────────────────────────────▼─────────────────────────────────────────┐ -│ MindSift Single Container (Go 1.24) │ -│ │ -│ ┌─────────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ Embedded Assets │ │ Chi HTTP Mux │ │ WebSocket Hub │ │ -│ │ (React SPA Dist) │ │ REST / Webhook │ │ (Realtime Sync) │ │ -│ └─────────────────────┘ └────────┬────────┘ └────────┬────────┘ │ -│ │ │ │ -│ ┌───────▼─────────────────────▼────────┐ │ -│ │ Business Service Layer │ │ -│ │ (Capture, Triage, Docs, Lists) │ │ -│ └──────────────────┬───────────────────┘ │ -│ │ │ -│ ┌──────────────────▼───────────────────┐ │ -│ │ Data Access Layer (sqlc / WAL) │ │ -│ └──────────────────┬───────────────────┘ │ -└─────────────────────────────────────────────────┼───────────────────────┘ - │ - ┌──────────────────▼───────────────────┐ - │ SQLite 3 (WAL Mode, zero-config) │ - │ *Optional: PostgreSQL Driver │ - └──────────────────────────────────────┘ -``` +### Start mit Docker Compose -### Stack-Entscheidungen & Rationale - -| Schicht | Technologie | Begründung & Architekturentscheidung | -| :--- | :--- | :--- | -| **Backend** | **Go 1.24+ (Chi Router)** | Extrem geringer Speicherverbrauch (~15–25 MB RAM idle), Kaltstart in < 5 ms, überlegene Concurrency für WebSockets, erzeugt ein statisches Binary. | -| **Frontend** | **React 19 + TypeScript + Vite** | Bietet mit **Framer Motion** die beste Gesten- und Physik-Engine für flüssige Tinder-Swipes und mit **Tiptap** den industrieweit führenden blockbasierten Rich-Text-Editor. | -| **Styling** | **Tailwind CSS v4** | Höchste Performance, null CSS-Runtime-Overhead, perfekte Abbildung des Gemini Dark-Themas mit Design-Tokens. | -| **Realtime** | **Go WebSockets (`nhooyr/websocket`)** | Schlanke, native WebSocket-Rooms für kollaborative Einkaufslisten mit Sub-10ms-Latenz und automatischem Reconnect. | -| **Datenbank** | **SQLite (WAL-Modus) / sqlc** | Standardmäßig Zero-Config mit Write-Ahead-Logging, ACID-sicher und als einzelne Datei einfachst per Backup sicherbar. Optional umschaltbar auf PostgreSQL per `DATABASE_URL`. | -| **PWA / Offline** | **Workbox / IndexedDB** | App startet auch im Funkloch sofort; Gedanken werden lokal vorgehalten und beim nächsten Verbindungsaufbau transparent synchronisiert. | -| **Container** | **Distroless / Alpine Scratch** | Minimalistisches Multi-Stage Dockerfile. End-Image-Größe < 35 MB. Keine Sicherheitsrisiken durch überflüssige System-Pakete. | - ---- - -## 🗄️ Datenbankschema - -Das Schema ist relational strukturiert, normalisiert und auf Lese- sowie Schreibgeschwindigkeit im Sub-Millisekundenbereich optimiert. - -```mermaid -erDiagram - USERS ||--o{ THOUGHTS : owns - USERS ||--o{ DOCUMENTS : authors - USERS ||--o{ SPACE_MEMBERS : participates - USERS ||--o{ API_KEYS : possesses - SPACES ||--o{ SPACE_MEMBERS : contains - SPACES ||--o{ LISTS : hosts - LISTS ||--o{ LIST_ITEMS : contains - DOCUMENTS ||--o{ DOCUMENT_BLOCKS : contains - - USERS { - uuid id PK - string email UK - string password_hash - string display_name - timestamp created_at - } - - THOUGHTS { - uuid id PK - uuid user_id FK - text content - string voice_transcript - string status "inbox | kept | discarded | converted" - string converted_type "task | document | list_item" - uuid converted_ref_id - timestamp due_date - timestamp created_at - timestamp triaged_at - } - - DOCUMENTS { - uuid id PK - uuid user_id FK - uuid space_id FK - string title - string icon - boolean is_archived - timestamp created_at - timestamp updated_at - } - - DOCUMENT_BLOCKS { - uuid id PK - uuid document_id FK - string block_type "paragraph | heading | task | code" - jsonb content - int sort_order - timestamp updated_at - } - - SPACES { - uuid id PK - string name - string icon - boolean is_personal - timestamp created_at - } - - SPACE_MEMBERS { - uuid space_id PK,FK - uuid user_id PK,FK - string role "owner | editor | viewer" - timestamp joined_at - } - - LISTS { - uuid id PK - uuid space_id FK - string title - string list_type "shopping | checklist" - timestamp updated_at - } - - LIST_ITEMS { - uuid id PK - uuid list_id FK - string content - boolean is_completed - uuid completed_by FK - timestamp completed_at - int sort_order - } - - API_KEYS { - uuid id PK - uuid user_id FK - string name - string key_hash UK - string scopes - timestamp last_used_at - } -``` - -### SQL DDL Definitionen (Auszug SQLite / Postgres kompatibel) - -```sql --- Aktivierung von WAL & Foreign Keys für SQLite -PRAGMA journal_mode = WAL; -PRAGMA foreign_keys = ON; -PRAGMA busy_timeout = 5000; - --- Gedanken & Instant Inbox Puffer -CREATE TABLE IF NOT EXISTS thoughts ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - content TEXT NOT NULL, - status TEXT CHECK(status IN ('inbox', 'kept', 'discarded', 'converted')) DEFAULT 'inbox', - converted_type TEXT CHECK(converted_type IN ('task', 'document', 'list_item', NULL)), - converted_ref_id TEXT, - due_date DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - triaged_at DATETIME, - FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE INDEX idx_thoughts_user_status ON thoughts(user_id, status, created_at DESC); - --- Kollaborative Listen (z.B. Familie & Einkaufen) -CREATE TABLE IF NOT EXISTS list_items ( - id TEXT PRIMARY KEY, - list_id TEXT NOT NULL, - content TEXT NOT NULL, - is_completed BOOLEAN NOT NULL DEFAULT 0, - completed_by TEXT, - sort_order INTEGER NOT NULL DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - completed_at DATETIME, - FOREIGN KEY(list_id) REFERENCES lists(id) ON DELETE CASCADE -); - -CREATE INDEX idx_list_items_sync ON list_items(list_id, is_completed, sort_order ASC); -``` - ---- - -## 🔌 API-Spezifikation & Webhooks - -Alle Endpunkte antworten mit `application/json` und nutzen Standard-HTTP-Statuscodes. Die Authentifizierung erfolgt über `Bearer ` oder dedizierte Ingestion-Keys. - -### 1. Instant Capture & Ingestion (Sub-Second Entry) - -```http -POST /api/v1/inbox -Authorization: Bearer -Content-Type: application/json - -{ - "content": "Zündkerzen für den Rasenmäher bestellen #garage", - "due_date": "2026-10-25T18:00:00Z" // Optional -} -``` -**Response (201 Created - < 30ms):** -```json -{ - "id": "th_9f8a3c21", - "status": "inbox", - "created_at": "2026-10-24T16:25:00Z" -} -``` - -### 2. Smarthome & Externe Webhook Ingestion - -Für Home Assistant, Node-RED, n8n oder Shell-Skripte: - -```http -POST /api/v1/webhooks/ingest?token=mindsift_sec_789xyz -Content-Type: application/json - -{ - "title": "Kellerfeuchtigkeit über 70%", - "priority": "urgent", - "tags": ["smarthome", "keller"] -} -``` - -### 3. Triage & Tinder-Style Swipe Deck - -* **Gedanken-Deck laden:** `GET /api/v1/triage/deck?limit=20` -* **Gedanke wischen (Swipe Action):** -```http -PATCH /api/v1/triage/th_9f8a3c21 -Authorization: Bearer -Content-Type: application/json - -{ - "action": "keep", // oder "discard" - "convert_to": "task", // optional: "task" | "document" | "list_item" - "target_list_id": "lst_shopping_123", // wenn convert_to = "list_item" - "due_date": "2026-10-28T09:00:00Z" -} -``` - -### 4. Realtime WebSocket Protocol (Kollaborative Listen) - -Client verbindet sich zu `wss://mindsift.local/api/v1/ws/spaces/:space_id`. - -* **Event: Item hinzugefügt/aktualisiert** -```json -{ - "event": "ITEM_UPDATED", - "payload": { - "list_id": "lst_shopping_123", - "item_id": "itm_456", - "content": "Hafermilch (Barista)", - "is_completed": true, - "user_name": "Sandro" - } -} -``` - ---- - -## 🚀 Gitea CI/CD Pipeline - -Das Projekt ist für den Betrieb mit **Gitea Actions** optimiert. Bei jedem Push auf `main` oder einem Release-Tag baut die Pipeline automatisch ein Multi-Arch-Docker-Image (`linux/amd64`, `linux/arm64`) und schiebt es in die Gitea Container Registry. - -### Workflow-Datei: `.gitea/workflows/build-and-publish.yaml` - -```yaml -name: MindSift Multi-Arch Docker Build - -on: - push: - branches: - - main - tags: - - 'v*' - pull_request: - branches: - - main - -jobs: - test: - name: Lint & Unit Tests - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '1.24' - cache: true - - - name: Run Backend Tests - run: | - go vet ./... - go test -v -race ./... - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - - name: Run Frontend Tests - run: | - cd frontend - npm ci - npm run lint - npm run test:run - - build-docker: - name: Build & Push Docker Image - needs: test - runs-on: ubuntu-latest - if: github.event_name == 'push' - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Set up QEMU (Multi-Arch) - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Gitea Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ gitea.server_url }} - username: ${{ gitea.actor }} - password: ${{ secrets.GITEA_TOKEN }} - - - name: Extract Metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ gitea.server_url }}/${{ gitea.repository }} - tags: | - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - type=semver,pattern={{version}} - - - name: Build and Push - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max -``` - -### Multi-Stage Dockerfile (`Dockerfile`) - -```dockerfile -# Stage 1: Frontend Build -FROM node:22-alpine AS frontend-builder -WORKDIR /app/frontend -COPY frontend/package*.json ./ -RUN npm ci -COPY frontend/ ./ -RUN npm run build - -# Stage 2: Go Backend Build -FROM golang:1.24-alpine AS backend-builder -WORKDIR /app -RUN apk add --no-cache build-base git -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -# Kopiere gebaute Frontend Assets in das embed.FS-Verzeichnis -COPY --from=frontend-builder /app/frontend/dist ./server/dist -RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-s -w -extldflags '-static'" -o mindsift ./cmd/server - -# Stage 3: Minimal Production Image -FROM alpine:3.21 -RUN apk add --no-cache ca-certificates tzdata -WORKDIR /app -COPY --from=backend-builder /app/mindsift /app/mindsift - -ENV PORT=8080 -ENV DATA_DIR=/data -VOLUME ["/data"] - -EXPOSE 8080 -USER 1000:1000 -ENTRYPOINT ["/app/mindsift"] -``` - ---- - -## 🐳 Deployment & Schnellstart - -### 1. Schnellstart mit Docker Compose (Empfohlen) - -Erstelle eine `docker-compose.yml`: +Lege eine `docker-compose.yml` an: ```yaml services: mindsift: - image: gitea.your-domain.com/user/mindsift:latest + image: git.dsit.me/sandrodach/mindsift:dev container_name: mindsift restart: unless-stopped ports: @@ -501,109 +93,39 @@ services: environment: - PORT=8080 - DATA_DIR=/data - - JWT_SECRET=change_this_to_a_secure_random_key_min_32_chars - - BASE_URL=https://mindsift.local - - LOG_LEVEL=info + - JWT_SECRET=bitte-hier-ein-langes-sicheres-geheimnis-eintragen + - ALLOW_REGISTRATION=false + # Optional: Eigener SMTP-Server für E-Mail-Alarme + # (kann alternativ auch später direkt in der Admin-Oberfläche eingetragen werden) + - SMTP_HOST= + - SMTP_PORT=587 + - SMTP_USER= + - SMTP_PASS= + - SMTP_FROM= + - SMTP_TLS=true ``` -Starte den Service: +Container starten: + ```bash docker compose up -d ``` -Die Weboberfläche steht sofort unter `http://localhost:8080` bereit. -### 2. Standalone Docker Run - -```bash -docker run -d \ - --name mindsift \ - --restart unless-stopped \ - -p 8080:8080 \ - -v $(pwd)/mindsift-data:/data \ - -e JWT_SECRET="dein-super-sicheres-geheimnis" \ - gitea.your-domain.com/user/mindsift:latest -``` - -### Umgebungsvariablen - -| Variable | Standardwert | Beschreibung | -| :--- | :--- | :--- | -| `PORT` | `8080` | Interner HTTP/WebSocket Port | -| `DATA_DIR` | `/data` | Verzeichnis für SQLite DB, Uploads und Cache | -| `DATABASE_URL` | `sqlite:///data/mindsift.db` | Optional: PostgreSQL DSN (`postgres://user:pass@host:5432/db`) | -| `JWT_SECRET` | *Pflichtfeld* | Secret für Authentifizierungs-Tokens (mind. 32 Zeichen) | -| `BASE_URL` | `http://localhost:8080` | Externe URL für Einladungslinks und Webhooks | -| `ALLOW_REGISTRATION`| `true` | Erstregistrierung erlauben (`false` nach Admin-Erstellung) | +Öffne anschließend **`http://localhost:8080`** im Browser. Beim allerersten Aufruf führt dich MindSift automatisch durch die **Ersteinrichtung** für dein Administrator-Konto. --- -## 🗺️ Schrittweiser Phasenplan +## 🔒 Deine Daten gehören dir -``` -Phase 1: Foundation ────► Phase 2: Triage Deck ────► Phase 3: Workspace - (Go + PWA + DB) (Framer Physics) (Tiptap Blocks) - │ -Phase 6: Hardening ◄──── Phase 5: Webhooks ◄──── Phase 4: Realtime - (CI/CD + Release) (Smarthome Ingest) (Family Lists) -``` - -### Phase 1: Core Foundation & Instant Capture (Sprint 1) -- [x] Repository-Initialisierung & Architektur-Design -- [x] Go Backend-Grundgerüst (Chi Router, Konfigurations-Management, Healthcheck) -- [x] SQLite Storage-Layer mit WAL-Modus und automatischer Migration -- [x] JWT-Authentifizierung & User-Management -- [x] Mobile-First PWA Setup (Vite, React, Tailwind CSS, Gemini Dark Theme) -- [x] **Instant-Capture-Interface:** Autofokus-Eingabemaske, Sprachaufnahme (Web Speech API), Latenz < 300 ms - -### Phase 2: Tinder-Style Triage & Task Engine (Sprint 2) -- [x] Interaktives Swipe-Deck mit **Framer Motion** (Gesten, Rotation, elastische Rückfederung) -- [x] Triage-Status-Engine (`inbox` -> `kept` / `discarded` / `converted`) -- [x] Quick-Convert: Wisch nach rechts transformiert Gedanken direkt in Aufgaben mit Due-Date, Projekt-Notizen oder Listen-Einträge -- [x] Tastatur-Shortcuts für Power-User am Desktop (`H`/`L` oder Pfeiltasten, `C`/`/` für Capture) -- [x] Haptisches Feedback auf Mobilgeräten (Web Vibration API) - -### Phase 3: Notion-Style Workspace (Sprint 3) -- [x] Block-Editor (Headings, Task-Checkboxes, Code-Blöcke, Callouts, Listen) -- [x] Dokumenten-Verwaltung für vertiefte Gedanken inkl. Pinning & Icon-Picker -- [x] Tagging & Volltextsuche über Gedanken und Dokumente -- [x] Markdown Export (`.md`) - -### Phase 4: Realtime Spaces & Family Sync (Sprint 4) -- [x] Multi-User Space-Architektur (Familien- und Team-Räume mit Invite-Codes) -- [x] Go WebSocket Hub mit Raum-Multiplexing -- [x] Echtzeit-Einkaufslisten: Sofortige Synchronisation von Checked-States über alle Endgeräte -- [x] Optimistische UI-Updates für verzögerungsfreies Offline-/Online-Erlebnis - -### Phase 5: Smarthome Webhooks & External Ecosystem (Sprint 5) -- [x] Ingestion-API mit API-Token-Management (`mindsift_sec_...`) -- [x] Webhook-Endpunkte für Home Assistant, Zigbee2MQTT, n8n und Cronjobs -- [x] Interaktiver Live-Webhook-Simulator in der UI -- [x] Backup- und Export-Endpunkt (kompletter JSON-Export) - -### Phase 6: Production Hardening, CI/CD & v1.0 (Sprint 6) -- [x] Gitea Actions Multi-Arch Workflow (`amd64`, `arm64`) -- [x] Single-Container Packaging (`embed.FS`) & Multi-Stage Dockerfile -- [x] Automatisierte Backend-Unit-Tests (`internal/storage/sqlite_test.go`) -- [ ] End-to-End Tests (Playwright) auf Ziel-Cluster -- [x] Erstellung der offiziellen Dokumentation & v1.0 Release-Bereitschaft - ---- - -## 🔒 Sicherheit & Datenschutz - -* **Zero Tracking:** Keine Analytics, keine externen CDNs, keine Telemetrie. -* **Content Security Policy (CSP):** Strikte Header; alle Assets und Fonts werden lokal ausgeliefert. -* **Sichere Passwörter:** Argon2id Passwort-Hashing nach OWASP-Empfehlung. -* **Isolierte Container:** Der Container läuft standardmäßig unter einer Non-Root-UID (`1000:1000`). +* **100 % Self-Hosted:** Keine externen Cloud-Dienste, keine versteckte Telemetrie, kein Tracking. +* **Portabel & Transparent:** Alle Workspace-Notizen liegen als lesbare Markdown-Dateien im Datenverzeichnis; zusätzlich kannst du jederzeit ein komplettes Backup deiner Daten als JSON-Datei herunterladen. --- ## 📄 Lizenz -Dieses Projekt ist lizenziert unter der **MIT-Lizenz** – siehe [LICENSE](LICENSE) für Details. - ---- +Veröffentlicht unter der **MIT-Lizenz** – siehe [LICENSE](LICENSE).
-Designed with ❤️ for clarity, speed and digital sovereignty. +Gebaut für maximale Klarheit, Geschwindigkeit und digitale Souveränität.
\ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 201bdf6..da1b50e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -31,10 +31,8 @@ func main() { } defer store.Close() - // Ensure initial default workspace exists on fresh self-hosted boot - if _, err := store.EnsureDefaultUser(); err != nil { - log.Printf("Warning: could not seed default user: %v", err) - } + // Seed SMTP defaults from environment variables into DB if not yet configured in DB + store.SeedSMTPDefaultsFromEnv(cfg.SMTPHost, cfg.SMTPPort, cfg.SMTPUser, cfg.SMTPPass, cfg.SMTPFrom, cfg.SMTPTLS) wsHub := ws.NewHub() diff --git a/docker-compose.yml b/docker-compose.yml index 1d214ca..d06a86f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: build: context: . dockerfile: Dockerfile - image: git.dsit.me/sandrodach/mindsift:latest + image: git.dsit.me/sandrodach/mindsift:dev container_name: mindsift restart: unless-stopped ports: @@ -15,5 +15,12 @@ services: - DATA_DIR=/data - JWT_SECRET=${JWT_SECRET:-change_this_to_a_secure_random_key_min_32_chars} - BASE_URL=${BASE_URL:-http://localhost:8080} - - ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-true} + - ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-false} - LOG_LEVEL=${LOG_LEVEL:-info} + # Optional SMTP / Mail Configuration (can also be configured in Admin UI) + - SMTP_HOST=${SMTP_HOST:-} + - SMTP_PORT=${SMTP_PORT:-587} + - SMTP_USER=${SMTP_USER:-} + - SMTP_PASS=${SMTP_PASS:-} + - SMTP_FROM=${SMTP_FROM:-} + - SMTP_TLS=${SMTP_TLS:-true} diff --git a/frontend/index.html b/frontend/index.html index 4661231..b020e97 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -15,10 +15,13 @@
diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..ef3a8a0 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2744 @@ +{ + "name": "mindsift-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mindsift-frontend", + "version": "1.0.0", + "dependencies": { + "framer-motion": "^11.15.0", + "lucide-react": "^0.468.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.65.0.tgz", + "integrity": "sha512-t2AnVba25baFdq7gczzS13JyVcdubLVbFX1X04mxrggDrTQPUXNv3XM0k9H1hr5tqvg7AVvtQ720QHAj0iYqhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.65.0.tgz", + "integrity": "sha512-dNONUaEwPGhl/nuO2KV8VABWlcjYyU6KtZGN8dQv5OS5JU7JSCxPijVNKXbRJd0iuTzTiJ4TjPt/4bbvDRNiMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.65.0.tgz", + "integrity": "sha512-zWQqUH+9zavr2Bxuzfpuv2SIF3X31cr58TvSQC9o1GhpOvkqRwr60GkZdQcudyT/BEgdpdSafjgd2I13MbTL7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.65.0.tgz", + "integrity": "sha512-RQYreBFWtO4//hIuOaZkYUnF/lPU4lz35dveDimDshWCJvVisVtJ/shmT9bkXK8wFJY+FJF89X6F3Cq1qI3LCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.65.0.tgz", + "integrity": "sha512-A0NfMj6Cuh/9BE5k3awJKv6HoxFVXIcLAb6SZqAMoxUHPA1ZNNK4RsGuUPD/vL7nEAm9LG+ZzXWWTrnC0XtkpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.65.0.tgz", + "integrity": "sha512-BI6/H7q3aP0I7S7GOrjyxTjgEJlSnovXE38+bdxBpuKN6z+d//uF+T+VQ3Q7ltJL/71p17ufartARX9Ja5gOGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.65.0.tgz", + "integrity": "sha512-W1vUprf09RWaVWnsFP1vlUkEWJzTZlpObOEAYZXt9Fq0UVAOYEkeGr7wBX7McKqqIQskXdBnpiOMgwPujordgA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.65.0.tgz", + "integrity": "sha512-s8QTcZWgrl9haFzEGIqyE5BWyo89pFDGLMBFZ752WYxk8DJwNU/O1X6pZgPw829d9/sm6CCHyrMP3aurt76p1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.65.0.tgz", + "integrity": "sha512-ke1f6dLRzkqveyjjF2fkEwQYZvxHtNNwjegY8nJrwcO0Bdqr8CzFO/pKE0Bns13cx0qz2Ba2Y0Hs3dSwMdnHxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.65.0.tgz", + "integrity": "sha512-926SMeUcy4y18Rz/w2aJiUKOnyAnm6czg72bXkQEaeemsll+Wr0hnsjb2BGRpHvWgwAMYy5ACJCotbvfLCfj0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.65.0.tgz", + "integrity": "sha512-6T7JNRP1hc4hqF2dUsEoVLOt9K8tmODoCLldtQRYnrddGGQY8Gqe2pM/qMMUo7EMowpP7oa2N95k0/8sYK/ipw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.65.0.tgz", + "integrity": "sha512-SImGFl1Je5kpyAgOCPONVT7h6ASV8mAUwGRjY7eZvVjPg//PfHOXgI+P241RF/+HqsA4PP3qD527ysWFKNQyhQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.65.0.tgz", + "integrity": "sha512-XeDTbmPF1Yu5asfXGPrMkWoxTDhgryQbmw5Q1+R582O51VNCk23J6f2pDrsAp5YkneBVBwtExx6ZGOb9D2aQ6A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.65.0.tgz", + "integrity": "sha512-VS55JUI/1nsljmV/jfqNkLZXFw8uMO8nNlfoHmSevNf6fO3neuhyDvx9dv3c/Ytt8r/431SL8Qax1dpYvOMZTg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.65.0.tgz", + "integrity": "sha512-Vblw84TbX7RS4la1Y4+yQ7aore/oSyaNtFbppuBr5Vs/4LfBeXKq9dIfoExw6zOvRERqd2rRAjnZRaMhH/0x5w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.65.0.tgz", + "integrity": "sha512-7+gHjtONvKgwR9InTKFaLq53f6YzBJkgeMRBENdWTsQr+spahGqQm8HF5aXpq8i4JaRrOqYyv6Zn1WltDTxaZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.65.0.tgz", + "integrity": "sha512-KvNDC0J6s4vVHhtJ6KrDXU0Er0au/hmRIBq/+1maCV8TseGupeKizq0bWcrbp2HE1T8xoknvN0Q/KZ/YF0qX3w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.65.0.tgz", + "integrity": "sha512-GExdD15WPvu/x/drELjVam7vX8EsPgvTMOa/iJL/p/4m/oOqzbb7JXZg8fXR4hlXtg/sF2zyTZ3bR0AEtEMlhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.65.0.tgz", + "integrity": "sha512-8sT9PemIZfIMED4Vd4KGoOXVDj5CMSWNcGdbheRUx9TYS03yOWdT2Q0lXPyEyNBB9YTilwewIQKNIBk6Nxi0Kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.65.0.tgz", + "integrity": "sha512-pO6Ehi0jo5yE0R6PltKhJhv9mlIiyzBJk5N5NUF2OF6xtVKI+1bdXxj/rb0jGXy3hck1ELVT0QPY3tzkAm360w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.65.0.tgz", + "integrity": "sha512-vo7N3/+cjjPDYr8vtxC+qSNgWa9sDvJ7g8N8R5SWqFY4WiNH1ULcAU70SKVyHeiIZOYYwNP9pTipmhN5sl1NPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.65.0.tgz", + "integrity": "sha512-ArwvgFAbjKcDM7qqotZ4OVYf+UMT9xH2YzkCzK4+AuZSDihMlz1Ekoe4INwBVXzn2mnJzuIYOcSx07iTBijGRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.65.0.tgz", + "integrity": "sha512-QGMmK4/cxKE/36FoLMSHQBCyymv/eFTnsCvvsBF0rgmOvVbvX78VWZRerL3dHa77f4pM6Xcc2iGDhJ10w/svKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.65.0.tgz", + "integrity": "sha512-XfKYFWHIPs4HPODIMsGF1IKlS3H1e3WokUZIgpmn4+uNnZBlnRDDHEylNyazbQXwhMKZre68OjtRYbKqb5+j8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.65.0.tgz", + "integrity": "sha512-wPWi++K4Bs1VR4XihOhfGi4IGX6Cub7FlerTf8X2v+2BtDHddgSqWCXwGD+h2yAe4/niIhNDwAltioOC+wEogQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz", + "integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.9", + "caniuse-lite": "^1.0.30001810", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.32.tgz", + "integrity": "sha512-UvuxFU9P3hM29LrQdwu0cD6w6/+SFr+UlB9s/knI7+O2rK9xFxKUwSDWxGdbbqQ9/V7BJ5s0NI5nQAE4uJp3gQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.29", + "caniuse-lite": "^1.0.30001816", + "electron-to-chromium": "^1.5.459", + "node-releases": "^2.0.59", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001819", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", + "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.464", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz", + "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.4.tgz", + "integrity": "sha512-zJZyBmQhArDRefuWTypZxwDpFDIYmrj8ypSBvwGD5P13Y95s6DjzKgu4DVdGbeQa5PDEsAOCAPNk4X+QOR006w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", + "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.5.tgz", + "integrity": "sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.13", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", + "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.17.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.65.0.tgz", + "integrity": "sha512-QVCvQUTGDN6JRwi3jVGCY3H4aTHvi9rX0LyoBMkGUvdh/Z6O51CzcLz85BWZaTCDGhl6ySGqYZs+xI1jmPqGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.65.0", + "@rollup/rollup-android-arm64": "4.65.0", + "@rollup/rollup-darwin-arm64": "4.65.0", + "@rollup/rollup-darwin-x64": "4.65.0", + "@rollup/rollup-freebsd-arm64": "4.65.0", + "@rollup/rollup-freebsd-x64": "4.65.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.65.0", + "@rollup/rollup-linux-arm-musleabihf": "4.65.0", + "@rollup/rollup-linux-arm64-gnu": "4.65.0", + "@rollup/rollup-linux-arm64-musl": "4.65.0", + "@rollup/rollup-linux-loong64-gnu": "4.65.0", + "@rollup/rollup-linux-loong64-musl": "4.65.0", + "@rollup/rollup-linux-ppc64-gnu": "4.65.0", + "@rollup/rollup-linux-ppc64-musl": "4.65.0", + "@rollup/rollup-linux-riscv64-gnu": "4.65.0", + "@rollup/rollup-linux-riscv64-musl": "4.65.0", + "@rollup/rollup-linux-s390x-gnu": "4.65.0", + "@rollup/rollup-linux-x64-gnu": "4.65.0", + "@rollup/rollup-linux-x64-musl": "4.65.0", + "@rollup/rollup-openbsd-x64": "4.65.0", + "@rollup/rollup-openharmony-arm64": "4.65.0", + "@rollup/rollup-win32-arm64-msvc": "4.65.0", + "@rollup/rollup-win32-ia32-msvc": "4.65.0", + "@rollup/rollup-win32-x64-gnu": "4.65.0", + "@rollup/rollup-win32-x64-msvc": "4.65.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/public/sw.js b/frontend/public/sw.js index 99f1384..86a830a 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1,33 +1,18 @@ -const CACHE_NAME = 'mindsift-pwa-v1'; -const ASSETS = ['/', '/index.html', '/manifest.json']; - -self.addEventListener('install', (event) => { - event.waitUntil( - caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)) - ); +// Network-first / cache-clearing service worker to ensure fresh builds across all browsers +self.addEventListener('install', () => { self.skipWaiting(); }); self.addEventListener('activate', (event) => { event.waitUntil( - caches.keys().then((keys) => - Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) - ) + caches + .keys() + .then((keys) => Promise.all(keys.map((k) => caches.delete(k)))) + .then(() => self.clients.claim()) ); - self.clients.claim(); }); -self.addEventListener('fetch', (event) => { - if (event.request.method !== 'GET' || event.request.url.includes('/api/')) { - return; - } - event.respondWith( - fetch(event.request) - .then((res) => { - const clone = res.clone(); - caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); - return res; - }) - .catch(() => caches.match(event.request).then((cached) => cached || caches.match('/'))) - ); +self.addEventListener('fetch', () => { + // Always use native browser network fetch so API calls and fresh Vite bundles are never intercepted by stale caches + return; }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 62712e6..493114c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,27 +1,91 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Layers, CheckSquare, FileText, ShoppingCart, - Webhook, Brain, + Settings, + Shield, LogOut, - User as UserIcon, - Sparkles, + ChevronDown, + RotateCcw, + Palette, } from 'lucide-react'; -import { DashboardStats, SharedList, Thought, User } from './lib/types'; -import { api, clearToken, getToken } from './lib/api'; +import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types'; +import { api, clearToken, getToken, triggerHaptic } from './lib/api'; +import { + AVAILABLE_THEMES, + initThemeOnBoot, + setActiveTheme as applyTheme, + ThemeId, +} from './lib/theme'; import { InstantCaptureBar } from './features/capture/InstantCaptureBar'; import { TriageDeck } from './features/triage/TriageDeck'; import { TasksView } from './features/triage/TasksView'; import { WorkspaceView } from './features/workspace/WorkspaceView'; import { SharedListsView } from './features/lists/SharedListsView'; -import { EcosystemView } from './features/settings/EcosystemView'; +import { SetupAndAuthView } from './features/auth/SetupAndAuthView'; +import { UserSettingsView } from './features/settings/UserSettingsView'; +import { AdminSettingsView } from './features/settings/AdminSettingsView'; -type ActiveTab = 'triage' | 'tasks' | 'workspace' | 'lists' | 'ecosystem'; +type ActiveTab = + | 'triage' + | 'tasks' + | 'workspace' + | 'lists' + | 'user_settings' + | 'admin_settings'; + +interface ErrorBoundaryState { + hasError: boolean; + errorText: string; +} + +class ViewErrorBoundary extends React.Component< + { children: React.ReactNode; onReset: () => void }, + ErrorBoundaryState +> { + constructor(props: { children: React.ReactNode; onReset: () => void }) { + super(props); + this.state = { hasError: false, errorText: '' }; + } + + static getDerivedStateFromError(err: Error): ErrorBoundaryState { + return { hasError: true, errorText: err?.message || 'Unbekannter Darstellungsfehler' }; + } + + render() { + if (this.state.hasError) { + return ( +
+

Ansicht konnte nicht geladen werden

+

{this.state.errorText}

+ +
+ ); + } + return this.props.children; + } +} export const App: React.FC = () => { + const [activeTheme, setActiveThemeState] = useState(() => initThemeOnBoot()); + const [bootLoading, setBootLoading] = useState(true); + const [authStatus, setAuthStatus] = useState({ + needsSetup: false, + allowRegistration: false, + instanceName: 'MindSift', + }); const [user, setUser] = useState(null); const [stats, setStats] = useState({ inboxCount: 0, @@ -36,14 +100,17 @@ export const App: React.FC = () => { const [activeTab, setActiveTab] = useState('triage'); const [selectedDocId, setSelectedDocId] = useState(null); const [captureFocusCount, setCaptureFocusCount] = useState(0); + const [refreshRevision, setRefreshRevision] = useState(0); - // Auth Modal State for switching or registering additional family members - const [showAuthModal, setShowAuthModal] = useState(false); - const [authMode, setAuthMode] = useState<'login' | 'register'>('login'); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [displayName, setDisplayName] = useState(''); - const [authError, setAuthError] = useState(''); + // Profile Dropdown State + const [showProfileMenu, setShowProfileMenu] = useState(false); + const dropdownRef = useRef(null); + + const handleChangeTheme = (themeId: ThemeId) => { + triggerHaptic(10); + applyTheme(themeId); + setActiveThemeState(themeId); + }; const refreshAll = async () => { try { @@ -54,52 +121,97 @@ export const App: React.FC = () => { ]); setUser(meRes.user); setStats(meRes.stats); - setDeck(deckRes); - setLists(listsRes); + setDeck(Array.isArray(deckRes) ? deckRes : []); + setLists(Array.isArray(listsRes) ? listsRes : []); } catch { - // Fallback to quickStart session - try { - const qs = await api.quickStart(); - setUser(qs.user); - const [st, dk, ls] = await Promise.all([ - api.getStats(), - api.getTriageDeck(), - api.getAllLists(), - ]); - setStats(st); - setDeck(dk); - setLists(ls); - } catch { - // Offline or waiting for server + // If token expired or invalid, api.ts dispatches mindsift:unauthorized + } + }; + + const checkBootAndAuth = async () => { + setBootLoading(true); + try { + const st = await api.getAuthStatus(); + setAuthStatus(st); + + if (st.needsSetup) { + clearToken(); + setUser(null); + return; } + + if (getToken()) { + await api.flushOfflineQueue(); + await refreshAll(); + } else { + setUser(null); + } + } catch { + // Server temporarily unreachable + } finally { + setBootLoading(false); } }; useEffect(() => { - const init = async () => { - if (!getToken()) { - try { - await api.quickStart(); - } catch { - // ignore - } + checkBootAndAuth(); + + const onUnauthorized = async () => { + setUser(null); + setShowProfileMenu(false); + try { + const st = await api.getAuthStatus(); + setAuthStatus(st); + } catch { + // ignore } - await api.flushOfflineQueue(); - await refreshAll(); }; - init(); const onOnline = async () => { - const synced = await api.flushOfflineQueue(); - if (synced > 0) refreshAll(); + if (getToken()) { + const synced = await api.flushOfflineQueue(); + if (synced > 0) refreshAll(); + } }; + + window.addEventListener('mindsift:unauthorized', onUnauthorized); window.addEventListener('online', onOnline); - return () => window.removeEventListener('online', onOnline); + return () => { + window.removeEventListener('mindsift:unauthorized', onUnauthorized); + window.removeEventListener('online', onOnline); + }; }, []); - const handleCaptured = (thought: Thought) => { - setDeck((prev) => [thought, ...prev]); - setStats((prev) => ({ ...prev, inboxCount: prev.inboxCount + 1 })); + // Close profile dropdown on outside click + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setShowProfileMenu(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleAuthenticated = async (loggedInUser: User) => { + setUser(loggedInUser); + setAuthStatus((prev) => ({ ...prev, needsSetup: false })); + setActiveTab('triage'); + await refreshAll(); + }; + + const handleCaptured = ( + thought: Thought, + target: 'inbox' | 'task' | 'list_item' | 'document' = 'inbox' + ) => { + if (target === 'inbox' && thought.status === 'inbox') { + setDeck((prev) => [thought, ...prev]); + setStats((prev) => ({ ...prev, inboxCount: prev.inboxCount + 1 })); + } else if (target === 'document' && thought.convertedRefId) { + setSelectedDocId(thought.convertedRefId); + setActiveTab('workspace'); + } + setRefreshRevision((r) => r + 1); refreshAll(); }; @@ -108,314 +220,331 @@ export const App: React.FC = () => { setActiveTab('workspace'); }; - const handleAuthSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setAuthError(''); + const handleTriggerAddButton = () => { + triggerHaptic(20); + window.scrollTo({ top: 0, behavior: 'smooth' }); + setCaptureFocusCount((c) => c + 1); + }; + + const handleLogout = async () => { + triggerHaptic(20); + clearToken(); + setUser(null); + setShowProfileMenu(false); + setActiveTab('triage'); try { - if (authMode === 'login') { - await api.login(email, password); - } else { - await api.register(email, password, displayName); - } - setShowAuthModal(false); - setEmail(''); - setPassword(''); - setDisplayName(''); - await refreshAll(); - } catch (err: any) { - setAuthError(err.message || 'Authentifizierung fehlgeschlagen'); + const st = await api.getAuthStatus(); + setAuthStatus(st); + } catch { + // ignore } }; - return ( -
- {/* Ambient Top Gemini Glow */} -
+ if (bootLoading) { + return ( +
+ Verbinde mit MindSift… +
+ ); + } - {/* Header Bar */} -
-
- {/* Brand Logo */} + // Show Setup Wizard or Login Screen if needsSetup or not logged in + if (authStatus.needsSetup || !user) { + return ( + + ); + } + + const isStaff = user.role === 'admin' || user.role === 'team_admin'; + const roleLabel = + user.role === 'admin' + ? 'Server-Admin' + : user.role === 'team_admin' + ? 'Team-Admin' + : 'Mitglied'; + + return ( +
+ {/* Top Header Bar */} +
+
+ {/* Logo & Headline */}
{ setActiveTab('triage'); - setCaptureFocusCount((c) => c + 1); + handleTriggerAddButton(); }} - className="flex items-center gap-2.5 cursor-pointer group" + className="flex items-center gap-2.5 cursor-pointer group select-none flex-shrink-0" > -
- +
+
-
- - MindSift +
+ + {authStatus.instanceName || 'MindSift'} - - Der Kopf frei in drei Sekunden. + + Der Kopf frei in drei Sekunden
- {/* Right User Pill */} -
+ {/* Quick-Capture Input Bubble directly in Header (above Tabs) */} +
+ +
+ + {/* User Profile Pill & Dropdown Menu */} +
+ + {showProfileMenu && ( +
+ {/* User Info Header */} +
+

{user.displayName}

+

{user.email}

+ + {roleLabel} + +
+ + {/* Theme Quick Switcher */} +
+ + + Design-Theme + +
+ {AVAILABLE_THEMES.map((t) => ( + + ))} +
+
+ + {/* Menu Items */} +
+ + + {isStaff && ( + + )} +
+ + {/* Logout */} +
+ +
+
+ )} +
+
+ + {/* Navigation tabs attached to header */} +
+
- {/* Main Content Container */} -
- {/* Global Instant Capture Section (Always Ready at Top) */} -
- -
- - {/* Pill Navigation Tabs */} - + {activeTab === 'user_settings' && ( + { + setUser(u); + refreshAll(); + }} + onBack={() => setActiveTab('triage')} + /> + )} - {/* Active View Content */} -
- {activeTab === 'triage' && ( - setCaptureFocusCount((c) => c + 1)} - /> - )} - - {activeTab === 'tasks' && ( - - )} - - {activeTab === 'workspace' && ( - - )} - - {activeTab === 'lists' && ( - - )} - - {activeTab === 'ecosystem' && ( - - )} -
+ {activeTab === 'admin_settings' && isStaff && ( + setActiveTab('triage')} + /> + )} + +
- - {/* Account Switcher / Registration Modal */} - {showAuthModal && ( -
-
-
-
- -

- {authMode === 'login' ? 'Konto wechseln' : 'Neues Familien-Konto anlegen'} -

-
- -
- - {user && ( -
-
- Aktuell aktiv: - {user.displayName} - ({user.email}) -
- -
- )} - -
- - -
- -
- {authMode === 'register' && ( - setDisplayName(e.target.value)} - placeholder="Anzeigename (z. B. Lisa)" - className="w-full h-11 px-4 rounded-full bg-[#131314] text-sm text-[#e3e3e3] gemini-border focus:outline-none" - required - /> - )} - setEmail(e.target.value)} - placeholder="E-Mail-Adresse" - className="w-full h-11 px-4 rounded-full bg-[#131314] text-sm text-[#e3e3e3] gemini-border focus:outline-none" - required - /> - setPassword(e.target.value)} - placeholder="Passwort (mind. 4 Zeichen)" - className="w-full h-11 px-4 rounded-full bg-[#131314] text-sm text-[#e3e3e3] gemini-border focus:outline-none" - required - /> - - {authError &&

{authError}

} - - -
-
-
- )}
); }; diff --git a/frontend/src/features/auth/SetupAndAuthView.tsx b/frontend/src/features/auth/SetupAndAuthView.tsx new file mode 100644 index 0000000..abda554 --- /dev/null +++ b/frontend/src/features/auth/SetupAndAuthView.tsx @@ -0,0 +1,530 @@ +import React, { useState } from 'react'; +import { + Brain, + Shield, + Lock, + KeyRound, + UserPlus, + LogIn, + Server, + Share2, + Check, + AlertCircle, +} from 'lucide-react'; +import { AuthStatus, User } from '../../lib/types'; +import { api, triggerHaptic } from '../../lib/api'; + +interface SetupAndAuthViewProps { + authStatus: AuthStatus; + onAuthenticated: (user: User) => void; +} + +export const SetupAndAuthView: React.FC = ({ + authStatus, + onAuthenticated, +}) => { + // --- Setup Wizard State --- + const [setupName, setSetupName] = useState(''); + const [setupEmail, setSetupEmail] = useState(''); + const [setupPassword, setSetupPassword] = useState(''); + const [instanceName, setInstanceName] = useState(authStatus.instanceName || 'MindSift'); + const [sharingMode, setSharingMode] = useState<'open' | 'teams_only' | 'strict_private'>( + 'teams_only' + ); + const [allowRegistration, setAllowRegistration] = useState(false); + const [createSampleData, setCreateSampleData] = useState(false); + + // --- Login / Invite / Register State --- + const [authTab, setAuthTab] = useState<'login' | 'invite' | 'register'>('login'); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [displayName, setDisplayName] = useState(''); + const [inviteCode, setInviteCode] = useState(''); + + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const handleCompleteSetup = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSubmitting(true); + triggerHaptic(15); + try { + const res = await api.completeSetup({ + email: setupEmail.trim(), + password: setupPassword, + displayName: setupName.trim(), + instanceName: instanceName.trim() || 'MindSift', + sharingMode, + allowRegistration, + createSampleData, + }); + onAuthenticated(res.user); + } catch (err: any) { + setError(err.message || 'Fehler bei der Ersteinrichtung'); + } finally { + setSubmitting(false); + } + }; + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSubmitting(true); + triggerHaptic(15); + try { + const res = await api.login(email.trim(), password); + onAuthenticated(res.user); + } catch (err: any) { + setError(err.message || 'Ungültige Anmeldedaten'); + } finally { + setSubmitting(false); + } + }; + + const handleRegisterOrInvite = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSubmitting(true); + triggerHaptic(15); + try { + const code = authTab === 'invite' ? inviteCode.trim() : ''; + const res = await api.register(email.trim(), password, displayName.trim(), code); + onAuthenticated(res.user); + } catch (err: any) { + setError(err.message || 'Registrierung fehlgeschlagen'); + } finally { + setSubmitting(false); + } + }; + + // ============================================================================ + // FIRST-BOOT SERVER SETUP WIZARD + // ============================================================================ + if (authStatus.needsSetup) { + return ( +
+
+ {/* Top Brand Bar */} +
+
+
+ +
+
+

+ MindSift Ersteinrichtung +

+

+ Initiales Administrator-Konto & Server-Konfiguration +

+
+
+ First Boot +
+ +
+

+ Willkommen auf deiner neuen MindSift-Instanz. Es existiert noch kein Benutzerkonto in + der Datenbank. Lege jetzt das erste Server-Admin-Konto an und + bestimme die Sicherheitseinstellungen. +

+ + {error && ( +
+ + {error} +
+ )} + + {/* Section 1: Admin Account */} +
+

+ + 1. Server-Administrator anlegen +

+ +
+
+ + setSetupName(e.target.value)} + placeholder="z.B. Admin / Dein Name" + required + autoFocus + className="w-full h-8 px-2.5 input-field" + /> +
+
+ + setSetupEmail(e.target.value)} + placeholder="admin@deinedomain.de" + required + className="w-full h-8 px-2.5 input-field" + /> +
+
+ +
+ + setSetupPassword(e.target.value)} + placeholder="Mindestens 4 Zeichen" + minLength={4} + required + className="w-full h-8 px-2.5 input-field" + /> +
+
+ + {/* Section 2: Server & Isolation */} +
+

+ + 2. Instanz & Berechtigungen +

+ +
+ + setInstanceName(e.target.value)} + placeholder="MindSift" + className="w-full h-8 px-2.5 input-field" + /> +
+ +
+ +
+ + + + + +
+
+ +
+ + + +
+
+ +
+ +
+
+
+
+ ); + } + + // ============================================================================ + // STANDARD LOGIN / INVITE CODE / REGISTRATION VIEW + // ============================================================================ + return ( +
+
+ {/* Top Header */} +
+
+
+ +
+
+

+ {authStatus.instanceName || 'MindSift'} +

+

Der Kopf frei in drei Sekunden

+
+
+ +
+ + {/* Navigation Tabs */} +
+ + + + + {authStatus.allowRegistration && ( + + )} +
+ + {/* Form Body */} +
+ {error && ( +
+ + {error} +
+ )} + + {authTab === 'login' && ( +
+
+ + setEmail(e.target.value)} + placeholder="name@beispiel.de" + required + autoFocus + className="w-full h-8 px-2.5 input-field" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + required + className="w-full h-8 px-2.5 input-field" + /> +
+ +
+ +
+ + {!authStatus.allowRegistration && ( +

+ Öffentliche Registrierung ist deaktiviert. Zugang nur mit Konto oder{' '} + + . +

+ )} +
+ )} + + {(authTab === 'invite' || authTab === 'register') && ( +
+ {authTab === 'invite' && ( +
+ + setInviteCode(e.target.value.toUpperCase())} + placeholder="MS-XXXXXXXX" + required + autoFocus + className="w-full h-8 px-2.5 input-field font-mono uppercase" + /> +
+ )} + +
+ + setDisplayName(e.target.value)} + placeholder="Dein Name" + required + className="w-full h-8 px-2.5 input-field" + /> +
+ +
+ + setEmail(e.target.value)} + placeholder="name@beispiel.de" + required + className="w-full h-8 px-2.5 input-field" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="Mindestens 4 Zeichen" + minLength={4} + required + className="w-full h-8 px-2.5 input-field" + /> +
+ +
+ +
+
+ )} +
+
+
+ ); +}; diff --git a/frontend/src/features/capture/InstantCaptureBar.tsx b/frontend/src/features/capture/InstantCaptureBar.tsx index 8a32e82..832505c 100644 --- a/frontend/src/features/capture/InstantCaptureBar.tsx +++ b/frontend/src/features/capture/InstantCaptureBar.tsx @@ -1,40 +1,76 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Mic, MicOff, Send, Sparkles, Calendar, AlertTriangle, CheckCircle2 } from 'lucide-react'; -import { PriorityLevel, Thought } from '../../lib/types'; +import { + Mic, + MicOff, + ArrowUp, + Sparkles, + Calendar, + Check, + CheckSquare, + ShoppingCart, + FileText, + Layers, + ChevronDown, +} from 'lucide-react'; +import { PriorityLevel, SharedList, Thought } from '../../lib/types'; import { api, triggerHaptic } from '../../lib/api'; +export type CaptureTarget = 'inbox' | 'task' | 'list_item' | 'document'; + interface InstantCaptureBarProps { - onCaptured: (thought: Thought) => void; + onCaptured: (thought: Thought, target?: CaptureTarget) => void; + lists?: SharedList[]; autoFocusTrigger?: number; } export const InstantCaptureBar: React.FC = ({ onCaptured, + lists = [], autoFocusTrigger = 0, }) => { const [content, setContent] = useState(''); const [priority, setPriority] = useState('normal'); const [dueDate, setDueDate] = useState(''); + const [target, setTarget] = useState('inbox'); + const [targetListId, setTargetListId] = useState(''); const [showOptions, setShowOptions] = useState(false); const [isListening, setIsListening] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - const [justSaved, setJustSaved] = useState(false); + const [justSavedLabel, setJustSavedLabel] = useState(null); const inputRef = useRef(null); + const containerRef = useRef(null); const recognitionRef = useRef(null); useEffect(() => { inputRef.current?.focus(); }, [autoFocusTrigger]); + useEffect(() => { + if (!targetListId && lists.length > 0) { + setTargetListId(lists[0].id); + } + }, [lists, targetListId]); + + // Close dropdown on outside click + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setShowOptions(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + // Global keyboard shortcut: 'c' or '/' focuses the Instant Capture input immediately useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - const target = e.target as HTMLElement; + const el = e.target as HTMLElement; const isEditing = - target.tagName === 'INPUT' || - target.tagName === 'TEXTAREA' || - target.isContentEditable; + el.tagName === 'INPUT' || + el.tagName === 'TEXTAREA' || + el.isContentEditable; if (!isEditing && (e.key === 'c' || e.key === 'C' || e.key === '/')) { e.preventDefault(); @@ -102,164 +138,359 @@ export const InstantCaptureBar: React.FC = ({ } }; - const handleSubmit = async (e?: React.FormEvent) => { - if (e) e.preventDefault(); + const executeCapture = async ( + overrideTarget?: CaptureTarget, + overrideListId?: string + ) => { const trimmed = content.trim(); if (!trimmed || isSubmitting) return; + const effectiveTarget = overrideTarget || target; + const effectiveListId = + overrideListId || targetListId || (lists.length > 0 ? lists[0].id : ''); + const capturedPriority = priority; + const capturedDueDate = dueDate; + const capturedSource = isListening ? 'voice' : 'manual'; + + // Optimistically clear input immediately so the UI feels instant (<10ms) in all browsers + setContent(''); + setPriority('normal'); + setDueDate(''); + setTarget('inbox'); + setShowOptions(false); setIsSubmitting(true); triggerHaptic(15); + const savedMsg = + effectiveTarget === 'task' + ? 'Als Aufgabe!' + : effectiveTarget === 'list_item' + ? 'Auf Liste!' + : effectiveTarget === 'document' + ? 'Als Notiz!' + : 'Im Dump!'; + setJustSavedLabel(savedMsg); + setTimeout(() => setJustSavedLabel(null), 1600); + inputRef.current?.focus(); + try { const created = await api.captureThought( trimmed, - priority, - isListening ? 'voice' : 'manual', - dueDate + capturedPriority, + capturedSource, + capturedDueDate, + { + target: effectiveTarget, + targetListId: effectiveTarget === 'list_item' ? effectiveListId : '', + } ); - setContent(''); - setPriority('normal'); - setDueDate(''); - setShowOptions(false); - setJustSaved(true); - setTimeout(() => setJustSaved(false), 1800); - onCaptured(created); - inputRef.current?.focus(); + onCaptured(created, effectiveTarget); + } catch { + // Restore text if capture unexpectedly failed completely + setContent(trimmed); } finally { setIsSubmitting(false); } }; + const handleSubmit = async (e?: React.FormEvent) => { + if (e) e.preventDefault(); + await executeCapture(); + }; + + const selectedList = lists.find((l) => l.id === targetListId) || lists[0]; + + const getTargetLabel = () => { + if (target === 'task') return 'Direkt-Aufgabe'; + if (target === 'list_item') return selectedList ? `${selectedList.icon} ${selectedList.title}` : 'Auf Liste'; + if (target === 'document') return 'Direkt-Notiz'; + return 'Quick Dump'; + }; + return ( -
-
+
+ + {/* Quick-Capture Input Bar (Radius controlled by --radius-pill) */}
- +
+ +
setContent(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + executeCapture(); + } + }} placeholder={ isListening - ? 'Ich höre zu… sprich deinen Gedanken aus' - : 'Gedanke, Idee oder Zutat sofort festhalten… (Taste C oder /)' + ? 'Ich höre zu…' + : target === 'task' + ? 'Neue Aufgabe direkt anlegen… (Enter)' + : target === 'list_item' + ? `Eintrag direkt zu "${selectedList?.title || 'Liste'}" hinzufügen…` + : target === 'document' + ? 'Neue Workspace-Notiz direkt anlegen…' + : 'Quick Dump: Gedanke einfach reinrotzen & später swipen… (C / Enter)' } - className="w-full bg-transparent text-[#e3e3e3] placeholder-[#9aa0a6] text-sm sm:text-base focus:outline-none" + className="w-full bg-transparent text-main placeholder-faint text-xs sm:text-sm focus:outline-none" autoComplete="off" /> -
- {/* Options Toggle Pill (Due Date / Priority) */} +
+ {/* Target & Options Toggle Pill */} - {/* Voice Dictation Pill Button */} + {/* Voice Dictation Button */} - {/* Instant Send Pill Button */} + {/* Instant Send Button */}
- {/* Expandable Quick Metadata Pills */} + {/* Expandable Target & Metadata Panel (Theme-controlled Dropdown) */} {showOptions && ( -
-
- - Priorität: - {(['low', 'normal', 'high', 'urgent'] as PriorityLevel[]).map((p) => { - const labels: Record = { - low: 'Niedrig', - normal: 'Normal', - high: 'Hoch', - urgent: 'Dringend', - }; - const active = priority === p; - return ( - - ); - })} +
+ {/* Row 1: Where should this entry go? */} +
+
+ + Wohin damit? {content.trim() ? '(Klick speichert sofort oder wählt Ziel)' : '(Ziel für Eingabe wählen)'} + + + Standard: Quick Dump für späteres Swipen + +
+ +
+ {/* Option 1: Quick Dump (Triage) */} + + + {/* Option 2: Direct Task */} + + + {/* Option 3: Direct List Item */} + + + {/* Option 4: Direct Workspace Note */} + +
-
- Fällig am: - setDueDate(e.target.value)} - className="bg-[#1e1f20] text-[#e3e3e3] px-3 py-1 rounded-full gemini-border text-xs focus:outline-none focus:border-[#8ab4f8]" - /> + {/* Optional Sub-Row: Shared List Picker */} + {lists.length > 0 && ( +
+ + + Liste: + + {lists.map((lst) => { + const isSelected = target === 'list_item' && targetListId === lst.id; + return ( + + ); + })} +
+ )} + + {/* Row 2: Priority & Due Date */} +
+
+ Priorität: + {(['low', 'normal', 'high', 'urgent'] as PriorityLevel[]).map((p) => { + const labels: Record = { + low: 'Niedrig', + normal: 'Normal', + high: 'Hoch', + urgent: 'Dringend', + }; + const active = priority === p; + return ( + + ); + })} +
+ +
+ + + Fällig am: + + setDueDate(e.target.value)} + className="input-field px-2 py-0.5 text-xs" + /> +
)} diff --git a/frontend/src/features/lists/SharedListsView.tsx b/frontend/src/features/lists/SharedListsView.tsx index 5301c04..54c0203 100644 --- a/frontend/src/features/lists/SharedListsView.tsx +++ b/frontend/src/features/lists/SharedListsView.tsx @@ -148,54 +148,39 @@ export const SharedListsView: React.FC = ({ onRefreshGloba const text = newItemText.trim(); const qty = newItemQty.trim(); - const cat = newItemCategory; + const cat = newItemCategory || undefined; setNewItemText(''); setNewItemQty(''); - const created = await api.createListItem(activeList.id, text, cat, qty); - setLists((prev) => - prev.map((l) => { - if (l.id !== activeList.id) return l; - if (l.items.some((i) => i.id === created.id)) return l; - return { ...l, items: [created, ...l.items] }; - }) - ); + try { + const item = await api.createListItem(activeList.id, text, qty, cat); + setLists((prev) => + prev.map((l) => (l.id === activeList.id ? { ...l, items: [item, ...l.items] } : l)) + ); + onRefreshGlobal(); + } catch (err: any) { + alert(err.message || 'Fehler beim Erstellen des Listeneintrags'); + } }; const handleToggleItem = async (item: ListItem) => { if (!activeList) return; - triggerHaptic(18); + triggerHaptic(10); const nextCompleted = !item.isCompleted; - // Optimistic UI update (BRAIN.md Section 3) setLists((prev) => prev.map((l) => { if (l.id !== activeList.id) return l; return { ...l, - items: l.items.map((i) => - i.id === item.id ? { ...i, isCompleted: nextCompleted } : i - ), + items: l.items.map((i) => (i.id === item.id ? { ...i, isCompleted: nextCompleted } : i)), }; }) ); - try { - const updated = await api.toggleListItem(activeList.id, item.id, nextCompleted); - setLists((prev) => - prev.map((l) => { - if (l.id !== activeList.id) return l; - return { - ...l, - items: l.items.map((i) => (i.id === item.id ? updated : i)), - }; - }) - ); - } catch { - // Rollback on error - loadListsForSpace(activeSpace!.id); - } + await api.toggleListItem(activeList.id, item.id, nextCompleted); + onRefreshGlobal(); }; const handleDeleteItem = async (itemId: string) => { @@ -264,18 +249,18 @@ export const SharedListsView: React.FC = ({ onRefreshGloba const completedItems = activeList?.items.filter((i) => i.isCompleted) || []; return ( -
+
{/* Top Space Bar & Realtime Indicator */} -
-
+
+
{spaces.map((sp) => (
@@ -302,10 +287,10 @@ export const SharedListsView: React.FC = ({ onRefreshGloba {activeSpace && (
@@ -315,14 +300,14 @@ export const SharedListsView: React.FC = ({ onRefreshGloba
@@ -333,19 +318,19 @@ export const SharedListsView: React.FC = ({ onRefreshGloba {showNewSpaceModal && ( setNewSpaceName(e.target.value)} - placeholder="Name des neuen Familien-/Team-Spaces (z. B. Sommerurlaub 2027)…" - className="flex-1 h-10 px-4 rounded-full bg-[#1e1f20] text-sm text-[#e3e3e3] gemini-border focus:outline-none" + placeholder="Name des neuen Familien-/Team-Spaces (z. B. Urlaub 2026)…" + className="flex-1 h-9 px-3 input-field text-sm" autoFocus /> @@ -355,19 +340,19 @@ export const SharedListsView: React.FC = ({ onRefreshGloba {showJoinModal && ( setJoinCode(e.target.value)} placeholder="Invite-Code des Partners eingeben…" - className="flex-1 h-10 px-4 rounded-full bg-[#1e1f20] text-sm text-[#e3e3e3] gemini-border focus:outline-none uppercase" + className="flex-1 h-9 px-3 input-field text-sm uppercase" autoFocus /> @@ -375,7 +360,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba )} {/* List Tabs within Active Space */} -
+
{lists.map((lst) => { const openCount = lst.items.filter((i) => !i.isCompleted).length; const isSelected = activeList?.id === lst.id; @@ -383,15 +368,17 @@ export const SharedListsView: React.FC = ({ onRefreshGloba @@ -400,9 +387,9 @@ export const SharedListsView: React.FC = ({ onRefreshGloba
@@ -410,7 +397,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba {showNewListModal && (
{['🛒', '🏔️', '🍕', '🎉', '🧰', '🎁'].map((ic) => ( @@ -418,8 +405,10 @@ export const SharedListsView: React.FC = ({ onRefreshGloba key={ic} type="button" onClick={() => setNewListIcon(ic)} - className={`w-9 h-9 rounded-full flex items-center justify-center ${ - newListIcon === ic ? 'bg-[#8ab4f8]/25 border border-[#8ab4f8]' : 'bg-[#1e1f20]' + className={`w-9 h-9 flex items-center justify-center rounded-btn transition-colors ${ + newListIcon === ic + ? 'bg-accent/20 border border-accent text-accent' + : 'bg-canvas hover:bg-panel-elevated border border-border' }`} > {ic} @@ -431,12 +420,12 @@ export const SharedListsView: React.FC = ({ onRefreshGloba value={newListTitle} onChange={(e) => setNewListTitle(e.target.value)} placeholder="Titel der Liste (z. B. Baumarkt, Drogerie, Camping)…" - className="flex-1 h-10 px-4 rounded-full bg-[#1e1f20] text-sm text-[#e3e3e3] gemini-border focus:outline-none" + className="flex-1 h-9 px-3 input-field text-sm" autoFocus /> @@ -445,16 +434,16 @@ export const SharedListsView: React.FC = ({ onRefreshGloba {/* Active Shared List Card */} {activeList ? ( -
+
{/* Quick-Add Pill Form */} - -
+ +
setNewItemText(e.target.value)} placeholder={`Eintrag zu "${activeList.title}" hinzufügen (z. B. Hafermilch)…`} - className="flex-1 h-12 px-5 rounded-full bg-[#131314] text-sm sm:text-base text-[#e3e3e3] placeholder-[#9aa0a6] gemini-border focus:outline-none focus:border-[#8ab4f8]" + className="flex-1 h-9 px-3 input-field text-sm" />
= ({ onRefreshGloba value={newItemQty} onChange={(e) => setNewItemQty(e.target.value)} placeholder="Menge (z.B. 2x)" - className="w-32 h-12 px-4 rounded-full bg-[#131314] text-sm text-[#e3e3e3] placeholder-[#9aa0a6] gemini-border focus:outline-none focus:border-[#8ab4f8]" + className="w-32 h-9 px-3 input-field text-sm" />
)) @@ -543,37 +532,37 @@ export const SharedListsView: React.FC = ({ onRefreshGloba {/* Completed Items Section */} {completedItems.length > 0 && ( -
-
- - Bereits im Einkaufswagen / Erledigt ({completedItems.length}) +
+
+ + Erledigt ({completedItems.length})
-
+
{completedItems.map((item) => (
handleToggleItem(item)} - className="p-3.5 rounded-2xl bg-[#131314]/60 gemini-border flex items-center justify-between gap-3 cursor-pointer" + className="px-3 py-1.5 bg-canvas/50 hover:bg-canvas/80 flex items-center justify-between gap-3 cursor-pointer transition-colors" > -
- - +
+ + {item.content} {item.quantity && ( - ({item.quantity}) + ({item.quantity}) )}
{item.completedByName && ( - ✓ {item.completedByName} + ✓ {item.completedByName} )}
))} diff --git a/frontend/src/features/settings/AdminSettingsView.tsx b/frontend/src/features/settings/AdminSettingsView.tsx new file mode 100644 index 0000000..a344b11 --- /dev/null +++ b/frontend/src/features/settings/AdminSettingsView.tsx @@ -0,0 +1,1325 @@ +import React, { useEffect, useState } from 'react'; +import { + Shield, + Users, + Server, + Mail, + Plus, + Trash2, + KeyRound, + Copy, + Check, + ArrowLeft, + Share2, + Send, + AlertCircle, + Building2, + UserPlus, +} from 'lucide-react'; +import { + Organization, + SystemSettings, + Team, + User, + UserInvite, + UserRole, +} from '../../lib/types'; +import { api, triggerHaptic } from '../../lib/api'; + +interface AdminSettingsViewProps { + currentUser: User; + onBack: () => void; +} + +type AdminSection = 'users' | 'orgs' | 'server' | 'smtp'; + +export const AdminSettingsView: React.FC = ({ + currentUser, + onBack, +}) => { + const isServerAdmin = currentUser.role === 'admin'; + const [section, setSection] = useState('users'); + + // --- Users & Invites State --- + const [users, setUsers] = useState([]); + const [invites, setInvites] = useState([]); + const [newUserName, setNewUserName] = useState(''); + const [newUserEmail, setNewUserEmail] = useState(''); + const [newUserPassword, setNewUserPassword] = useState(''); + const [newUserRole, setNewUserRole] = useState('member'); + + const [inviteEmail, setInviteEmail] = useState(''); + const [inviteRole, setInviteRole] = useState('member'); + const [inviteTeamId, setInviteTeamId] = useState(''); + const [copiedInviteId, setCopiedInviteId] = useState(null); + + const [resetPwdUserId, setResetPwdUserId] = useState(null); + const [resetPwdVal, setResetPwdVal] = useState(''); + + // --- Organizations & Teams State --- + const [orgs, setOrgs] = useState([]); + const [allTeams, setAllTeams] = useState([]); + const [newOrgName, setNewOrgName] = useState(''); + const [selectedOrgId, setSelectedOrgId] = useState(''); + const [newTeamName, setNewTeamName] = useState(''); + const [newTeamDesc, setNewTeamDesc] = useState(''); + const [memberSelectByTeam, setMemberSelectByTeam] = useState>({}); + const [memberRoleByTeam, setMemberRoleByTeam] = useState>({}); + + // --- Server & SMTP Settings State --- + const [settings, setSettings] = useState({ + sharingMode: 'teams_only', + allowRegistration: false, + instanceName: 'MindSift', + smtpHost: '', + smtpPort: 587, + smtpUser: '', + smtpPass: '', + smtpFrom: '', + smtpTls: true, + smtpConfiguredViaEnv: false, + }); + const [testSmtpEmail, setTestSmtpEmail] = useState(currentUser.email || ''); + const [testingSmtp, setTestingSmtp] = useState(false); + + const [statusMsg, setStatusMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); + + const loadAll = async () => { + try { + const [uList, invList, orgList, teamList] = await Promise.all([ + api.adminGetUsers(), + api.adminGetInvites(), + api.getOrganizations(), + api.getUserTeams(), + ]); + setUsers(uList); + setInvites(invList); + setOrgs(orgList); + setAllTeams(teamList); + if (orgList.length > 0 && !selectedOrgId) { + setSelectedOrgId(orgList[0].id); + } + + if (isServerAdmin) { + const sys = await api.adminGetSettings(); + setSettings({ + sharingMode: sys.sharingMode || 'teams_only', + allowRegistration: Boolean(sys.allowRegistration), + instanceName: sys.instanceName || 'MindSift', + smtpHost: sys.smtpHost || '', + smtpPort: sys.smtpPort || 587, + smtpUser: sys.smtpUser || '', + smtpPass: sys.smtpPass || '', + smtpFrom: sys.smtpFrom || '', + smtpTls: sys.smtpTls ?? true, + smtpConfiguredViaEnv: Boolean(sys.smtpConfiguredViaEnv), + }); + } + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Laden der Administrationsdaten' }); + } + }; + + useEffect(() => { + loadAll(); + }, []); + + // ============================================================================ + // HANDLERS: USERS & INVITES + // ============================================================================ + const handleCreateUser = async (e: React.FormEvent) => { + e.preventDefault(); + setStatusMsg(null); + triggerHaptic(15); + try { + const created = await api.adminCreateUser({ + email: newUserEmail.trim(), + password: newUserPassword, + displayName: newUserName.trim() || newUserEmail.split('@')[0], + role: newUserRole, + }); + setUsers((prev) => [...prev, created]); + setNewUserName(''); + setNewUserEmail(''); + setNewUserPassword(''); + setNewUserRole('member'); + setStatusMsg({ type: 'ok', text: `Benutzer "${created.displayName}" wurde angelegt.` }); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Anlegen des Benutzers' }); + } + }; + + const handleCreateInvite = async (e: React.FormEvent) => { + e.preventDefault(); + setStatusMsg(null); + triggerHaptic(15); + try { + const inv = await api.adminCreateInvite({ + email: inviteEmail.trim(), + role: inviteRole, + teamId: inviteTeamId, + }); + setInvites((prev) => [inv, ...prev]); + setInviteEmail(''); + setStatusMsg({ + type: 'ok', + text: `Einladungscode ${inv.code} erfolgreich erstellt.`, + }); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Erstellen der Einladung' }); + } + }; + + const handleDeleteInvite = async (id: string) => { + try { + await api.adminDeleteInvite(id); + setInvites((prev) => prev.filter((i) => i.id !== id)); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message }); + } + }; + + const handleCopyInviteCode = (inv: UserInvite) => { + navigator.clipboard.writeText(inv.code); + setCopiedInviteId(inv.id); + setTimeout(() => setCopiedInviteId(null), 2000); + }; + + const handleChangeUserRole = async (u: User, role: UserRole) => { + setStatusMsg(null); + try { + const updated = await api.adminUpdateUser(u.id, { role }); + setUsers((prev) => prev.map((item) => (item.id === u.id ? updated : item))); + setStatusMsg({ + type: 'ok', + text: `Rolle für ${updated.displayName} auf "${role}" aktualisiert.`, + }); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message }); + } + }; + + const handleToggleUserActive = async (u: User) => { + setStatusMsg(null); + try { + const updated = await api.adminUpdateUser(u.id, { isActive: !u.isActive }); + setUsers((prev) => prev.map((item) => (item.id === u.id ? updated : item))); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message }); + } + }; + + const handleConfirmResetPassword = async (userId: string) => { + if (!resetPwdVal.trim()) return; + setStatusMsg(null); + try { + await api.adminResetPassword(userId, resetPwdVal.trim()); + setResetPwdUserId(null); + setResetPwdVal(''); + setStatusMsg({ type: 'ok', text: 'Passwort erfolgreich zurückgesetzt.' }); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message }); + } + }; + + const handleDeleteUser = async (u: User) => { + if ( + !window.confirm( + `Benutzer "${u.displayName}" (${u.email}) wirklich unwiderruflich löschen?` + ) + ) + return; + setStatusMsg(null); + try { + await api.adminDeleteUser(u.id); + setUsers((prev) => prev.filter((item) => item.id !== u.id)); + setStatusMsg({ type: 'ok', text: 'Benutzer gelöscht.' }); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message }); + } + }; + + // ============================================================================ + // HANDLERS: ORGANIZATIONS & TEAMS + // ============================================================================ + const handleCreateOrg = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newOrgName.trim()) return; + setStatusMsg(null); + try { + const created = await api.createOrganization(newOrgName.trim()); + setNewOrgName(''); + setSelectedOrgId(created.id); + await loadAll(); + setStatusMsg({ type: 'ok', text: `Organisation "${created.name}" angelegt.` }); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message }); + } + }; + + const handleDeleteOrg = async (org: Organization) => { + if (!window.confirm(`Organisation "${org.name}" inklusive aller zugehörigen Teams löschen?`)) + return; + setStatusMsg(null); + try { + await api.deleteOrganization(org.id); + await loadAll(); + setStatusMsg({ type: 'ok', text: `Organisation "${org.name}" gelöscht.` }); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message }); + } + }; + + const handleCreateTeam = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedOrgId || !newTeamName.trim()) return; + setStatusMsg(null); + try { + const created = await api.createOrgTeam( + selectedOrgId, + newTeamName.trim(), + newTeamDesc.trim() + ); + setNewTeamName(''); + setNewTeamDesc(''); + await loadAll(); + setStatusMsg({ type: 'ok', text: `Team "${created.name}" wurde erstellt.` }); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message }); + } + }; + + const handleDeleteTeam = async (team: Team) => { + if (!window.confirm(`Team "${team.name}" wirklich löschen?`)) return; + setStatusMsg(null); + try { + await api.deleteTeam(team.id); + await loadAll(); + setStatusMsg({ type: 'ok', text: `Team "${team.name}" gelöscht.` }); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message }); + } + }; + + const handleAddMemberToTeam = async (teamId: string) => { + const selectedUserId = memberSelectByTeam[teamId]; + if (!selectedUserId) return; + const role = memberRoleByTeam[teamId] || 'member'; + setStatusMsg(null); + try { + await api.addTeamMember(teamId, { userId: selectedUserId, role }); + await loadAll(); + setStatusMsg({ type: 'ok', text: 'Teammitglied hinzugefügt.' }); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message }); + } + }; + + const handleRemoveMemberFromTeam = async (teamId: string, userId: string) => { + setStatusMsg(null); + try { + await api.removeTeamMember(teamId, userId); + await loadAll(); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message }); + } + }; + + // ============================================================================ + // HANDLERS: SERVER & SMTP SETTINGS + // ============================================================================ + const handleSaveSystemSettings = async (patch?: Partial) => { + setStatusMsg(null); + const next = { ...settings, ...(patch || {}) }; + setSettings(next); + try { + const saved = await api.adminUpdateSettings(next); + setSettings((prev) => ({ ...prev, ...saved })); + setStatusMsg({ type: 'ok', text: 'Server-Einstellungen erfolgreich gespeichert.' }); + } catch (err: any) { + setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Speichern' }); + } + }; + + const handleTestSmtp = async () => { + if (!testSmtpEmail.trim()) return; + setTestingSmtp(true); + setStatusMsg(null); + try { + await api.adminUpdateSettings(settings); + await api.adminTestSMTP(testSmtpEmail.trim()); + setStatusMsg({ + type: 'ok', + text: `Test-E-Mail erfolgreich an ${testSmtpEmail.trim()} gesendet!`, + }); + } catch (err: any) { + setStatusMsg({ + type: 'err', + text: `SMTP-Test fehlgeschlagen: ${err.message}`, + }); + } finally { + setTestingSmtp(false); + } + }; + + // Collect all teams across orgs for invite dropdown + const flattenedTeams: Team[] = + orgs.flatMap((o) => o.teams || []).length > 0 + ? orgs.flatMap((o) => o.teams || []) + : allTeams; + + return ( +
+ {/* Top Page Header */} +
+
+ +
+

+ + Administration & Team-Steuerung +

+

+ Benutzerverwaltung, Einladungscodes, Organisationen, Teams, Isolationsmodus & Mail-Server +

+
+
+ + + {isServerAdmin ? 'Server-Administrator' : 'Team-Administrator'} + +
+ + {statusMsg && ( +
+
+ {statusMsg.type === 'ok' ? ( + + ) : ( + + )} + {statusMsg.text} +
+ +
+ )} + + {/* Main 2-Column Layout */} +
+ {/* Left Navigation Sidebar */} + + + {/* Right Content Area */} +
+ {/* ================================================================= */} + {/* SECTION 1: BENUTZER, ROLLEN & EINLADUNGEN */} + {/* ================================================================= */} + {section === 'users' && ( +
+
+

Benutzersteuerung, Rollen & Einladungen

+

+ Lege Benutzerkonten direkt an oder erzeuge Einladungscodes, mit denen sich eingeladene + Personen selbst registrieren können, auch wenn die öffentliche Registrierung gesperrt ist. +

+
+ + {/* Direct User Creation Form */} +
+

+ + Neues Benutzerkonto direkt anlegen +

+ +
+
+
+ + setNewUserName(e.target.value)} + placeholder="z.B. Anna Beispiel" + required + className="w-full min-w-0 h-8 px-2.5 input-field" + /> +
+ +
+ + setNewUserEmail(e.target.value)} + placeholder="anna@beispiel.de" + required + className="w-full min-w-0 h-8 px-2.5 input-field" + /> +
+ +
+ + setNewUserPassword(e.target.value)} + placeholder="Mind. 4 Zeichen" + minLength={4} + required + className="w-full min-w-0 h-8 px-2.5 input-field" + /> +
+ +
+ + +
+
+ +
+ +
+
+
+ + {/* Invite Code Generator */} +
+

+ + Einladungscode erzeugen (gültig für 7 Tage) +

+ +
+
+
+ + setInviteEmail(e.target.value)} + placeholder="Leer lassen für beliebige E-Mail" + className="w-full min-w-0 h-8 px-2.5 input-field" + /> +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+
+ + {/* Active Invites List */} + {invites.length > 0 && ( +
+

+ Aktive & bisherige Einladungscodes: +

+
+ {invites.map((inv) => ( +
+
+ + {inv.code} + + + Rolle: {inv.role} + {inv.email ? ` · Für: ${inv.email}` : ''} + + {inv.usedBy ? ( + + Eingelöst + + ) : ( + + Offen + + )} +
+ +
+ {!inv.usedBy && ( + + )} + +
+
+ ))} +
+
+ )} +
+ + {/* Users List */} +
+

+ Registrierte Benutzer ({users.length}) +

+
+ {users.map((u) => ( +
+
+
+
+ {u.displayName} + {!u.isActive && ( + + Gesperrt + + )} +
+

{u.email}

+
+ +
+ {/* Role selector */} + + + {isServerAdmin && ( + <> + + + {u.id !== currentUser.id && ( + <> + + + + + )} + + )} +
+
+ + {resetPwdUserId === u.id && ( +
+ setResetPwdVal(e.target.value)} + placeholder="Neues Passwort eingeben…" + className="flex-1 h-7 px-2 input-field" + /> + + +
+ )} +
+ ))} +
+
+
+ )} + + {/* ================================================================= */} + {/* SECTION 2: ORGANISATIONEN & TEAMS */} + {/* ================================================================= */} + {section === 'orgs' && ( +
+
+

Organisationen & Teams verwalten

+

+ Erstelle Organisationen und unterteile sie in Teams. Weise Benutzern Teams zu, damit + sie im Modus "Teams isoliert" gemeinsam an Listen und Aufgaben arbeiten können. +

+
+ + {/* Create Organization & Team */} +
+
+

1. Neue Organisation gründen

+
+ setNewOrgName(e.target.value)} + placeholder="z.B. Homelab, Familie, Firma…" + required + className="flex-1 min-w-0 h-8 px-2.5 input-field" + /> + +
+
+ +
+

2. Neues Team in Organisation anlegen

+ {orgs.length === 0 ? ( +

+ Bitte lege zuerst links eine Organisation an. +

+ ) : ( +
+
+ + setNewTeamName(e.target.value)} + placeholder="Team-Name (z.B. Infrastruktur)" + required + className="w-full min-w-0 h-8 px-2.5 input-field" + /> +
+
+ setNewTeamDesc(e.target.value)} + placeholder="Kurzbeschreibung (optional)…" + className="flex-1 min-w-0 h-8 px-2.5 input-field" + /> + +
+
+ )} +
+
+ + {/* Organizations & Nested Teams Display */} +
+ {orgs.length === 0 ? ( +
+ Noch keine Organisationen vorhanden. +
+ ) : ( + orgs.map((org) => { + const orgTeams = Array.isArray(org.teams) ? org.teams : []; + return ( +
+
+
+ + {org.name} + + ({orgTeams.length} {orgTeams.length === 1 ? 'Team' : 'Teams'}) + +
+ +
+ +
+ {orgTeams.length === 0 ? ( +

+ Noch keine Teams in dieser Organisation. +

+ ) : ( + orgTeams.map((team) => { + const members = Array.isArray(team.members) ? team.members : []; + return ( +
+
+
+ + + {team.name} + + {team.description && ( +

+ {team.description} +

+ )} +
+ +
+ + {/* Team Members */} +
+

+ Mitglieder ({members.length}): +

+ {members.length === 0 ? ( +

+ Noch keine Mitglieder zugewiesen. +

+ ) : ( +
+ {members.map((m) => ( +
+ + {m.displayName} + + + ({m.role}) + + +
+ ))} +
+ )} +
+ + {/* Add User to Team */} +
+ + + + + +
+
+ ); + }) + )} +
+
+ ); + }) + )} +
+
+ )} + + {/* ================================================================= */} + {/* SECTION 3: SERVER & SHARING-ISOLATION (SERVER-ADMIN ONLY) */} + {/* ================================================================= */} + {section === 'server' && isServerAdmin && ( +
+
+

+ Server-Grundeinstellungen & Benutzer-Sichtbarkeit +

+

+ Als Server-Admin bestimmst du zentral für alle Nutzer, ob sie komplett isoliert + arbeiten, sich nur innerhalb ihrer Teams sehen oder ob alle Nutzer für alle sichtbar sind. +

+
+ +
+
+ +
+ + setSettings((prev) => ({ ...prev, instanceName: e.target.value })) + } + className="flex-1 h-9 px-3 input-field" + /> + +
+
+ + {/* Sharing Isolation Cards */} +
+ + +
+ + + + + +
+
+ + {/* Public Registration Toggle Switch */} +
+
+

Öffentliche Selbstregistrierung erlauben

+

+ Wenn deaktiviert, können sich fremde Personen auf dem Login-Bildschirm nicht selbst registrieren. + Neue Konten erfordern dann zwingend einen Einladungscode oder müssen von einem Admin angelegt werden. +

+
+ +
+ + +
+
+
+
+ )} + + {/* ================================================================= */} + {/* SECTION 4: E-MAIL / SMTP-SERVER (SERVER-ADMIN ONLY) */} + {/* ================================================================= */} + {section === 'smtp' && isServerAdmin && ( +
+
+
+

+ E-Mail / SMTP-Server für Aufgaben-Alarme +

+

+ Konfiguriere den Postausgangsserver per Docker-Compose Environment-Variablen oder + direkt hier über die Oberfläche (wird persistent in der SQLite-Datenbank gespeichert). +

+
+ + {settings.smtpConfiguredViaEnv && ( + + Docker Env erkannt + + )} +
+ +
+
+ + + setSettings((prev) => ({ ...prev, smtpHost: e.target.value })) + } + placeholder="z.B. smtp.example.com oder mail.deinedomain.de" + className="w-full h-9 px-3 input-field font-mono" + /> +
+ +
+ + + setSettings((prev) => ({ ...prev, smtpPort: Number(e.target.value) })) + } + placeholder="587" + className="w-full h-9 px-3 input-field font-mono" + /> +
+ +
+ + + setSettings((prev) => ({ ...prev, smtpUser: e.target.value })) + } + placeholder="alerts@deinedomain.de" + className="w-full h-9 px-3 input-field font-mono" + /> +
+ +
+ + + setSettings((prev) => ({ ...prev, smtpPass: e.target.value })) + } + placeholder="••••••••••••" + className="w-full h-9 px-3 input-field font-mono" + /> +
+ +
+ + + setSettings((prev) => ({ ...prev, smtpFrom: e.target.value })) + } + placeholder="MindSift " + className="w-full h-9 px-3 input-field" + /> +
+ +
+ +
+
+ +
+ +
+ + {/* Test SMTP Delivery */} +
+

SMTP-Verbindung testen

+

+ Sendet sofort eine Test-E-Mail mit den oben eingetragenen Daten. +

+
+ setTestSmtpEmail(e.target.value)} + placeholder="empfaenger@beispiel.de" + className="flex-1 h-8 px-3 input-field" + /> + +
+
+
+ )} +
+
+
+ ); +}; diff --git a/frontend/src/features/settings/EcosystemView.tsx b/frontend/src/features/settings/EcosystemView.tsx index 5c6ddb5..cff2903 100644 --- a/frontend/src/features/settings/EcosystemView.tsx +++ b/frontend/src/features/settings/EcosystemView.tsx @@ -92,34 +92,34 @@ export const EcosystemView: React.FC = ({ onIngested }) => { const baseUrl = window.location.origin; return ( -
+
{/* Section 1: API Keys for Smarthome & Automation */} -
+
-
- +
+
-

+

Webhooks & Ingestion API-Schlüssel

-

+

Verbinde Home Assistant, Zigbee2MQTT, n8n oder eigene Cronjobs direkt mit deiner MindSift Inbox.

-
+ setNewKeyName(e.target.value)} placeholder="Bezeichnung (z. B. Home Assistant, Proxmox Backup, Rauchmelder)…" - className="flex-1 h-11 px-4 rounded-full bg-[#131314] text-sm text-[#e3e3e3] placeholder-[#9aa0a6] gemini-border focus:outline-none focus:border-[#8ab4f8]" + className="flex-1 h-9 px-3 input-field text-sm" />
@@ -180,22 +180,22 @@ export const EcosystemView: React.FC = ({ onIngested }) => {
{/* Section 2: Interactive Smarthome Webhook Simulator */} -
-
-
- +
+
+
+
-

+

Live Smarthome Webhook-Simulator

-

+

Teste den Webhook-Endpunkt direkt hier – das Event landet sofort als Karte im Triage-Deck.

-
+
{[ { label: '🧺 Waschmaschine fertig', text: 'Waschmaschine im Keller ist fertig – Wäsche aufhängen!', prio: 'high' as PriorityLevel }, { label: '🚨 Rauchmelder Batterie', text: 'Rauchmelder Dachgeschoss meldet niedrige Batterie (12%)', prio: 'urgent' as PriorityLevel }, @@ -208,24 +208,24 @@ export const EcosystemView: React.FC = ({ onIngested }) => { setSimTitle(preset.text); setSimPriority(preset.prio); }} - className="px-3.5 py-1.5 rounded-full bg-[#131314] hover:bg-[#282a2c] gemini-border text-xs text-[#e3e3e3]" + className="btn-action text-xs" > {preset.label} ))}
-
+
setSimTitle(e.target.value)} - className="flex-1 h-11 px-4 rounded-full bg-[#131314] text-sm text-[#e3e3e3] gemini-border focus:outline-none" + className="flex-1 h-9 px-3 input-field text-sm" />
{simStatus && ( -

{simStatus}

+

{simStatus}

)} {/* Code Example Snippets */} -
-
- +
+
+ cURL / Home Assistant REST Command:
-
+          
 {`curl -X POST "${baseUrl}/api/v1/webhooks/ingest?token=${exampleToken}" \\
   -H "Content-Type: application/json" \\
   -d '{
@@ -255,16 +255,16 @@ export const EcosystemView: React.FC = ({ onIngested }) => {
       
{/* Section 3: Data Sovereignty & Export */} -
+
-
- +
+
-

+

100 % Datensouveränität & Komplett-Export

-

+

Exportiere jederzeit alle Gedanken, Aufgaben, Dokumente und geteilten Listen als portables JSON-Archiv.

@@ -272,7 +272,7 @@ export const EcosystemView: React.FC = ({ onIngested }) => { +
+

Kontoeinstellungen

+

+ Persönliches Profil, Design-Themes, Aufgaben-Alarme, Private Webhooks & Daten-Backup +

+
+
+ + + Rolle: {roleBadgeLabel} + +
+ + {/* Main 2-Column Layout */} +
+ {/* Left Navigation Sidebar */} + + + {/* Right Content Area */} +
+ {/* ================================================================= */} + {/* SECTION 1: PROFIL & SICHERHEIT */} + {/* ================================================================= */} + {section === 'profile' && ( +
+
+

Profil & Anmeldedaten

+

+ Verwalte deinen Anzeigenamen, deine E-Mail-Adresse und dein persönliches Passwort. +

+
+ + {profileMsg && ( +
+ {profileMsg.type === 'ok' ? ( + + ) : ( + + )} + {profileMsg.text} +
+ )} + +
+
+
+ + setDisplayName(e.target.value)} + required + className="w-full h-9 px-3 input-field" + /> +
+ +
+ + setEmail(e.target.value)} + required + className="w-full h-9 px-3 input-field" + /> +
+
+ +
+ + setNewPassword(e.target.value)} + placeholder="Mindestens 4 Zeichen" + className="w-full h-9 px-3 input-field" + /> +
+ +
+ +
+
+
+ )} + + {/* ================================================================= */} + {/* SECTION 2: DARSTELLUNG & THEMES (SINGLE-FILE CSS THEMING) */} + {/* ================================================================= */} + {section === 'appearance' && ( +
+
+

+ Darstellung & Single-File CSS Themes +

+

+ Jedes Theme ist eine einzige, eigenständige CSS-Datei, in der alle Farben, + Oberflächen, Kontraste und Ecken-Rundungen in 6 festen Gruppen gebündelt sind. +

+
+ +
+ {AVAILABLE_THEMES.map((t) => { + const isSelected = activeTheme === t.id; + return ( + + ); + })} +
+ +
+

Eigenes Theme anpassen oder hinzufügen:

+

+ Alle Theme-Dateien liegen unter{' '} + frontend/src/themes/ (inklusive der + Kopiervorlage{' '} + theme-template.css). Dort sind alle + Eigenschaften in 6 Blöcken gruppiert: 1. Akzentfarbe,{' '} + 2. Oberflächen, 3. Textfarben,{' '} + 4. Rahmen & Schatten, 5. Statusfarben und{' '} + 6. Rundungen. +

+
+
+ )} + + {/* ================================================================= */} + {/* SECTION 3: ALARME (MULTI-CHANNEL: EMAIL, DISCORD, CUSTOM WEBHOOK) */} + {/* ================================================================= */} + {section === 'alarms' && ( +
+
+

+ Aufgaben-Alarme & Benachrichtigungsziele +

+

+ Wähle aus, über welche Kanäle du benachrichtigt werden möchtest, sobald eine + Aufgabe mit Fälligkeitsdatum ansteht. Mehrere Ziele können gleichzeitig aktiv sein. +

+
+ + {alarmMsg && ( +
+ {alarmMsg.type === 'ok' ? ( + + ) : ( + + )} + {alarmMsg.text} +
+ )} + +
+ {/* Lead Time Selector */} +
+
+ +

+ Wann soll MindSift den Alarm vor dem Fälligkeitszeitpunkt auslösen? +

+
+ +
+ + {/* Channel 1: E-Mail */} +
+
+ + + +
+ +
+ + setAlarmEmail(e.target.value)} + placeholder={currentUser.email} + className="w-full h-8 px-2.5 input-field" + /> +
+
+ + {/* Channel 2: Discord Webhook */} +
+
+ + + +
+ +
+ + setDiscordWebhookUrl(e.target.value)} + placeholder="https://discord.com/api/webhooks/123456789/token..." + className="w-full h-8 px-2.5 input-field font-mono" + /> +
+
+ + {/* Channel 3: Custom HTTP Webhook */} +
+
+ + + +
+ +
+ + setCustomWebhookUrl(e.target.value)} + placeholder="https://dein-server.local/webhook/mindsift-alarm" + className="w-full h-8 px-2.5 input-field font-mono" + /> +
+
+ +
+ +
+
+
+ )} + + {/* ================================================================= */} + {/* SECTION 4: PRIVATE WEBHOOKS & EXPORT */} + {/* ================================================================= */} + {section === 'webhooks' && ( +
+
+

+ Private Webhooks (Eingehend) & Daten-Export +

+

+ Erstelle persönliche API-Schlüssel, um aus externen Skripten, Server-Monitoring + oder Discord-Bots automatisch Aufgaben und Gedanken in dein MindSift einzuspeisen. +

+
+ + {/* Create API Key */} +
+ setNewKeyName(e.target.value)} + placeholder="Bezeichnung des Schlüssels (z.B. Proxmox Monitoring, Discord-Forwarder)…" + className="flex-1 h-9 px-3 input-field" + /> + +
+ + {newlyCreatedKey && ( +
+
+

+ Neuer API-Schlüssel erstellt (bitte jetzt kopieren, wird nur einmal im + Klartext angezeigt): +

+ {newlyCreatedKey} +
+ +
+ )} + + {/* Existing Keys List */} +
+ {apiKeys.length === 0 ? ( +
+ Noch keine persönlichen API-Schlüssel vorhanden. +
+ ) : ( + apiKeys.map((k) => ( +
+
+

{k.name}

+

+ Präfix: {k.keyPrefix}•••••••• · Erstellt:{' '} + {new Date(k.createdAt).toLocaleDateString()} +

+
+ +
+ )) + )} +
+ + {/* Curl Example */} +
+
+ + + Curl-Beispiel: Direkt als Aufgabe einspeisen + + +
+
+                  {curlTaskExample}
+                
+
+ + {/* Full Data Backup */} +
+
+

Vollständiger Daten-Export (JSON Backup)

+

+ Lädt alle deine Gedanken, Aufgaben, Markdown-Dokumente und Listen als portable + JSON-Datei herunter. +

+
+ +
+
+ )} +
+
+
+ ); +}; diff --git a/frontend/src/features/triage/TasksView.tsx b/frontend/src/features/triage/TasksView.tsx index ec851d9..d48ea80 100644 --- a/frontend/src/features/triage/TasksView.tsx +++ b/frontend/src/features/triage/TasksView.tsx @@ -1,37 +1,87 @@ import React, { useEffect, useState } from 'react'; import { - CheckCircle2, - Circle, + Check, + Plus, Calendar, Trash2, RotateCcw, FileText, - Tag, - AlertCircle, + LayoutList, + Columns, + ArrowRight, + ArrowLeft, + Edit3, + X, + Layers, + Share2, + Users, } from 'lucide-react'; -import { Thought } from '../../lib/types'; +import { TaskList, Thought, Space } from '../../lib/types'; import { api, triggerHaptic } from '../../lib/api'; +const getInitials = (name?: string): string => { + if (!name) return '?'; + const parts = name.trim().split(/\s+/); + if (parts.length >= 2) { + return (parts[0][0] + parts[1][0]).toUpperCase(); + } + return name.slice(0, 2).toUpperCase(); +}; + interface TasksViewProps { onRefreshStats: () => void; onOpenDocument: (docId: string) => void; + refreshTrigger?: number; } -export const TasksView: React.FC = ({ onRefreshStats, onOpenDocument }) => { +export const TasksView: React.FC = ({ + onRefreshStats, + onOpenDocument, + refreshTrigger = 0, +}) => { + const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list'); const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open'); const [tasks, setTasks] = useState([]); + const [taskLists, setTaskLists] = useState([]); + const [selectedListId, setSelectedListId] = useState('all'); + const [spaces, setSpaces] = useState([]); const [discarded, setDiscarded] = useState([]); const [loading, setLoading] = useState(true); + // Quick Task Add state + const [quickTitle, setQuickTitle] = useState(''); + const [quickDueDate, setQuickDueDate] = useState(''); + + // New TaskList modal state + const [showNewListModal, setShowNewListModal] = useState(false); + const [newListTitle, setNewListTitle] = useState(''); + const [newListIcon, setNewListIcon] = useState('📋'); + const [newListSpaceId, setNewListSpaceId] = useState(''); + + // Share TaskList modal state + const [showShareListModal, setShowShareListModal] = useState(false); + const [sharingTaskList, setSharingTaskList] = useState(null); + const [shareTargetSpaceId, setShareTargetSpaceId] = useState(''); + + // Active notes inline edit ID + const [activeNotesTaskId, setActiveNotesTaskId] = useState(null); + const [notesDraft, setNotesDraft] = useState(''); + const loadData = async () => { setLoading(true); try { - const [allTasks, allDiscarded] = await Promise.all([ - api.getTasks(), - api.getThoughts('discarded'), + const [allTasks, allLists, allSpaces, allDiscarded] = await Promise.all([ + api.getTasks(selectedListId === 'all' ? undefined : selectedListId).catch(() => []), + api.getTaskLists().catch(() => []), + api.getSpaces().catch(() => []), + api.getThoughts('discarded').catch(() => []), ]); - setTasks(allTasks); - setDiscarded(allDiscarded); + setTasks(Array.isArray(allTasks) ? allTasks : []); + setTaskLists(Array.isArray(allLists) ? allLists : []); + setSpaces(Array.isArray(allSpaces) ? allSpaces : []); + setDiscarded(Array.isArray(allDiscarded) ? allDiscarded : []); + } catch { + // Prevent unhandled promise rejection from crashing view } finally { setLoading(false); } @@ -39,15 +89,28 @@ export const TasksView: React.FC = ({ onRefreshStats, onOpenDocu useEffect(() => { loadData(); - }, []); + }, [refreshTrigger, selectedListId]); const handleToggleDone = async (task: Thought) => { triggerHaptic(15); const nextState = !task.isCompleted; + const nextKanban = nextState ? 'done' : 'todo'; setTasks((prev) => - prev.map((t) => (t.id === task.id ? { ...t, isCompleted: nextState } : t)) + prev.map((t) => + t.id === task.id ? { ...t, isCompleted: nextState, kanbanStatus: nextKanban } : t + ) ); - await api.updateThought(task.id, { isCompleted: nextState }); + await api.updateThought(task.id, { isCompleted: nextState, kanbanStatus: nextKanban }); + onRefreshStats(); + }; + + const handleKanbanMove = async (task: Thought, nextStatus: 'todo' | 'in_progress' | 'done') => { + triggerHaptic(15); + const isCompleted = nextStatus === 'done'; + setTasks((prev) => + prev.map((t) => (t.id === task.id ? { ...t, kanbanStatus: nextStatus, isCompleted } : t)) + ); + await api.updateThought(task.id, { kanbanStatus: nextStatus, isCompleted }); onRefreshStats(); }; @@ -56,6 +119,12 @@ export const TasksView: React.FC = ({ onRefreshStats, onOpenDocu await api.updateThought(task.id, { dueDate: newDate }); }; + const handleSaveNotes = async (task: Thought) => { + setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, notes: notesDraft } : t))); + await api.updateThought(task.id, { notes: notesDraft }); + setActiveNotesTaskId(null); + }; + const handleConvertToDoc = async (task: Thought) => { triggerHaptic(20); const updated = await api.triageAction(task.id, 'keep', { convertTo: 'document' }); @@ -87,183 +156,947 @@ export const TasksView: React.FC = ({ onRefreshStats, onOpenDocu onRefreshStats(); }; - const openTasks = tasks.filter((t) => !t.isCompleted); - const doneTasks = tasks.filter((t) => t.isCompleted); + const handleQuickAddTask = async (e: React.FormEvent) => { + e.preventDefault(); + const title = quickTitle.trim(); + if (!title) return; + triggerHaptic(10); + setQuickTitle(''); + + try { + const created = await api.captureThought(title, 'normal', 'manual', quickDueDate, { + target: 'task', + taskListId: selectedListId === 'all' ? '' : selectedListId, + }); + setTasks((prev) => [created, ...prev]); + onRefreshStats(); + } catch { + loadData(); + } + }; + + const handleCreateTaskList = async (e: React.FormEvent) => { + e.preventDefault(); + const title = newListTitle.trim(); + if (!title) return; + try { + const created = await api.createTaskList({ + title, + icon: newListIcon, + spaceId: newListSpaceId, + }); + setTaskLists((prev) => [...prev, created]); + setSelectedListId(created.id); + setShowNewListModal(false); + setNewListTitle(''); + } catch (err: any) { + alert(err.message || 'Fehler beim Erstellen der Aufgabenliste'); + } + }; + + const handleDeleteTaskList = async (listId: string) => { + if ( + !window.confirm( + 'Möchten Sie diese Aufgabenliste wirklich löschen? Die enthaltenen Aufgaben bleiben erhalten.' + ) + ) + return; + try { + await api.deleteTaskList(listId); + setTaskLists((prev) => prev.filter((l) => l.id !== listId)); + setSelectedListId('all'); + } catch (err: any) { + alert(err.message); + } + }; + + const handleOpenShareModal = (tl: TaskList) => { + setSharingTaskList(tl); + setShareTargetSpaceId(tl.spaceId || ''); + setShowShareListModal(true); + }; + + const handleSaveShareList = async () => { + if (!sharingTaskList) return; + triggerHaptic(15); + try { + const updated = await api.updateTaskList(sharingTaskList.id, { + spaceId: shareTargetSpaceId, + }); + setTaskLists((prev) => + prev.map((l) => (l.id === updated.id ? { ...l, ...updated } : l)) + ); + setShowShareListModal(false); + setSharingTaskList(null); + await loadData(); + } catch (err: any) { + alert(err.message || 'Fehler beim Speichern der Freigabe'); + } + }; + + const safeTasks = Array.isArray(tasks) ? tasks : []; + const safeTaskLists = Array.isArray(taskLists) ? taskLists : []; + const safeSpaces = Array.isArray(spaces) ? spaces : []; + const safeDiscarded = Array.isArray(discarded) ? discarded : []; + + const openTasks = safeTasks.filter((t) => !t.isCompleted); + const doneTasks = safeTasks.filter((t) => t.isCompleted); + + const todoKanban = safeTasks.filter( + (t) => (!t.kanbanStatus || t.kanbanStatus === 'todo') && !t.isCompleted + ); + const inProgressKanban = safeTasks.filter( + (t) => t.kanbanStatus === 'in_progress' && !t.isCompleted + ); + const doneKanban = safeTasks.filter((t) => t.kanbanStatus === 'done' || t.isCompleted); + + const currentSelectedList = safeTaskLists.find((l) => l.id === selectedListId); return ( -
- {/* Filter Pills */} -
-
+
+ {/* Top Header: Task Lists Tabs & View Mode Switcher */} +
+ {/* Task Lists selector tabs */} +
- + {safeTaskLists.map((tl) => ( +
+ + {selectedListId === tl.id && ( + + )} +
+ ))}
- {filter === 'discarded' && discarded.length > 0 && ( - - )} + {/* Right Header Section: Sharing, Avatar Stack & View Switcher */} +
+ {currentSelectedList && ( +
+ + + {/* Avatar Stack in the corner */} + {currentSelectedList.members && currentSelectedList.members.length > 0 ? ( +
handleOpenShareModal(currentSelectedList)} + className="flex items-center -space-x-1.5 cursor-pointer hover:opacity-90 transition-opacity pl-0.5" + title={`Geteilt mit ${currentSelectedList.members.length} Mitgliedern (${currentSelectedList.spaceName || 'Team'}). Klick zum Verwalten.`} + > + {currentSelectedList.members.slice(0, 3).map((m) => ( +
+ {getInitials(m.displayName || m.email)} +
+ ))} + {currentSelectedList.members.length > 3 && ( +
+ +{currentSelectedList.members.length - 3} +
+ )} +
+ ) : ( +
handleOpenShareModal(currentSelectedList)} + className="flex items-center cursor-pointer pl-0.5" + title="Privat (nur du). Klick zum Teilen." + > +
+ 🔒 +
+
+ )} +
+ )} + + {/* View Switcher: List vs Kanban */} +
+ + +
+
- {loading ? ( -
Lade Aufgaben…
- ) : filter === 'discarded' ? ( -
- {discarded.length === 0 ? ( -
- Keine verworfenen Gedanken im Papierkorb. -
- ) : ( - discarded.map((item) => ( -
+ + setQuickTitle(e.target.value)} + placeholder={ + currentSelectedList + ? `Aufgabe zu "${currentSelectedList.title}" hinzufügen… (Enter)` + : 'Neue Aufgabe anlegen… (Enter)' + } + className="flex-1 bg-transparent px-2 py-1 text-sm text-main placeholder-faint focus:outline-none" + /> +
+ setQuickDueDate(e.target.value)} + className="h-7 px-2 input-field text-xs" + title="Fälligkeitsdatum" + /> + +
+ + + {/* NEW LIST MODAL */} + {showNewListModal && ( +
+
+
+

+ + Neue Aufgabenliste erstellen +

+ - + + +
+ +
+
+ +
+ setNewListIcon(e.target.value)} + maxLength={2} + className="w-10 h-8 text-center input-field text-sm" + title="Icon (Emoji)" + /> + setNewListTitle(e.target.value)} + placeholder="z.B. Homelab, Einkäufe, Steuern" + autoFocus + required + className="flex-1 h-8 input-field px-2.5 text-xs" + />
- )) - )} -
- ) : ( -
- {(filter === 'open' ? openTasks : doneTasks).length === 0 ? ( -
- {filter === 'open' - ? 'Keine offenen Aufgaben. Wische Gedanken in der Triage nach rechts, um Aufgaben anzulegen!' - : 'Noch keine erledigten Aufgaben.'} -
- ) : ( - (filter === 'open' ? openTasks : doneTasks).map((task) => ( -
-
- -
-

+ {safeSpaces.length > 0 && ( +

+ + +
+ )} + +
+ + +
+ +
+
+ )} + + {/* SHARE LIST MODAL */} + {showShareListModal && sharingTaskList && ( +
+
+
+

+ + Liste teilen: {sharingTaskList.title} +

+ +
+ +
+
+ + +
+ + {/* Members preview */} + {(() => { + const selectedSpace = safeSpaces.find((s) => s.id === shareTargetSpaceId); + const members = selectedSpace?.members || []; + if (members.length > 0) { + return ( +
+

+ Personen mit Zugriff ({members.length}): +

+
+ {members.map((m) => ( +
+
+
+ {getInitials(m.displayName || m.email)} +
+
+

{m.displayName || m.email}

+

{m.email}

+
+
+ + {m.role === 'owner' ? 'Eigentümer' : m.role === 'editor' ? 'Bearbeiter' : 'Mitglied'} + +
+ ))} +
+
+ ); + } else if (shareTargetSpaceId) { + return ( +

Keine Mitglieder in diesem Bereich.

+ ); + } else { + return ( +
+

Diese Aufgabenliste ist privat.

+

+ Wähle oben einen Familien- oder Team-Space aus, um die Liste und alle enthaltenen Aufgaben automatisch mit den Mitgliedern zu teilen. +

+
+ ); + } + })()} +
+ +
+ + +
+
+
+ )} + + {/* VIEW MODES: LIST OR KANBAN */} + {viewMode === 'kanban' ? ( + /* KANBAN BOARD (3 COLUMNS) */ +
+ {/* COLUMN 1: TODO */} +
+
+ + + Zu erledigen + + {todoKanban.length} +
+ +
+ {todoKanban.map((task) => ( +
+
+

{task.content}

+ +
-
- {task.priority === 'urgent' && ( - - Dringend - - )} - {task.priority === 'high' && ( - - Hoch - - )} - {task.tags?.map((tag) => ( - +