Release 0.8.0: Cloud-Variante (Supabase, Multi-Studio, Realtime, Web-Deploy)
Rapport ist jetzt dual: lokal (wie bisher) ODER Cloud auf eigenem Supabase-Server. Beide Modi haben dieselben Funktionen, Cloud zusätzlich Multi-User + Live-Sync. Storage-Architektur - src/storage/adapter.js: einheitliche Promise-API, LocalStorage- und SupabaseAdapter - src/storage/migrations.js: applyMigrations als reine Funktion, für beide Backends - Konfig-driven: VITE_SUPABASE_URL im Production-Build → automatisch Cloud-Modus Postgres-Schema (supabase/migrations/0001–0010) - 29 Tabellen, multi-tenant via studio_id + Row-Level-Security - Audit-Spalten (created_by/updated_by/at) + Trigger - Seed-Trigger pro neuem Studio (Rollen, Templates, Absenz-Typen) - Realtime-Publication für Live-Sync - RPCs: ensure_profile, create_studio_with_admin (mit Personen-Sharing), list_studios, load_persons_for_studio, attach_user_to_studio Cloud-Features (App) - BackendChoice.jsx als Erst-Screen «Lokal oder Cloud» - CloudSetup.jsx: 3-Schritt-Wizard für Erst-Einrichtung - Login.jsx: Modus-Switcher + Server-URL + Studio-Dropdown + Passwort-Vergessen - ResetPassword.jsx: empfängt Mail-Link-Klick via PASSWORD_RECOVERY-Event - Realtime: Änderungen zwischen Browsern ohne Reload sichtbar - Settings → System: Cloud-Verbindung, Studio-Switcher, weiteres Studio anlegen - Settings → Team: Mitarbeiter via Email einladen (Admin-Aktion) - Personen-Sharing: bei neuem Studio Personen aus anderen Studios übernehmen - Reload-Resume: studio_id in sessionStorage, kein erneuter Login nötig Web-Deploy - deploy/docker-compose.yml + nginx.conf: dist/ via nginx-Container, Port 8080 - .env.production.example: Build-time Cloud-URL - DEPLOY.md: Anleitung für LAN-only und extern via Nginx Proxy Manager Doku - README.md: Cloud-Variante prominent erklärt - ARCHITECTURE.md: Storage-Adapter, Migrations, neue Views in Risiko-Tabelle - DEPLOY.md: Schritt-für-Schritt für Mac Mini + NPM Version-Bump auf 0.8.0 in package.json, src-tauri/tauri.conf.json, Cargo.toml. Changelog-Entry im App.jsx-Modal (Karim sieht ihn beim ersten Start). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
// Storage-Adapter: Auswahl zwischen LocalStorage und Supabase (Cloud).
|
||||
//
|
||||
// Auswahl-Logik:
|
||||
// localStorage["rapport_backend"] === "cloud" → SupabaseAdapter
|
||||
// alles andere (default) → LocalStorageAdapter
|
||||
//
|
||||
// Umschalten: `localStorage.setItem("rapport_backend", "cloud")` (später UI-Toggle).
|
||||
// Cloud braucht zusätzlich VITE_SUPABASE_URL und VITE_SUPABASE_ANON_KEY in .env.local.
|
||||
// Fallback: wenn Cloud gewählt aber env fehlt, kommt LocalStorage zurück (mit Warning).
|
||||
//
|
||||
// Bewusst NICHT im Adapter:
|
||||
// - UI-State (Dark Mode, Zoom, …) — per-Device, bleibt direkt in localStorage
|
||||
// - Session/Auth — sessionStorage / Supabase-Auth-eigenes Storage
|
||||
// - Migrations — siehe migrations.js, läuft nach dem load auf den Rohdaten
|
||||
|
||||
import { STORAGE_KEY } from "../constants.js";
|
||||
import { SupabaseAdapter } from "./supabase-adapter.js";
|
||||
|
||||
export class LocalStorageAdapter {
|
||||
async hasExistingData() {
|
||||
return !!localStorage.getItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
async load() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (!stored) return null;
|
||||
return JSON.parse(stored);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async save(data) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.error("LocalStorage save failed:", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async clear() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
function createAdapter() {
|
||||
// Build-time-Default für Web-Deploy: wenn eine Production-Build VITE_SUPABASE_URL
|
||||
// gesetzt hat (z.B. via .env.production) und der User noch nichts gewählt hat,
|
||||
// wird Cloud automatisch der Modus. Damit landet ein Browser, der app.rapport.kgva.ch
|
||||
// öffnet, direkt im Cloud-Login (bzw. Init-Dialog wenn Server leer).
|
||||
// Dev (`npm run dev`) ist ausgenommen — dort sieht der User weiterhin den
|
||||
// BackendChoice-Screen, weil .env.local oft auf localhost zeigt.
|
||||
if (typeof localStorage !== "undefined" && import.meta.env.PROD) {
|
||||
const envUrl = import.meta.env.VITE_SUPABASE_URL;
|
||||
if (envUrl && !localStorage.getItem("rapport_backend_chosen")) {
|
||||
localStorage.setItem("rapport_backend_chosen", "1");
|
||||
localStorage.setItem("rapport_backend", "cloud");
|
||||
if (!localStorage.getItem("rapport_cloud_url")) {
|
||||
localStorage.setItem("rapport_cloud_url", envUrl.replace(/\/+$/, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const backend = (typeof localStorage !== "undefined"
|
||||
&& localStorage.getItem("rapport_backend")) || "local";
|
||||
if (backend === "cloud") {
|
||||
const url = import.meta.env.VITE_SUPABASE_URL;
|
||||
const key = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
if (!url || !key) {
|
||||
console.warn("rapport_backend=cloud, aber VITE_SUPABASE_URL/ANON_KEY fehlen — Fallback auf LocalStorage.");
|
||||
return new LocalStorageAdapter();
|
||||
}
|
||||
console.info("Storage-Adapter: SupabaseAdapter aktiv (URL:", url + ")");
|
||||
return new SupabaseAdapter(url, key);
|
||||
}
|
||||
return new LocalStorageAdapter();
|
||||
}
|
||||
|
||||
// Singleton — wird beim Modul-Load gewählt.
|
||||
// Switch zur Laufzeit erfordert (vorerst) einen App-Reload.
|
||||
export const storage = createAdapter();
|
||||
|
||||
export const isCloudBackend = storage instanceof SupabaseAdapter;
|
||||
|
||||
// Für Dev-Tests im Browser: window.__rapport.storage und ein Helper, um die
|
||||
// Cloud-Verbindung ohne UI zu testen.
|
||||
if (typeof window !== "undefined") {
|
||||
window.__rapport = window.__rapport || {};
|
||||
window.__rapport.storage = storage;
|
||||
window.__rapport.useCloud = () => {
|
||||
localStorage.setItem("rapport_backend", "cloud");
|
||||
location.reload();
|
||||
};
|
||||
window.__rapport.useLocal = () => {
|
||||
localStorage.setItem("rapport_backend", "local");
|
||||
location.reload();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user