Files
RAPPORT/src/views/MigrationScreen.jsx
T
karim 00f07d76f6 Rapport 0.6 — Initial Public Release
Sicherheits-Hardening
- Passwort-Hashing mit PBKDF2 (SHA-256, 100k Iterationen) inkl. transparenter
  Migration bestehender Klartext-Passwörter beim ersten Login
- Login Brute-Force-Schutz (5 Fehlversuche → 60s Lockout), Constant-Time-Compare,
  Mindestpasswortlänge 8 Zeichen
- HTML-Sanitizer für Brieftexte (Allowlist, entfernt javascript:/data:/vbscript:-URLs,
  Event-Handler, Script-Tags; rel=noopener für target=_blank)
- Datenexport entfernt Legacy-Klartextpasswörter (Hashes bleiben)
- Kryptografische IDs via crypto.randomUUID statt Math.random
- sessionStorage speichert keine Credentials mehr

GUI & Performance
- Code-Splitting pro View via React.lazy + Suspense (Initial-Bundle 86 KB gzipped)
- swissqrbill als lokale Dependency — QR-Rechnungen offline-fähig
- Spesenbelege (Bild/PDF) direkt in der Tageserfassung mit Bildkomprimierung
- Avatar-Upload: 256px-Skalierung + JPEG-Kompression, Typprüfung
- Über-Rapport-Modal, einheitliche Bearbeiten-Icons, Pinnwand-Kategorien als Pills

Bug-Fixes
- Auto-überfällig-Routine läuft nur noch einmal pro Tag (verhindert Re-Render-Loop)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 01:16:26 +02:00

142 lines
6.2 KiB
React

import React, { useState } from "react";
import Setup from "./Setup.jsx";
export default function MigrationScreen({ data, onComplete }) {
const [backed, setBacked] = useState(false);
const [goSetup, setGoSetup] = useState(false);
const studioName = data.settings?.name || "Studio";
if (goSetup) {
return <Setup onComplete={onComplete} />;
}
const handleBackup = () => {
try {
const stored = localStorage.getItem("studio_data_v1") || JSON.stringify(data);
const blob = new Blob([stored], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `rapport-backup-${new Date().toISOString().split("T")[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch {}
setBacked(true);
};
return (
<div style={{
minHeight: "100vh", minWidth: "100vw",
background: "#ebe7e1",
display: "flex", alignItems: "center", justifyContent: "center",
fontFamily: "'DM Mono', 'Courier New', monospace",
position: "fixed", inset: 0, zIndex: 9999,
overflowY: "auto", padding: "24px 0",
}}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:wght@300;400;500&display=swap');
@keyframes mig-fade-in {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.mig-card { animation: mig-fade-in 0.5s cubic-bezier(0.22,1,0.36,1) both; }
.mig-btn-primary {
width: 100%; padding: 13px;
background: #1a1a18; color: #f0ede8;
border: none; border-radius: 10px;
font-family: 'DM Mono', monospace; font-size: 13px;
font-weight: 500; letter-spacing: 0.04em;
cursor: pointer; transition: background 0.18s;
}
.mig-btn-primary:hover { background: #2e2e28; }
.mig-btn-backup {
width: 100%; padding: 13px;
background: transparent; color: #1a1a18;
border: 2px solid #1a1a18; border-radius: 10px;
font-family: 'DM Mono', monospace; font-size: 13px;
font-weight: 500; letter-spacing: 0.04em;
cursor: pointer; transition: background 0.18s;
display: flex; align-items: center; justify-content: center; gap: 8px;
}
.mig-btn-backup:hover { background: #f0ece6; }
.mig-btn-backup.done { border-color: #2d6a4f; color: #2d6a4f; background: #eaf5ee; cursor: default; }
.mig-skip { background: none; border: none; font-family: inherit; font-size: 11px; color: #aaa; cursor: pointer; text-decoration: underline; padding: 0; }
.mig-skip:hover { color: #888; }
`}</style>
<div className="mig-card" style={{
background: "#fdfcfa",
borderRadius: 20,
padding: "44px 40px 36px",
width: "100%", maxWidth: 440,
boxShadow: "0 8px 40px rgba(0,0,0,0.10), 0 2px 8px rgba(0,0,0,0.07)",
border: "1px solid #ddd8d0",
margin: "0 20px",
}}>
{/* Header */}
<div style={{ textAlign: "center", marginBottom: 28 }}>
<div style={{ fontFamily: "Krungthep, 'Archivo Black', sans-serif", fontSize: 30, color: "#1a1a18", letterSpacing: "-0.02em", lineHeight: 1 }}>
RAPPORT
</div>
<div style={{ fontSize: 9, color: "#b0aca4", letterSpacing: "0.2em", marginTop: 6, fontWeight: 500 }}>
{studioName.toUpperCase()}
</div>
<div style={{ width: 32, height: 1.5, background: "#ddd8d0", margin: "14px auto 0" }} />
</div>
{/* Info banner */}
<div style={{ padding: "14px 16px", background: "#fff8ed", borderRadius: 10, border: "1px solid #f0e4c4", marginBottom: 10 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: "#b07848", letterSpacing: "0.06em", marginBottom: 6 }}>
VERSION 0.5 NEUE INITIALISIERUNG
</div>
<div style={{ fontSize: 12, color: "#7a5a30", lineHeight: 1.65 }}>
Diese Version enthält umfangreiche Datenbankänderungen und ein neues Anmeldesystem. Die App muss neu eingerichtet werden.
</div>
</div>
{/* Recommendation */}
<div style={{ padding: "12px 16px", background: "#f5f0e8", borderRadius: 10, border: "1px solid #e0d8cc", marginBottom: 24, fontSize: 12, color: "#5a5040", lineHeight: 1.65 }}>
<strong style={{ display: "block", marginBottom: 4, fontSize: 11, letterSpacing: "0.06em", color: "#1a1a18" }}>EMPFEHLUNG</strong>
Sichern Sie zuerst die bestehenden Daten. Alte Datenbankstrukturen sind zwar importierbar, das Einspielen von Backups zum Testen wird jedoch ausdrücklich abgeraten umbenannte Felder können zu Fehlern führen.
</div>
{/* Step 1: Backup */}
<div style={{ marginBottom: 6 }}>
<div style={{ fontSize: 9, letterSpacing: "0.15em", color: "#8c8880", marginBottom: 8, fontWeight: 500 }}>
SCHRITT 1 DATEN SICHERN
</div>
<button
className={`mig-btn-backup${backed ? " done" : ""}`}
onClick={backed ? undefined : handleBackup}
>
{backed
? <><span></span> Backup heruntergeladen</>
: <><span style={{ fontSize: 16 }}></span> Datenbank herunterladen</>
}
</button>
{!backed && (
<div style={{ textAlign: "right", marginTop: 6 }}>
<button className="mig-skip" onClick={() => setBacked(true)}>
Kein Backup nötig überspringen
</button>
</div>
)}
</div>
{/* Step 2: Setup */}
<div style={{ marginTop: 20, opacity: backed ? 1 : 0.3, transition: "opacity 0.3s", pointerEvents: backed ? "auto" : "none" }}>
<div style={{ fontSize: 9, letterSpacing: "0.15em", color: "#8c8880", marginBottom: 8, fontWeight: 500 }}>
SCHRITT 2 NEU EINRICHTEN
</div>
<button className="mig-btn-primary" onClick={() => setGoSetup(true)}>
Weiter zur Einrichtung
</button>
</div>
</div>
</div>
);
}