6 Commits

Author SHA1 Message Date
karim 5a34d0a60f feat(login): Hinweis-Banner wenn Server nicht erreichbar
Ergaenzt den listStudios-Fix: statt stillem Fallback sieht der User jetzt
'Server nicht erreichbar', wenn die API beim Start nicht antwortet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:01:22 +02:00
karim 1846a00d07 fix(cloud): API-Fehler nicht als 'leere Instanz' werten
listStudios() gab bei Kong/API-Fehler [] zurück, was App.jsx als '0 Studios'
interpretierte und faelschlich den Init-/Registrierungs-Screen zeigte (statt
Login). Nach Reload war Kong wieder da -> Login. Daher das 'beim Ausloggen
kommt Init'-Symptom.

- supabase-adapter.listStudios(): wirft jetzt statt [] zu verschlucken
- App.jsx: catch setzt cloudStudios=null + cloudUnreachable=true
- Routing-Guard zeigt CloudSetup nur bei !cloudUnreachable && length===0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 14:57:52 +02:00
karim 4c04f1cb56 fix(storage): split_part statt storage.foldername in Policies
Die Storage-API droppt/erstellt storage.foldername() bei ihren Boot-
Migrations neu. Policies, die davon abhingen, blockierten den Drop
('cannot drop function foldername') und schickten die Storage-API in
eine Crash-Loop. split_part(name,'/',1) liefert dieselbe erste
Pfad-Komponente (studio_id) ohne diese Kopplung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 14:18:01 +02:00
karim df69a2dc6b fix(storage): public-Spalte aus bucket-Insert entfernen
storage.buckets hat beim Postgres-Init noch keine public-Spalte (fügt die
Storage-API erst beim Boot hinzu). Der Insert brach daher mit
ON_ERROR_STOP ab und verhinderte alle folgenden Migrations — u.a.
ensure_profile (0005), wodurch die User-Anlage im Self-Host scheiterte.
Default von public ist false (Buckets privat), Spalte ist verzichtbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 14:15:01 +02:00
karim bb69cc0657 Release 0.8.2: latest.json signiert für Updater
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 19:55:23 +02:00
karim edee7b9f28 0.8.2: Auto-Recovery für hängende 0.8.0-Installationen + UpdateNotifier vor Login
- Auto-Recovery in adapter.js: wenn Cloud-Modus gesetzt + lokale Daten
  vorhanden + keine Cloud-Session → Cloud-Konfiguration wird beim
  nächsten App-Start automatisch zurückgenommen. Marker rapport_080_recovery
  verhindert wiederholte Auslösung.
- UpdateNotifier wird in allen Pre-Login-Screens gerendert (BackendChoice,
  Setup, MigrationScreen, CloudSetup, Login) — so kann ein hängender
  Wizard sich via Auto-Update selbst befreien.
- Tauri-Builds ignorieren VITE_SUPABASE_URL aus dem Build. Desktop-User
  geben die Server-URL immer aktiv im Login ein, statt eine irrelevante
  Default-IP vorgesetzt zu bekommen.
