Files
dogfather-universe/cloudflare-worker/migrations/0011_webdesign.sql
T
DogFatherGitandClaude Opus 5 4b3ec450d5 Webdesign-Bereich: Fundament, oeffentliche Seiten, Zugangsschutz, PayPal
Umsetzung des "Website Masterplan" (16 Seiten) unter /webdesign.

Zugangsschutz mit EIGENER Schranke (server/webdesign-gate.js) statt gate.js:
gate.js laesst seit dem oeffentlichen Start am 21.08.2026 jeden durch, weil
die Pruefung auf SITE_PUBLIC_LAUNCH_AT als allererste Zeile steht. Haette man
/webdesign dahintergehaengt, waere der ausdruecklich nicht-oeffentliche Bereich
inklusive Preisen und spaeteren Kundendaten ab der ersten Sekunde fuer jeden
lesbar gewesen. Eigenes Sitzungs-Cookie, bereich="webdesign" im Token, damit
ein gueltiges Universe-Cookie hier NICHT gilt. 37/37 Tests.

Sieben oeffentliche Seiten in fuenf Sprachen (de, de-CH mit echtem Dialekt, en,
fr, pt). Preise, Zeitrahmen, Paketnamen und die 30-%-Regel stehen an genau
EINER Stelle in wd-core.js -- der Masterplan verlangt "ueberall
widerspruchsfrei", und vier Kopien laufen bei der ersten Preisaenderung
auseinander.

Als App installierbar auf Handy und PC. Der Service Worker speichert bewusst
KEINE HTML-Seite zwischen: nach dem Abmelden wuerden sonst geschuetzte Seiten
weiter ausgeliefert, ohne dass der Server je gefragt wird. 18/18 Tests.

Handy-Abnahme ueber alle Seiten in fuenf Breiten (320-1440) und fuenf Sprachen:
40/40. Der Test fand 35 echte Fehler (Touch-Ziele unter 44px), behoben im
Designsystem statt einzeln pro Seite.

PayPal (Wunsch 22.08.2026 "sofort auf meinem paypal"): Orders API mit
intent=CAPTURE, also sofortiger Einzug statt blosser Reservierung. Gebuehr und
Nettobetrag getrennt gespeichert. Betraege durchgehend als Ganzzahl in Cent.
Gefaelschte Webhooks werden abgewiesen. Fail closed solange Zugangsdaten
fehlen. 28/28 Tests gegen einen nachgebauten PayPal-Server.

Datenbank: 15 Tabellen mit Praefix wd_, fachlich vollstaendig vom Universe
getrennt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 18:06:52 +02:00

322 lines
15 KiB
SQL

