Compare commits
5 Commits
5a34d0a60f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e0cf5f1381 | |||
| f88825ebe0 | |||
| afc6163b2d | |||
| 44ddc5ee12 | |||
| 266e7d61d4 |
@@ -3,8 +3,15 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#1a1a18" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Rapport" />
|
||||
<meta name="description" content="Studio-Management für Architekturbüros" />
|
||||
<title>Rapport</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rapport",
|
||||
"private": true,
|
||||
"version": "0.8.2",
|
||||
"version": "0.8.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "Rapport",
|
||||
"short_name": "Rapport",
|
||||
"description": "Studio-Management für Architekturbüros",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#ebe7e1",
|
||||
"theme_color": "#1a1a18",
|
||||
"lang": "de-CH",
|
||||
"categories": ["business", "productivity"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/pwa-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/pwa-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
Generated
+1
-1
@@ -2880,7 +2880,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rapport"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rapport"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
description = "Rapport — Studio-Management für Architekturbüros"
|
||||
authors = ["Karim Gabriele Varano <karim@gabrielevarano.ch>"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "RAPPORT PRE-RELEASE",
|
||||
"version": "0.8.2",
|
||||
"version": "0.8.3",
|
||||
"identifier": "com.karimgabrielevarano.rapport",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
+19
-10
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useCallback, useRef, Suspense, lazy } from "react";
|
||||
import { NAV_ITEMS, defaultData } from "./constants.js";
|
||||
import { verifyPassword, withHashedPassword, stripCredentials } from "./utils.js";
|
||||
import { storage, isCloudBackend } from "./storage/adapter.js";
|
||||
import { storage, isCloudBackend, isServerMode } from "./storage/adapter.js";
|
||||
import { applyMigrations } from "./storage/migrations.js";
|
||||
import Login from "./views/Login.jsx";
|
||||
import Setup from "./views/Setup.jsx";
|
||||
@@ -303,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.2");
|
||||
const [changelogVersion, setChangelogVersion] = useState("0.8.2");
|
||||
const [showChangelog, setShowChangelog] = useState(() => localStorage.getItem("rapport_changelog_seen") !== "0.8.3");
|
||||
const [changelogVersion, setChangelogVersion] = useState("0.8.3");
|
||||
const [showAbout, setShowAbout] = useState(false);
|
||||
const [navOpen, setNavOpen] = useState(false);
|
||||
const [expandedNav, setExpandedNav] = useState(new Set(["buchhaltung"]));
|
||||
@@ -464,8 +464,10 @@ export default function App() {
|
||||
// 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.
|
||||
// Server-Modus (gehostete Web-GUI): nie die Lokal/Server-Wahl zeigen — die
|
||||
// App ist fest an diesen Server gebunden. Nur die lokale DMG zeigt die Wahl.
|
||||
const hasChosenBackend = localStorage.getItem("rapport_backend_chosen") === "1";
|
||||
if (!hasChosenBackend && isNewInstall && !data.settings.setupCompleted && !currentUser) {
|
||||
if (!isServerMode && !hasChosenBackend && isNewInstall && !data.settings.setupCompleted && !currentUser) {
|
||||
return <><BackendChoice /><UpdateNotifier /></>;
|
||||
}
|
||||
|
||||
@@ -516,7 +518,7 @@ export default function App() {
|
||||
</>;
|
||||
}
|
||||
return <>
|
||||
<Login verifyLogin={verifyLogin} settings={data.settings} version="0.8.2" cloudUnreachable={cloudUnreachable} />
|
||||
<Login verifyLogin={verifyLogin} settings={data.settings} version="0.8.3" cloudUnreachable={cloudUnreachable} />
|
||||
<UpdateNotifier />
|
||||
</>;
|
||||
}
|
||||
@@ -791,8 +793,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.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>
|
||||
<button onClick={() => { setChangelogVersion("0.8.3"); 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.3</button>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
@@ -857,6 +859,13 @@ export default function App() {
|
||||
|
||||
{showChangelog && (() => {
|
||||
const CHANGELOGS = {
|
||||
"0.8.3": {
|
||||
items: [
|
||||
["Desktop-App-Verhalten", "Kein Text mehr markierbar, kein Rechtsklick-Menü — die App verhält sich jetzt wie eine native Desktop-Applikation. Eingabefelder sind weiterhin voll benutzbar."],
|
||||
["PWA-Support", "Die Web-Version lässt sich auf dem Homebildschirm von iPhone/iPad und Android hinzufügen. Nach dem Hinzufügen öffnet sie sich vollbild ohne Browser-Chrome — mit eigenem App-Icon und Name."],
|
||||
["Testmodus klar gekennzeichnet", "Wer Rapport ohne Server ausprobiert, sieht jetzt explizit, dass es sich um einen Testmodus mit Einschränkungen handelt (5 MB Limit, kein Backup, kein Mehrbenutzer)."],
|
||||
],
|
||||
},
|
||||
"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."],
|
||||
@@ -946,7 +955,7 @@ export default function App() {
|
||||
},
|
||||
};
|
||||
const versions = Object.keys(CHANGELOGS);
|
||||
const current = CHANGELOGS[changelogVersion] || CHANGELOGS["0.8.2"];
|
||||
const current = CHANGELOGS[changelogVersion] || CHANGELOGS["0.8.3"];
|
||||
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" }}>
|
||||
@@ -975,7 +984,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.2"); }}>
|
||||
<button className="btn btn-primary" style={{ width: "100%", fontSize: 13 }} onClick={() => { setShowChangelog(false); localStorage.setItem("rapport_changelog_seen", "0.8.3"); }}>
|
||||
Schliessen
|
||||
</button>
|
||||
</div>
|
||||
@@ -990,7 +999,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.2 · Studio-Management für Architekturbüros</div>
|
||||
<div style={{ fontSize: 11, color: "#888", marginTop: 6, letterSpacing: "0.04em" }}>Alpha 0.8.3 · 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>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; height: 100%; }
|
||||
#root { height: 100%; }
|
||||
|
||||
/* Desktop-App-Verhalten: kein Textmarkieren, kein Drag */
|
||||
* { user-select: none; -webkit-user-drag: none; }
|
||||
input, textarea, [contenteditable] { user-select: text; }
|
||||
|
||||
@@ -3,6 +3,8 @@ import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
document.addEventListener('contextmenu', e => e.preventDefault())
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
|
||||
+26
-1
@@ -45,8 +45,17 @@ export class LocalStorageAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// SERVER-MODUS: dieser Build ist die gehostete Web-GUI eines bestimmten
|
||||
// Servers (gesetzt via VITE_SERVER_MODE=1 im Dockerfile.app). Dann ist die App
|
||||
// FEST an diesen einen Server gebunden — keine Lokal/Server-Wahl, kein Wechsel
|
||||
// der Server-Adresse, kein Verbinden auf andere Instanzen. Nur der Login auf
|
||||
// genau diesem Server. Die lokale DMG (Tauri) setzt diese Flag NIE und behält
|
||||
// die volle Wahl (Lokal / beliebige Server-IP).
|
||||
const _isTauri = typeof window !== "undefined" && !!window.__TAURI_INTERNALS__;
|
||||
export const isServerMode = import.meta.env.VITE_SERVER_MODE === "1" && !_isTauri;
|
||||
|
||||
function createAdapter() {
|
||||
const isTauri = typeof window !== "undefined" && !!window.__TAURI_INTERNALS__;
|
||||
const isTauri = _isTauri;
|
||||
// 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
|
||||
@@ -54,6 +63,22 @@ function createAdapter() {
|
||||
const envUrl = isTauri ? null : import.meta.env.VITE_SUPABASE_URL;
|
||||
const envKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
|
||||
// ── Server-Modus: fest auf diesen Server, ohne Wahlmöglichkeit ───────────
|
||||
if (isServerMode) {
|
||||
if (!envUrl || !envKey) {
|
||||
console.error("VITE_SERVER_MODE=1, aber VITE_SUPABASE_URL/ANON_KEY fehlen — Fehlkonfiguration des Web-Builds.");
|
||||
return new LocalStorageAdapter();
|
||||
}
|
||||
// localStorage konsistent halten (Login liest cloud_url für listStudios),
|
||||
// aber der User kann nichts davon ändern — die Werte kommen aus dem Build.
|
||||
if (typeof localStorage !== "undefined") {
|
||||
localStorage.setItem("rapport_backend", "cloud");
|
||||
localStorage.setItem("rapport_backend_chosen", "1");
|
||||
localStorage.setItem("rapport_cloud_url", envUrl.replace(/\/+$/, ""));
|
||||
}
|
||||
return new SupabaseAdapter(envUrl, envKey);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -82,10 +82,13 @@ export default function BackendChoice() {
|
||||
</p>
|
||||
|
||||
<button className="bc-option" onClick={() => pick("local")}>
|
||||
<div className="bc-title">Lokal auf diesem Gerät</div>
|
||||
<div className="bc-title" style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
Testmodus (lokal)
|
||||
<span style={{ fontSize: 10, fontWeight: 600, letterSpacing: "0.08em", color: "#9a7858", background: "rgba(154,120,88,0.10)", borderRadius: 4, padding: "2px 6px" }}>TEST</span>
|
||||
</div>
|
||||
<div className="bc-desc">
|
||||
Daten liegen ausschliesslich in diesem Browser / dieser App. Kein Server nötig.
|
||||
Ideal zum Ausprobieren oder als Solo-Setup.
|
||||
Daten liegen nur in diesem Browser. Kein Backup, kein Mehrbenutzer, begrenzt auf ~5 MB.
|
||||
Nur zum Ausprobieren — für den produktiven Einsatz einen Server verbinden.
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
||||
+10
-3
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { storage, isCloudBackend } from "../storage/adapter.js";
|
||||
import { storage, isCloudBackend, isServerMode } from "../storage/adapter.js";
|
||||
|
||||
const isValidEmail = (s) => /.+@.+\..+/.test(s);
|
||||
|
||||
@@ -121,8 +121,10 @@ export default function Login({ verifyLogin, settings, version, cloudUnreachable
|
||||
const userPlaceholder = isCloud ? "name@studio.ch" : "admin";
|
||||
const userInputType = isCloud ? "email" : "text";
|
||||
|
||||
const showUrlField = isCloud && editUrl;
|
||||
const showUrlBadge = isCloud && !editUrl && cloudUrl;
|
||||
// Im Server-Modus (gehostete Web-GUI) gibt es keine Server-Adress-Eingabe —
|
||||
// die App ist fest an diesen Server gebunden.
|
||||
const showUrlField = isCloud && editUrl && !isServerMode;
|
||||
const showUrlBadge = isCloud && !editUrl && cloudUrl && !isServerMode;
|
||||
|
||||
// Hostname zur Anzeige (ohne Protokoll, ohne Port falls Standard)
|
||||
let urlDisplay = cloudUrl;
|
||||
@@ -381,6 +383,10 @@ export default function Login({ verifyLogin, settings, version, cloudUnreachable
|
||||
)}
|
||||
|
||||
{/* Verbindung-Switch + Server-Anzeige (dezent darunter) */}
|
||||
{/* Verbindungs-Switch (Lokal/Server + Server-Adresse) nur in der lokalen
|
||||
DMG. Die gehostete Web-GUI (Server-Modus) ist fest an diesen Server
|
||||
gebunden — keine Wahl, kein Wechsel. */}
|
||||
{!isServerMode && (
|
||||
<div style={{
|
||||
marginTop: 22, paddingTop: 16,
|
||||
borderTop: "1px solid #ebe7e1",
|
||||
@@ -413,6 +419,7 @@ export default function Login({ verifyLogin, settings, version, cloudUnreachable
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 16, textAlign: "center", fontSize: 9, color: "#c8c4be", letterSpacing: "0.08em" }}>
|
||||
{version ? `V${version}` : ""}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
-- ============================================================================
|
||||
-- RAPPORT — Studio für einen bestimmten User anlegen (Server-/Hosting-Pfad)
|
||||
-- ============================================================================
|
||||
-- `create_studio_with_admin` nutzt auth.uid() und läuft nur im Kontext eines
|
||||
-- eingeloggten Users (Frontend). RAPPORT-HOST provisioniert aber serverseitig
|
||||
-- mit service_role und kennt keinen auth.uid() — es übergibt die Ziel-User-ID
|
||||
-- explizit.
|
||||
--
|
||||
-- `create_studio_for_user` ist die service_role-Variante: identische Wirkung
|
||||
-- (Studio + Admin-Membership + Settings), aber die User-ID ist ein Parameter.
|
||||
-- Bewusst NICHT an `authenticated` gegrantet — nur service_role darf das, sonst
|
||||
-- könnte ein User sich selbst zum Admin beliebiger Studios machen.
|
||||
-- ============================================================================
|
||||
|
||||
create or replace function create_studio_for_user(
|
||||
p_user_id uuid,
|
||||
p_name text,
|
||||
p_slug text,
|
||||
p_username text default null,
|
||||
p_display_name text default null
|
||||
)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
v_studio_id uuid;
|
||||
v_email text;
|
||||
v_username text;
|
||||
v_display text;
|
||||
begin
|
||||
if p_user_id is null then
|
||||
raise exception 'p_user_id required';
|
||||
end if;
|
||||
select email into v_email from auth.users where id = p_user_id;
|
||||
if v_email is null then
|
||||
raise exception 'user % does not exist', p_user_id;
|
||||
end if;
|
||||
|
||||
-- Profil sicherstellen (profiles.username/display_name sind NOT NULL; das
|
||||
-- Frontend braucht sie beim ersten Login in die Instanz). Aus E-Mail
|
||||
-- abgeleitet, falls nicht explizit übergeben.
|
||||
v_username := coalesce(nullif(p_username, ''), split_part(v_email, '@', 1));
|
||||
v_display := coalesce(nullif(p_display_name, ''), v_username);
|
||||
insert into profiles (id, username, display_name)
|
||||
values (p_user_id, v_username, v_display)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
insert into studios (name, slug) values (p_name, p_slug) returning id into v_studio_id;
|
||||
insert into studio_members (studio_id, user_id, app_role_id)
|
||||
values (v_studio_id, p_user_id, 'r-admin');
|
||||
|
||||
-- Studio-Name + setup_completed in die settings übernehmen (Seed-Trigger hat
|
||||
-- die Zeile mit Defaults bereits angelegt) — analog create_studio_with_admin.
|
||||
update studio_settings
|
||||
set name = p_name, setup_completed = true
|
||||
where studio_id = v_studio_id;
|
||||
|
||||
return v_studio_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Nur service_role (RAPPORT-HOST). KEIN Grant an anon/authenticated.
|
||||
revoke all on function create_studio_for_user(uuid, text, text, text, text) from public;
|
||||
grant execute on function create_studio_for_user(uuid, text, text, text, text) to service_role;
|
||||
Reference in New Issue
Block a user