- Anon-Key bleibt aus dem Build (kein Geheimnis, pro Cloud-Instanz fix).
- BackendChoice zeigt nur dann eine vorkonfigurierte URL, wenn nicht in Tauri.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 19:54:14 +02:00
11 changed files with 125 additions and 59 deletions
+5 -5
View File
@@ -1,11 +1,11 @@
{
"version": "0.8.1",
"notes": "Rapport 0.8.1",
"pub_date": "2026-05-23T17:32:24Z",
"version": "0.8.2",
"notes": "Rapport 0.8.2",
"pub_date": "2026-05-23T17:54:59Z",
"platforms": {
"darwin-aarch64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSUjV0czVoWDR4NGlsbU1CSGpuY1pPR01sUWZDYVBBVEZjY0RuVTVJSFlKSXM5N2k1SUVCVTFldlYvOFp2U3NLV1ZYYmhoMldMZG1NWjdqMnRpbVlhWTFZeHlKQ1BOUlFFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc5NTU3NTQ0CWZpbGU6UkFQUE9SVCBQUkUtUkVMRUFTRS5hcHAudGFyLmd6ClNwbkwxaEN5THlPTlJFQXd6TmQ4cWRjUG52NG1adDRUN1FXekgySGRGN3ZGci96dnZYdlNWTkdzaUtYSjlhajlZNUxQK2tnaytQZ1h2dG0zRDRtSkNnPT0K",
"url": "https://git.kgva.ch/karim/RAPPORT/releases/download/0.8.1/RAPPORT%20PRE-RELEASE.app.tar.gz"
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSUjV0czVoWDR4NHFSRmtHVUhpSkFlMDFwVS9XRGZjOGh1MnoybDNVcWh4ZkpIcGFQTFhVTHg5TVJ1SFA5L2kxbVQ5NUYvYmRsdlVmTHExcWZ4NThqeUZvQmk4WCtXTkEwPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc5NTU4ODk5CWZpbGU6UkFQUE9SVCBQUkUtUkVMRUFTRS5hcHAudGFyLmd6Cjdvc2VDeW0rbEQvbnJDeDJzbTRIS1VQT1l0VU5pRmNNT1g0Wkc4NXB3UENGVjFPUFdRM0gveE1jcENKZjFMUTRCQ2tMMHNTUDBORkNiYVpOODZZdUJRPT0K",
"url": "https://git.kgva.ch/karim/RAPPORT/releases/download/0.8.2/RAPPORT%20PRE-RELEASE.app.tar.gz"
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "rapport",
"private": true,
"version": "0.8.1",
"version": "0.8.2",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -2880,7 +2880,7 @@ dependencies = [
[[package]]
name = "rapport"
version = "0.8.0"
version = "0.8.1"
dependencies = [
"log",
"serde",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rapport"
version = "0.8.1"
version = "0.8.2"
description = "Rapport — Studio-Management für Architekturbüros"
authors = ["Karim Gabriele Varano <karim@gabrielevarano.ch>"]
license = "AGPL-3.0-or-later"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "RAPPORT PRE-RELEASE",
"version": "0.8.1",
"version": "0.8.2",
"identifier": "com.karimgabrielevarano.rapport",
"build": {
"frontendDist": "../dist",
+43 -16
View File
@@ -61,6 +61,10 @@ export default function App() {
// Cloud-spezifisch: Liste der Studios auf der Instanz (für Erst-Setup-Check).
// null = noch nicht geladen; Array = geladen.
const [cloudStudios, setCloudStudios] = useState(null);
// true, wenn listStudios() fehlschlug (Kong/API nicht erreichbar). Wird
// genutzt, um NICHT fälschlich den Init-/Registrierungs-Screen zu zeigen —
// ein API-Fehler ist nicht dasselbe wie "Instanz hat 0 Studios".
const [cloudUnreachable, setCloudUnreachable] = useState(false);
// Passwort-Reset-Flow: Supabase löst beim Klick auf Reset-Link in der Mail
// ein PASSWORD_RECOVERY-Event aus → wir zeigen dann das Reset-Formular.
const [passwordRecovery, setPasswordRecovery] = useState(false);
@@ -87,13 +91,15 @@ export default function App() {
console.error("Cloud-Resume load failed:", e);
}
}
// Studios der Instanz holen — entscheidet später, ob CloudSetup oder Login zeigt
// Studios der Instanz holen — entscheidet später, ob CloudSetup oder Login zeigt.
// Bei Fehler (Kong/API down): NICHT [] setzen (das hieße "Instanz leer" →
// fälschlich Init-Screen). Stattdessen Unreachable-Flag → Login bleibt.
try {
const list = await storage.listStudios?.();
if (!cancelled) setCloudStudios(list || []);
if (!cancelled) { setCloudStudios(list || []); setCloudUnreachable(false); }
} catch (e) {
console.error("listStudios failed:", e);
if (!cancelled) setCloudStudios([]);
if (!cancelled) { setCloudStudios(null); setCloudUnreachable(true); }
}
if (!cancelled) setLoading(false);
return;
@@ -297,8 +303,8 @@ export default function App() {
const [modal, setModal] = useState(null);
const [printContent, setPrintContent] = useState(null);
const [darkMode, setDarkMode] = useState(() => localStorage.getItem("rapport_dark") === "1");
const [showChangelog, setShowChangelog] = useState(() => localStorage.getItem("rapport_changelog_seen") !== "0.8.1");
const [changelogVersion, setChangelogVersion] = useState("0.8.1");
const [showChangelog, setShowChangelog] = useState(() => localStorage.getItem("rapport_changelog_seen") !== "0.8.2");
const [changelogVersion, setChangelogVersion] = useState("0.8.2");
const [showAbout, setShowAbout] = useState(false);
const [navOpen, setNavOpen] = useState(false);
const [expandedNav, setExpandedNav] = useState(new Set(["buchhaltung"]));
@@ -456,19 +462,21 @@ export default function App() {
// Erst-Screen einer frischen Installation: «Lokal oder Cloud?».
// Sichtbar solange der User die Wahl noch nicht getroffen hat UND es keine
// lokalen Daten gibt. Sobald er gewählt hat, übernimmt der jeweilige Wizard.
// UpdateNotifier wird in allen Pre-Login-Screens mitgerendert, damit ein
// hängender Setup-Wizard sich via Auto-Update selbst befreien kann.
const hasChosenBackend = localStorage.getItem("rapport_backend_chosen") === "1";
if (!hasChosenBackend && isNewInstall && !data.settings.setupCompleted && !currentUser) {
return <BackendChoice />;
return <><BackendChoice /><UpdateNotifier /></>;
}
// Setup- und Migrations-Screens sind LocalStorage-Spezifika. Im Cloud-Modus
// erfolgt Erst-Einrichtung über den Init-Dialog im Login.
if (!isCloudBackend && isNewInstall && !data.settings.setupCompleted) {
return <Setup onComplete={handleSetupComplete} />;
return <><Setup onComplete={handleSetupComplete} /><UpdateNotifier /></>;
}
if (!isCloudBackend && !localStorage.getItem("rapport_v0_5_migrated")) {
return <MigrationScreen data={data} onComplete={handleSetupComplete} />;
return <><MigrationScreen data={data} onComplete={handleSetupComplete} /><UpdateNotifier /></>;
}
// Passwort-Reset hat höchste Priorität — User kommt von Mail-Link
@@ -493,12 +501,24 @@ export default function App() {
// Cloud + Instanz ist leer (0 Studios) → mehrseitiger Setup-Wizard.
// Cloud + Studios vorhanden → klassischer Login.
// UpdateNotifier wird hier auch gerendert, damit der Auto-Update-Check auch
// ohne Login läuft (sonst kommt man bei einem fehlerhaften Setup-Screen nie
// an ein neueres Build, das den Bug fixt).
if (!currentUser) {
if (isCloudBackend && cloudStudios !== null && cloudStudios.length === 0) {
// Init-/Registrierungs-Screen NUR wenn der API-Call erfolgreich war UND
// wirklich 0 Studios lieferte. Bei !cloudUnreachable ausgeschlossen, dass
// ein Kong/API-Fehler (cloudStudios === null) hier fälschlich Init zeigt.
if (isCloudBackend && !cloudUnreachable && cloudStudios !== null && cloudStudios.length === 0) {
const cloudUrl = localStorage.getItem("rapport_cloud_url") || "";
return <CloudSetup cloudInit={cloudInit} cloudUrl={cloudUrl} />;
return <>
<CloudSetup cloudInit={cloudInit} cloudUrl={cloudUrl} />
<UpdateNotifier />
</>;
}
return <Login verifyLogin={verifyLogin} settings={data.settings} version="0.8.1" />;
return <>
<Login verifyLogin={verifyLogin} settings={data.settings} version="0.8.2" cloudUnreachable={cloudUnreachable} />
<UpdateNotifier />
</>;
}
if (printContent) {
@@ -771,8 +791,8 @@ export default function App() {
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<button onClick={() => setShowAbout(true)} style={{ background: "none", border: "none", padding: 0, color: "#555", fontSize: 10, letterSpacing: "0.08em", cursor: "pointer", fontFamily: "inherit", textAlign: "left" }}
onMouseEnter={e => e.currentTarget.style.color = "#aaa"} onMouseLeave={e => e.currentTarget.style.color = "#555"}>ÜBER RAPPORT</button>
<button onClick={() => { setChangelogVersion("0.8.1"); setShowChangelog(true); }} style={{ background: "none", border: "none", padding: 0, color: "#aaa", fontSize: 10, letterSpacing: "0.08em", cursor: "pointer", fontFamily: "inherit" }}
onMouseEnter={e => e.currentTarget.style.color = "#f0ede8"} onMouseLeave={e => e.currentTarget.style.color = "#aaa"}>0.8.1</button>
<button onClick={() => { setChangelogVersion("0.8.2"); setShowChangelog(true); }} style={{ background: "none", border: "none", padding: 0, color: "#aaa", fontSize: 10, letterSpacing: "0.08em", cursor: "pointer", fontFamily: "inherit" }}
onMouseEnter={e => e.currentTarget.style.color = "#f0ede8"} onMouseLeave={e => e.currentTarget.style.color = "#aaa"}>0.8.2</button>
</div>
</div>}
@@ -837,6 +857,13 @@ export default function App() {
{showChangelog && (() => {
const CHANGELOGS = {
"0.8.2": {
items: [
["Selbstheilung für hängende 0.8.0-Installationen", "Wer von 0.7 auf 0.8 geupdated hat und in den Cloud-Setup-Wizard geschoben wurde, kommt mit 0.8.2 automatisch zurück in seinen Lokal-Modus. Der Auto-Recovery-Code erkennt: Cloud-Modus gesetzt + lokale Daten vorhanden + keine Cloud-Anmeldung → Cloud-Konfiguration wird zurückgenommen, alle Daten bleiben erhalten."],
["Auto-Update auch ohne Login", "Bisher prüfte Rapport erst nach dem Login auf Updates — wer in einem fehlerhaften Setup-Bildschirm hing, kam nicht an den Bugfix. Jetzt läuft der Update-Check auch im «Lokal oder Cloud»-Wizard, im Login-Screen und im Cloud-Setup."],
["Tauri ohne fest eingebaute Server-IP", "Die Desktop-App enthält keine vorkonfigurierte Cloud-Adresse mehr. Wer Cloud nutzen will, gibt die Server-Adresse beim Login aktiv ein — kein automatisches Vorausfüllen mit irrelevanten IPs."],
],
},
"0.8.1": {
items: [
["Update-Fix", "Behebt einen Fehler beim Upgrade von 0.7 auf 0.8: Lokal-Installationen wurden ungewollt in den Cloud-Modus geschoben und der Cloud-Setup-Wizard angezeigt, obwohl bereits lokale Daten vorhanden waren. Die App prüft jetzt vor einem automatischen Modus-Wechsel, ob lokale Daten existieren — und in Tauri-Installationen wird der Modus nie implizit gesetzt."],
@@ -919,7 +946,7 @@ export default function App() {
},
};
const versions = Object.keys(CHANGELOGS);
const current = CHANGELOGS[changelogVersion] || CHANGELOGS["0.8.1"];
const current = CHANGELOGS[changelogVersion] || CHANGELOGS["0.8.2"];
return (
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.55)", zIndex: 200, display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}>
<div style={{ background: "#fff", borderRadius: 10, width: "100%", maxWidth: 480, boxShadow: "0 8px 40px rgba(0,0,0,0.18)", overflow: "hidden" }}>
@@ -948,7 +975,7 @@ export default function App() {
))}
</div>
<div style={{ padding: "12px 32px 24px" }}>
<button className="btn btn-primary" style={{ width: "100%", fontSize: 13 }} onClick={() => { setShowChangelog(false); localStorage.setItem("rapport_changelog_seen", "0.8.1"); }}>
<button className="btn btn-primary" style={{ width: "100%", fontSize: 13 }} onClick={() => { setShowChangelog(false); localStorage.setItem("rapport_changelog_seen", "0.8.2"); }}>
Schliessen
</button>
</div>
@@ -963,7 +990,7 @@ export default function App() {
<div style={{ background: "#1a1a18", padding: "28px 32px 24px" }}>
<div style={{ fontSize: 10, letterSpacing: "0.18em", color: "#b07848", marginBottom: 8, fontWeight: 600 }}>ÜBER RAPPORT</div>
<div style={{ fontFamily: "'Playfair Display', serif", fontSize: 28, color: "#f0ede8", fontWeight: 400, lineHeight: 1.1 }}>Rapport</div>
<div style={{ fontSize: 11, color: "#888", marginTop: 6, letterSpacing: "0.04em" }}>Alpha 0.8.1 · Studio-Management für Architekturbüros</div>
<div style={{ fontSize: 11, color: "#888", marginTop: 6, letterSpacing: "0.04em" }}>Alpha 0.8.2 · Studio-Management für Architekturbüros</div>
</div>
<div style={{ padding: "20px 32px 8px" }}>
<div style={{ fontSize: 11, fontWeight: 600, color: "#888", letterSpacing: "0.1em", marginBottom: 12 }}>LIZENZ</div>
+36 -20
View File
@@ -46,22 +46,37 @@ export class LocalStorageAdapter {
}
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).
//
// Bewusst NICHT auto-cloud wenn:
// - Dev-Build (`npm run dev`) — User sieht weiterhin BackendChoice
// - lokale Daten existieren (würde bestehenden Lokal-User auf Cloud zwingen
// und seine Daten unsichtbar machen) — Bug bei 0.7→0.8-Upgrade
// - Tauri-Desktop-App (Tauri-User sollen immer aktiv wählen, nicht implizit
// aufgrund von .env.production eines Builds umgehauen werden)
if (typeof localStorage !== "undefined" && import.meta.env.PROD) {
const envUrl = import.meta.env.VITE_SUPABASE_URL;
const isTauri = typeof window !== "undefined" && !!window.__TAURI_INTERNALS__;
// Build-time-URL nur für Web-Deploy gültig. Tauri-Builds ignorieren den
// eingebrannten Wert — Desktop-User geben die Server-URL aktiv ein.
// Der Anon-Key bleibt aus dem Build, weil er pro Cloud-Instanz konstant ist
// (kein User-Geheimnis, sondern Public-Konfig).
const envUrl = isTauri ? null : import.meta.env.VITE_SUPABASE_URL;
const envKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (typeof localStorage !== "undefined") {
// ── Auto-Recovery für 0.8.0-Upgrade-Bug ──────────────────────────────
// 0.8.0 hat Cloud-Modus ungewollt gesetzt bei Lokal-Usern (siehe 0.8.1
// CHANGELOG). 0.8.1 verhindert das nur prospektiv, behebt aber den
// bestehenden Cloud-State nicht. Hier räumen wir nachträglich auf:
// Cloud-Modus gesetzt + lokale Daten vorhanden + keine Cloud-Session
// → Cloud-State löschen, User landet im BackendChoice oder Lokal-Modus.
const RECOVERY_MARKER = "rapport_080_recovery";
const backend = localStorage.getItem("rapport_backend");
const hasLocalData = !!localStorage.getItem(STORAGE_KEY);
const isTauri = typeof window !== "undefined" && !!window.__TAURI_INTERNALS__;
if (envUrl && !localStorage.getItem("rapport_backend_chosen") && !hasLocalData && !isTauri) {
const hasCloudSession = !!localStorage.getItem("rapport_supabase_session");
if (backend === "cloud" && hasLocalData && !hasCloudSession && !localStorage.getItem(RECOVERY_MARKER)) {
console.warn("Auto-Recovery: Cloud-Modus gesetzt, aber Lokal-Daten vorhanden und kein Cloud-Login — räume Cloud-State auf.");
localStorage.removeItem("rapport_backend");
localStorage.removeItem("rapport_backend_chosen");
localStorage.removeItem("rapport_cloud_url");
localStorage.setItem(RECOVERY_MARKER, "1");
}
// ── Auto-Cloud-Default für Web-Deploy ────────────────────────────────
// Nur wenn keine lokalen Daten existieren und der Browser auf einer
// konfigurierten Web-Instanz landet (envUrl aus build).
if (import.meta.env.PROD && envUrl && !localStorage.getItem("rapport_backend_chosen") && !hasLocalData) {
localStorage.setItem("rapport_backend_chosen", "1");
localStorage.setItem("rapport_backend", "cloud");
if (!localStorage.getItem("rapport_cloud_url")) {
@@ -73,14 +88,15 @@ function createAdapter() {
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.");
// URL kommt bevorzugt aus localStorage (vom User eingegeben). Tauri hat
// gar keine env-URL; Web-Build hat sie ggf. als Fallback.
const url = (typeof localStorage !== "undefined" && localStorage.getItem("rapport_cloud_url")) || envUrl;
if (!url || !envKey) {
console.warn("rapport_backend=cloud, aber URL oder ANON_KEY fehlen — Fallback auf LocalStorage.");
return new LocalStorageAdapter();
}
console.info("Storage-Adapter: SupabaseAdapter aktiv (URL:", url + ")");
return new SupabaseAdapter(url, key);
return new SupabaseAdapter(url, envKey);
}
return new LocalStorageAdapter();
}
+7 -4
View File
@@ -125,10 +125,13 @@ export class SupabaseAdapter {
// Kein Auth nötig (RPC läuft als SECURITY DEFINER).
async listStudios() {
const { data, error } = await this.client.rpc("list_studios");
if (error) {
console.error("listStudios:", error.message);
return [];
}
// WICHTIG: Fehler NICHT zu [] verschlucken. Ein leeres Array bedeutet
// "Instanz hat 0 Studios" → das Frontend zeigt dann den Registrierungs-/
// Init-Screen. Ein Netzwerk-/API-Fehler (Kong down, Port 8000 nicht
// erreichbar) ist aber NICHT dasselbe wie "leer" — würden wir hier []
// zurückgeben, landet ein eingeloggter User nach Reload fälschlich im
// Init-Flow. Daher: werfen und den Caller entscheiden lassen.
if (error) throw new Error("listStudios: " + error.message);
return data || [];
}
+4 -1
View File
@@ -7,7 +7,10 @@ import React from "react";
// Lokal → bestehender Setup.jsx
// Cloud → Login mit Init-Modus oder Login-Modus (je nach Studio-Vorhandensein)
const envCloudUrl = import.meta.env.VITE_SUPABASE_URL || "";
// Tauri-User geben die Server-URL immer aktiv ein. Build-time-URL ist nur
// für Web-Deploy gedacht (z.B. app.rapport.kgva.ch → 127.0.0.1:54321 lokal).
const isTauri = typeof window !== "undefined" && !!window.__TAURI_INTERNALS__;
const envCloudUrl = isTauri ? "" : (import.meta.env.VITE_SUPABASE_URL || "");
export default function BackendChoice() {
const pick = (backend, cloudUrl = "") => {
+7 -1
View File
@@ -16,7 +16,7 @@ function writeAttempts(state) {
try { sessionStorage.setItem(ATTEMPT_KEY, JSON.stringify(state)); } catch {}
}
export default function Login({ verifyLogin, settings, version }) {
export default function Login({ verifyLogin, settings, version, cloudUnreachable = false }) {
// Backend-Modus aus localStorage (per-Device, ähnlich Dark Mode).
// Beim Wechsel wird die App neu geladen, damit der Storage-Adapter neu initialisiert.
const [backend, setBackend] = useState(() => localStorage.getItem("rapport_backend") || "local");
@@ -236,6 +236,12 @@ export default function Login({ verifyLogin, settings, version }) {
<div style={{ width: 32, height: 1.5, background: "#ddd8d0", margin: "16px auto 0" }} />
</div>
{cloudUnreachable && (
<div style={{ marginBottom: 18, padding: "9px 14px", background: "#fff5f0", borderRadius: 8, border: "1px solid #f5c9b0", fontSize: 11, color: "#b5621e", textAlign: "center", lineHeight: 1.5 }}>
Server nicht erreichbar. Bitte Verbindung prüfen und neu laden.
</div>
)}
<form onSubmit={handleSubmit}>
{isCloud && studios.length > 1 && (
<div style={{ marginBottom: 14 }}>
+19 -8
View File
@@ -11,44 +11,55 @@
-- Buckets sind PRIVATE — Zugriff nur über signierte URLs (zeitlich begrenzt).
-- ============================================================================
insert into storage.buckets (id, name, public)
-- Hinweis: KEINE `public`-Spalte angeben. Beim Postgres-Init existiert sie in
-- storage.buckets noch nicht (die fügt die Storage-API erst beim Boot per
-- eigener Migration hinzu). Default ist `false` → Buckets sind privat, wie
-- gewünscht. Würden wir `public` referenzieren, bräche der Init hier ab und
-- ALLE folgenden Migrations (inkl. ensure_profile in 0005) liefen nicht mehr.
insert into storage.buckets (id, name)
values
('receipts', 'receipts', false),
('logos', 'logos', false)
('receipts', 'receipts'),
('logos', 'logos')
on conflict (id) do nothing;
-- ────────────────────────────────────────────────────────────────────────────
-- RLS-Policies auf storage.objects
-- ────────────────────────────────────────────────────────────────────────────
-- Prinzip: erste Pfad-Komponente ist studio_id; Zugriff nur wenn Member.
-- `(storage.foldername(name))[1]` gibt die erste Pfad-Komponente zurück.
-- `split_part(name, '/', 1)` gibt die erste Pfad-Komponente zurück.
--
-- Bewusst NICHT storage.foldername() benutzen: die Storage-API droppt/erstellt
-- diese Funktion bei ihren eigenen Boot-Migrations neu. Eine Policy-Abhängigkeit
-- darauf würde diesen Drop blockieren ("cannot drop function foldername") und
-- die Storage-API in eine Crash-Loop schicken. split_part ist ein eingebautes
-- Postgres-Builtin ohne diese Kopplung.
create policy "rapport_storage_read"
on storage.objects for select
using (
bucket_id in ('receipts','logos')
and is_studio_member( (storage.foldername(name))[1]::uuid )
and is_studio_member( split_part(name, '/', 1)::uuid )
);
create policy "rapport_storage_insert"
on storage.objects for insert
with check (
bucket_id in ('receipts','logos')
and is_studio_member( (storage.foldername(name))[1]::uuid )
and is_studio_member( split_part(name, '/', 1)::uuid )
);
create policy "rapport_storage_update"
on storage.objects for update
using (
bucket_id in ('receipts','logos')
and is_studio_member( (storage.foldername(name))[1]::uuid )
and is_studio_member( split_part(name, '/', 1)::uuid )
);
create policy "rapport_storage_delete"
on storage.objects for delete
using (
bucket_id in ('receipts','logos')
and is_studio_member( (storage.foldername(name))[1]::uuid )
and is_studio_member( split_part(name, '/', 1)::uuid )
);
-- ────────────────────────────────────────────────────────────────────────────