-- =====================================================================
-- 0011_webdesign.sql — Datenbank für den Bereich /webdesign
-- (Masterplan S.10 Lead-System, S.12 Kundenportal, S.13 Verwaltung)
--
-- Warum alles mit dem Präfix "wd_":
-- Diese Datenbank trägt bereits das Universe (Team, Bewerbungen,
-- Supporter, Abstimmungen, Testimonials). Der Webdesign-Bereich ist ein
-- eigenes Geschäft mit eigenen Kunden — ohne Präfix wäre in einem Jahr
-- nicht mehr erkennbar, welche "users"-Tabelle wozu gehört, und ein
-- versehentlicher JOIN über zwei fachlich getrennte Welten ist der
-- Anfang jedes Datenlecks.
--
-- Grundsätze:
-- - Löschen wird vermieden. Kritische Datensätze werden archiviert
-- (Masterplan S.13: "Kritische Löschungen werden zunächst archiviert").
-- - Jede Tabelle mit Kundenbezug hat eine kunde_id, über die serverseitig
-- gefiltert wird. Ohne diese Spalte gibt es keine saubere Trennung.
-- - Zeitstempel immer als ISO-8601-Text in UTC (wie im restlichen
-- Projekt), damit Sortierung und Vergleich rein textuell funktionieren.
-- =====================================================================
-- ---------------------------------------------------------------------
-- ANFRAGEN (Masterplan S.10)
-- Entsteht durch das öffentliche Formular. Wird NICHT gelöscht, sondern
-- archiviert — eine Anfrage ist auch dann noch belegrelevant, wenn daraus
-- nichts wurde.
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS wd_anfragen (
id TEXT PRIMARY KEY,
-- Kurze, sprechende Nummer für die Kommunikation ("A-2608-0001").
nummer TEXT NOT NULL UNIQUE,
-- Kontakt
name TEXT NOT NULL,
email TEXT NOT NULL,
telefon TEXT,
kontaktweg TEXT NOT NULL DEFAULT 'email', -- email | telefon
-- Projekt
paket TEXT NOT NULL, -- onepager | website | shop | verwaltung | betreuung | unklar
ziel TEXT, -- Was soll die Seite erreichen
zielgruppe TEXT,
wirkung TEXT,
seiten TEXT, -- gewünschte Seiten/Bereiche
funktionen TEXT, -- JSON-Array angekreuzter Funktionen
-- Kategoriespezifische Folgefragen als JSON, damit neue Fragen keine
-- Schemaänderung brauchen (Masterplan S.10 "Intelligente Felder").
details TEXT,
-- Vorhandene Inhalte
hat_texte TEXT, -- ja | teilweise | nein
hat_bilder TEXT,
hat_logo TEXT,
hat_domain TEXT,
-- Rahmen
budget TEXT,
termin TEXT,
bemerkung TEXT,
-- Verwaltung
status TEXT NOT NULL DEFAULT 'neu', -- neu | pruefung | rueckfrage | angebot | angenommen | abgeschlossen | abgelehnt
archiviert INTEGER NOT NULL DEFAULT 0,
kunde_id TEXT, -- gesetzt, sobald daraus ein Kunde wurde
sprache TEXT NOT NULL DEFAULT 'de', -- in welcher Sprache angefragt wurde -> so wird geantwortet
-- Nachvollziehbarkeit
erstellt_am TEXT NOT NULL,
aktualisiert_am TEXT,
ip_hash TEXT, -- nur Hash, nie die IP selbst (Datensparsamkeit)
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_wd_anfragen_status ON wd_anfragen(status, archiviert);
CREATE INDEX IF NOT EXISTS idx_wd_anfragen_erstellt ON wd_anfragen(erstellt_am DESC);
CREATE INDEX IF NOT EXISTS idx_wd_anfragen_email ON wd_anfragen(email);
-- Interne Notizen und Antworten an der Anfrage (Masterplan S.10).
CREATE TABLE IF NOT EXISTS wd_anfrage_notizen (
id TEXT PRIMARY KEY,
anfrage_id TEXT NOT NULL,
autor TEXT NOT NULL, -- dogfather | vanvan
text TEXT NOT NULL,
intern INTEGER NOT NULL DEFAULT 1, -- 1 = nur intern sichtbar
erstellt_am TEXT NOT NULL,
FOREIGN KEY (anfrage_id) REFERENCES wd_anfragen(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_wd_anfrage_notizen ON wd_anfrage_notizen(anfrage_id, erstellt_am);
-- ---------------------------------------------------------------------
-- KUNDEN (Masterplan S.12)
-- Ein Kunde entsteht erst, wenn ein Projekt gekauft oder eine Betreuung
-- vereinbart wurde. Registrierung allein reicht ausdrücklich nicht:
-- "Projektkauf oder laufende Betreuung wird vor Freischaltung geprüft".
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS wd_kunden (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
firma TEXT,
telefon TEXT,
sprache TEXT NOT NULL DEFAULT 'de',
-- Anmeldung
passwort_hash TEXT, -- NULL = noch kein Passwort gesetzt
email_bestaetigt INTEGER NOT NULL DEFAULT 0,
-- Freischaltung ist bewusst getrennt von der E-Mail-Bestätigung:
-- bestätigte E-Mail heißt nur "die Adresse gehört dir", nicht
-- "du darfst hier hinein".
freigeschaltet INTEGER NOT NULL DEFAULT 0,
gesperrt INTEGER NOT NULL DEFAULT 0,
gesperrt_grund TEXT,
-- Betreuung
betreuung_aktiv INTEGER NOT NULL DEFAULT 0,
betreuung_paket TEXT,
betreuung_start TEXT,
betreuung_ende TEXT,
benachrichtigungen INTEGER NOT NULL DEFAULT 1,
erstellt_am TEXT NOT NULL,
aktualisiert_am TEXT,
letzter_login TEXT,
archiviert INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_wd_kunden_email ON wd_kunden(email);
-- Einmal-Kennungen für E-Mail-Bestätigung, Einladung und Passwort-Reset.
-- Getrennt von der Kundentabelle, damit ein Kunde mehrere offene Vorgänge
-- haben kann und abgelaufene Kennungen einfach wegzuräumen sind.
CREATE TABLE IF NOT EXISTS wd_kunden_token (
token_hash TEXT PRIMARY KEY, -- nur der Hash, nie das Token selbst
kunde_id TEXT NOT NULL,
zweck TEXT NOT NULL, -- bestaetigung | einladung | passwort
laeuft_ab TEXT NOT NULL,
benutzt_am TEXT,
erstellt_am TEXT NOT NULL,
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_wd_kunden_token_kunde ON wd_kunden_token(kunde_id, zweck);
-- Aktive Anmeldungen. Serverseitig gespeichert, damit sich Sitzungen im
-- Verwaltungsbereich gezielt beenden lassen (Masterplan S.13:
-- "Aktive Sitzungen abmelden").
CREATE TABLE IF NOT EXISTS wd_kunden_sitzungen (
token_hash TEXT PRIMARY KEY,
kunde_id TEXT NOT NULL,
erstellt_am TEXT NOT NULL,
laeuft_ab TEXT NOT NULL,
letzte_aktion TEXT,
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_wd_sitzungen_kunde ON wd_kunden_sitzungen(kunde_id);
-- Fehlversuche bei der Kundenanmeldung (Sperre nach zu vielen Versuchen).
CREATE TABLE IF NOT EXISTS wd_login_versuche (
schluessel TEXT PRIMARY KEY, -- E-Mail in Kleinschreibung
anzahl INTEGER NOT NULL DEFAULT 0,
gesperrt_bis TEXT,
zuletzt TEXT NOT NULL
);
-- ---------------------------------------------------------------------
-- PROJEKTE (Masterplan S.12/S.13)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS wd_projekte (
id TEXT PRIMARY KEY,
nummer TEXT NOT NULL UNIQUE, -- "P-2608-0001"
kunde_id TEXT NOT NULL,
anfrage_id TEXT,
titel TEXT NOT NULL,
paket TEXT NOT NULL,
beschreibung TEXT,
status TEXT NOT NULL DEFAULT 'briefing', -- briefing | angebot | design | entwicklung | tests | abnahme | uebergeben | pausiert | abgebrochen
naechster_schritt TEXT,
wartet_auf TEXT, -- kunde | dogfather -> wer gerade am Zug ist
-- Geld. In CENT als Ganzzahl, nie als Kommazahl: 0.1 + 0.2 ergibt in
-- Gleitkomma nicht 0.3, und bei Rechnungsbeträgen ist das inakzeptabel.
preis_cent INTEGER,
anzahlung_cent INTEGER,
anzahlung_bezahlt INTEGER NOT NULL DEFAULT 0,
rest_bezahlt INTEGER NOT NULL DEFAULT 0,
waehrung TEXT NOT NULL DEFAULT 'EUR',
start_am TEXT,
richttermin TEXT,
uebergabe_am TEXT,
-- Portfoliofreigabe ist bewusst ein EIGENES Feld, getrennt von der
-- Veröffentlichung der Kundenseite (Masterplan S.13: "Portfoliofreigabe
-- separat dokumentieren"). Beides darf nie aneinander hängen.
portfolio_freigabe INTEGER NOT NULL DEFAULT 0,
portfolio_freigabe_am TEXT,
archiviert INTEGER NOT NULL DEFAULT 0,
erstellt_am TEXT NOT NULL,
aktualisiert_am TEXT,
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE,
FOREIGN KEY (anfrage_id) REFERENCES wd_anfragen(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_wd_projekte_kunde ON wd_projekte(kunde_id, archiviert);
CREATE INDEX IF NOT EXISTS idx_wd_projekte_status ON wd_projekte(status);
-- ---------------------------------------------------------------------
-- DATEIEN (Masterplan S.12: Versionen und Zeitstempel)
-- Die Datei selbst liegt im Dateisystem neben der Datenbank, hier steht
-- nur der Verweis. Der gespeicherte Name ist eine zufällige Kennung —
-- ein hochgeladener Originalname darf nie in einen Pfad wandern.
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS wd_dateien (
id TEXT PRIMARY KEY,
projekt_id TEXT,
kunde_id TEXT NOT NULL,
anfrage_id TEXT,
anzeigename TEXT NOT NULL, -- was der Mensch sieht
speichername TEXT NOT NULL, -- zufällig, im Dateisystem
typ TEXT NOT NULL, -- MIME-Typ
groesse INTEGER NOT NULL,
kategorie TEXT NOT NULL DEFAULT 'sonstiges', -- logo | bild | text | angebot | freigabe | sonstiges
version INTEGER NOT NULL DEFAULT 1,
ersetzt_id TEXT, -- Vorgängerversion
hochgeladen_von TEXT NOT NULL, -- kunde | dogfather | vanvan
-- Kunden dürfen nur sehen, was für sie bestimmt ist. Interne Arbeitsdateien
-- bleiben damit unsichtbar, ohne sie in einer zweiten Tabelle zu führen.
fuer_kunde INTEGER NOT NULL DEFAULT 1,
erstellt_am TEXT NOT NULL,
geloescht INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE,
FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_wd_dateien_projekt ON wd_dateien(projekt_id, geloescht);
CREATE INDEX IF NOT EXISTS idx_wd_dateien_kunde ON wd_dateien(kunde_id, geloescht);
-- ---------------------------------------------------------------------
-- NACHRICHTEN am Projekt (Masterplan S.12: "projektbezogene Kommunikation
-- bleibt vollständig nachvollziehbar")
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS wd_nachrichten (
id TEXT PRIMARY KEY,
projekt_id TEXT NOT NULL,
kunde_id TEXT NOT NULL,
autor TEXT NOT NULL, -- kunde | dogfather | vanvan
text TEXT NOT NULL,
intern INTEGER NOT NULL DEFAULT 0, -- 1 = Kunde sieht es NICHT
gelesen_kunde INTEGER NOT NULL DEFAULT 0,
gelesen_admin INTEGER NOT NULL DEFAULT 0,
erstellt_am TEXT NOT NULL,
FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE CASCADE,
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_wd_nachrichten_projekt ON wd_nachrichten(projekt_id, erstellt_am);
-- ---------------------------------------------------------------------
-- ÄNDERUNGSWÜNSCHE / ZUSATZANGEBOTE (Masterplan S.12)
-- Der Kunde meldet eine Idee an, Dogfather beziffert Aufwand und Preis,
-- der Kunde nimmt an oder lehnt ab. Ein eigener Datensatz statt einer
-- Nachricht, damit der Zustand eindeutig ist und nichts im Verlauf untergeht.
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS wd_aenderungen (
id TEXT PRIMARY KEY,
projekt_id TEXT NOT NULL,
kunde_id TEXT NOT NULL,
titel TEXT NOT NULL,
beschreibung TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'eingereicht', -- eingereicht | geprueft | angeboten | angenommen | abgelehnt | umgesetzt
aufwand_text TEXT,
zusatzpreis_cent INTEGER,
zusatzdauer_text TEXT,
antwort TEXT,
entschieden_am TEXT,
erstellt_am TEXT NOT NULL,
aktualisiert_am TEXT,
FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE CASCADE,
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_wd_aenderungen_projekt ON wd_aenderungen(projekt_id, status);
-- ---------------------------------------------------------------------
-- INHALTE, DIE IN DER VERWALTUNG PFLEGBAR SIND (Masterplan S.13)
-- Ein einfacher Schlüssel-Wert-Speicher mit mehrsprachigem JSON-Wert.
-- Bewusst kein eigenes Feld je Text: sonst braucht jeder neue Satz auf der
-- Seite eine Datenbankänderung.
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS wd_inhalte (
schluessel TEXT PRIMARY KEY,
wert_json TEXT NOT NULL, -- {"de":"...","de-CH":"...","en":"...","fr":"...","pt":"..."}
bereich TEXT, -- zur Gruppierung in der Oberfläche
aktualisiert_am TEXT NOT NULL,
aktualisiert_von TEXT
);
-- ---------------------------------------------------------------------
-- ÄNDERUNGSVERLAUF (Masterplan S.13: "Änderungsverlauf und verantwortliche
-- Person protokollieren")
-- Absichtlich eine eigene Tabelle statt Spalten in jeder Tabelle: so ist
-- der Verlauf an einer Stelle lesbar und überlebt auch das Archivieren
-- des Datensatzes, auf den er sich bezieht.
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS wd_verlauf (
id TEXT PRIMARY KEY,
bereich TEXT NOT NULL, -- anfrage | kunde | projekt | inhalt | zugang | datei
objekt_id TEXT,
aktion TEXT NOT NULL, -- angelegt | geaendert | archiviert | freigegeben | ...
wer TEXT NOT NULL, -- dogfather | vanvan | kunde:<id> | system
details TEXT, -- JSON: was genau sich geändert hat
erstellt_am TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_wd_verlauf_objekt ON wd_verlauf(bereich, objekt_id, erstellt_am DESC);
CREATE INDEX IF NOT EXISTS idx_wd_verlauf_zeit ON wd_verlauf(erstellt_am DESC);