Rechtliches: Erstbefuellung aus der Git-Geschichte (22 Versionen, 4 Anbieter)
Website-Seiten werden per React zu HTML gerendert, Portal-HTML kommt direkt aus Git. Eine gemeinsame Datenschutzerklaerung statt zwei Karten, weil webklar.com/datenschutz nur auf das Portal weiterleitet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c7c1e338a3
commit
c8c1d84794
3 files changed
+389
-6
No files matched your search
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"documentKey": "agb",
|
||||
"version": "v1",
|
||||
"validFrom": "2026-02-02",
|
||||
"status": "superseded",
|
||||
"changeNote": "Erste Fassung der AGB auf webklar.com",
|
||||
"source": "webklar.com@217bbdc:src/pages/AGB.tsx"
|
||||
},
|
||||
{
|
||||
"documentKey": "agb",
|
||||
"version": "v2",
|
||||
"validFrom": "2026-09-27",
|
||||
"status": "superseded",
|
||||
"changeNote": "Modul C Hosting ergänzt (Hosting-Abo, Wartungs-Abo mit beliebig vielen Postfächern)",
|
||||
"source": "webklar.com@dc27e50:src/pages/AGB.tsx"
|
||||
},
|
||||
{
|
||||
"documentKey": "agb",
|
||||
"version": "v3",
|
||||
"validFrom": "2026-09-27",
|
||||
"status": "superseded",
|
||||
"changeNote": "An die Vertragsbedingungen angeglichen (Wechsel, Zusatzspeicher, Sicherheit), Verweise auf die Abo-Bedingungen",
|
||||
"source": "webklar.com@2c1a4c1:src/pages/AGB.tsx"
|
||||
},
|
||||
{
|
||||
"documentKey": "agb",
|
||||
"version": "v4",
|
||||
"validFrom": "2026-09-28",
|
||||
"status": "superseded",
|
||||
"changeNote": "Haftungssatz in Modul C entfällt; Kündigung nach Verlängerung für Verbraucher",
|
||||
"source": "webklar.com@2bffca4:src/pages/AGB.tsx"
|
||||
},
|
||||
{
|
||||
"documentKey": "agb",
|
||||
"version": "v5",
|
||||
"validFrom": "2026-09-28",
|
||||
"status": "current",
|
||||
"changeNote": "Verbraucher-Kündigung als Ausnahme formuliert",
|
||||
"source": "webklar.com@044f788:src/pages/AGB.tsx"
|
||||
},
|
||||
{
|
||||
"documentKey": "agb",
|
||||
"version": "v6",
|
||||
"validFrom": "2026-09-28",
|
||||
"status": "draft",
|
||||
"changeNote": "§ 11 Haftung neu nach dem Muster der Abo-Bedingungen (noch nicht live)",
|
||||
"source": "webklar.com@f40e889:src/pages/AGB.tsx"
|
||||
},
|
||||
{
|
||||
"documentKey": "abo-bedingungen",
|
||||
"version": "v1",
|
||||
"validFrom": "2026-09-25",
|
||||
"status": "superseded",
|
||||
"changeNote": "Erste Fassung: Wartungs-Abo 50 €/Monat über Stripe mit Stundenkonto",
|
||||
"source": "kundenbereich@65429aa:public/abo-bedingungen.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "abo-bedingungen",
|
||||
"version": "v2",
|
||||
"validFrom": "2026-09-25",
|
||||
"status": "superseded",
|
||||
"changeNote": "Vertragsunterlagen Version 2 nach Rechtsrunde, Postfächer und Domains bei Hetzner",
|
||||
"source": "kundenbereich@4ab154b:public/abo-bedingungen.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "abo-bedingungen",
|
||||
"version": "v3",
|
||||
"validFrom": "2026-09-25",
|
||||
"status": "superseded",
|
||||
"changeNote": "Version 3: Mindestlaufzeit 12 Monate; Kontakt support@webklar.com",
|
||||
"source": "kundenbereich@dbabbf1:public/abo-bedingungen.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "abo-bedingungen",
|
||||
"version": "v4",
|
||||
"validFrom": "2026-09-30",
|
||||
"status": "current",
|
||||
"changeNote": "Version 4: Hosting-Abo und Wartungs-Abo, Postfächer, Zusatzspeicher, Wechsel, AVV; § 13 Abs. 3 mit Ausnahme für Absatz 1; Hosting-Preis 20 €",
|
||||
"source": "kundenbereich@e4efdac:public/abo-bedingungen.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "avv",
|
||||
"version": "v2",
|
||||
"validFrom": "2026-09-25",
|
||||
"status": "superseded",
|
||||
"changeNote": "Erste AVV als Anlage der Vertragsbedingungen Version 2 (in Version 1 nur „auf Anfrage“)",
|
||||
"source": "kundenbereich@4ab154b:public/abo-bedingungen.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "avv",
|
||||
"version": "v3",
|
||||
"validFrom": "2026-09-25",
|
||||
"status": "superseded",
|
||||
"changeNote": "AVV als Anlage der Vertragsbedingungen Version 3",
|
||||
"source": "kundenbereich@dbabbf1:public/abo-bedingungen.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "avv",
|
||||
"version": "v4",
|
||||
"validFrom": "2026-09-30",
|
||||
"status": "current",
|
||||
"changeNote": "AVV als Anlage der Vertragsbedingungen Version 4 (Anlagen 1 und 3 überarbeitet)",
|
||||
"source": "kundenbereich@e4efdac:public/abo-bedingungen.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "datenschutz",
|
||||
"version": "v1",
|
||||
"validFrom": "2026-09-25",
|
||||
"status": "superseded",
|
||||
"changeNote": "Stand 25.09.2026: Abo, Übernahmegutschrift",
|
||||
"source": "kundenbereich@48b13f4:public/datenschutz.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "datenschutz",
|
||||
"version": "v2",
|
||||
"validFrom": "2026-09-26",
|
||||
"status": "superseded",
|
||||
"changeNote": "Stand 26.09.2026: Chat mit Bildern, Dateien und Sprachnachrichten",
|
||||
"source": "kundenbereich@35d98da:public/datenschutz.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "datenschutz",
|
||||
"version": "v3",
|
||||
"validFrom": "2026-09-27",
|
||||
"status": "superseded",
|
||||
"changeNote": "Stand 27.09.2026: E-Mail bei Hetzner statt Porkbun, UptimeRobot, Bestellungen im Portal",
|
||||
"source": "kundenbereich@7e86928:public/datenschutz.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "datenschutz",
|
||||
"version": "v4",
|
||||
"validFrom": "2026-09-30",
|
||||
"status": "current",
|
||||
"changeNote": "Stand 30.09.2026: Livegang mit Vertragsbedingungen Version 4",
|
||||
"source": "kundenbereich@78b4d7b:public/datenschutz.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "impressum",
|
||||
"version": "v1",
|
||||
"validFrom": "2026-02-02",
|
||||
"status": "superseded",
|
||||
"changeNote": "Erste Fassung",
|
||||
"source": "webklar.com@6af24d2:src/pages/Impressum.tsx"
|
||||
},
|
||||
{
|
||||
"documentKey": "impressum",
|
||||
"version": "v2",
|
||||
"validFrom": "2026-09-25",
|
||||
"status": "current",
|
||||
"changeNote": "Neu nach DDG mit Gesellschaftern; Kontakt support@webklar.com",
|
||||
"source": "webklar.com@f74ddfb:src/pages/Impressum.tsx"
|
||||
},
|
||||
{
|
||||
"documentKey": "widerruf",
|
||||
"version": "v1",
|
||||
"validFrom": "2026-09-25",
|
||||
"status": "superseded",
|
||||
"changeNote": "Widerrufsformular im Kundenportal",
|
||||
"source": "kundenbereich@65429aa:public/widerrufen.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "widerruf",
|
||||
"version": "v2",
|
||||
"validFrom": "2026-09-25",
|
||||
"status": "superseded",
|
||||
"changeNote": "Überarbeitet nach Rechtsrunde",
|
||||
"source": "kundenbereich@4ab154b:public/widerrufen.html"
|
||||
},
|
||||
{
|
||||
"documentKey": "widerruf",
|
||||
"version": "v3",
|
||||
"validFrom": "2026-09-29",
|
||||
"status": "current",
|
||||
"changeNote": "Vertrag bzw. Bestellung auswählbar (§ 312k, § 356a BGB)",
|
||||
"source": "kundenbereich@7ffe95f:public/widerrufen.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* WOMS Rechtliches – Erstbefuellung (idempotent ueber das Feld `source`).
|
||||
* Liest scripts/legal-seed.json, erzeugt je Version eine Archivkopie als HTML
|
||||
* (Portal-HTML direkt aus Git, Website-Seiten per React zu HTML gerendert),
|
||||
* laedt sie in den Bucket legal-files und legt die Versionen an. Dazu die
|
||||
* Anbieter-Vertraege ohne Fristen und Preise.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/seed-legal.mjs --dry-run [--out DIR] (nur rendern, nichts hochladen)
|
||||
* APPWRITE_API_KEY=... node scripts/seed-legal.mjs
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const APPS = "/home/webklar/apps";
|
||||
const REPOS = {
|
||||
"webklar.com": process.env.WEBSITE_REPO || `${APPS}/webklar.com/build`,
|
||||
kundenbereich: process.env.PORTAL_REPO || `${APPS}/kundenbereich.webklar.com/build`,
|
||||
};
|
||||
const NODE_MODULES = process.env.WOMS_NODE_MODULES || `${APPS}/ticket.webklar.com/build/node_modules`;
|
||||
|
||||
const dryRun = process.argv.includes("--dry-run");
|
||||
const outIdx = process.argv.indexOf("--out");
|
||||
const outDir = outIdx > 0 ? process.argv[outIdx + 1] : null;
|
||||
const endpoint = process.env.APPWRITE_ENDPOINT || "https://appwrite.webklar.com/v1";
|
||||
const projectId = process.env.APPWRITE_PROJECT_ID || "6a1058610003c5a13a05";
|
||||
const databaseId = process.env.APPWRITE_DATABASE_ID || "woms-database";
|
||||
const apiKey = process.env.APPWRITE_API_KEY;
|
||||
|
||||
export const PROVIDERS = [
|
||||
{ provider: "Hetzner", purpose: "Server (Hosting aller Websites und Systeme)" },
|
||||
{ provider: "Hetzner", purpose: "E-Mail-Postfächer" },
|
||||
{ provider: "Porkbun", purpose: "Domains" },
|
||||
{ provider: "Stripe", purpose: "Zahlungen der Abos" },
|
||||
];
|
||||
|
||||
const require = createRequire(path.join(NODE_MODULES, "noop.js"));
|
||||
|
||||
function gitShow(repo, hash, file) {
|
||||
return execFileSync("git", ["-C", REPOS[repo], "show", `${hash}:${file}`], { maxBuffer: 20 * 1024 * 1024 });
|
||||
}
|
||||
|
||||
// Alles ausser React wird zu einem Platzhalter: Komponenten geben nur ihre Kinder aus,
|
||||
// Link wird zu <a>, Icons verschwinden. Die Exportnamen kommen aus den Imports der Seite.
|
||||
function stubFor(source, specifier) {
|
||||
const names = new Set();
|
||||
let hasDefault = false;
|
||||
const re = /import\s+(?:(\w+)\s*,?\s*)?(?:\{([^}]*)\})?\s*from\s*["']([^"']+)["']/g;
|
||||
for (const m of source.matchAll(re)) {
|
||||
if (m[3] !== specifier) continue;
|
||||
if (m[1]) hasDefault = true;
|
||||
for (const n of (m[2] || "").split(",")) {
|
||||
const name = n.trim().split(/\s+as\s+/)[0].replace(/^type\s+/, "");
|
||||
if (name) names.add(name);
|
||||
}
|
||||
}
|
||||
const lines = [
|
||||
"import React from 'react';",
|
||||
"const Pass = (p) => (p && p.children !== undefined ? React.createElement(React.Fragment, null, p.children) : null);",
|
||||
];
|
||||
for (const n of names) {
|
||||
lines.push(n === "Link" || n === "NavLink"
|
||||
? `export const ${n} = (p) => React.createElement('a', { href: p.to || p.href }, p.children);`
|
||||
: /^[A-Z]/.test(n) ? `export const ${n} = Pass;` : `export const ${n} = () => undefined;`);
|
||||
}
|
||||
if (hasDefault) lines.push("export default Pass;");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function renderTsx(source) {
|
||||
const esbuild = require("esbuild");
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "legal-"));
|
||||
const entry = path.join(tmp, "page.tsx");
|
||||
fs.writeFileSync(entry, source);
|
||||
const result = await esbuild.build({
|
||||
entryPoints: [entry],
|
||||
bundle: true,
|
||||
write: false,
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
jsx: "automatic",
|
||||
nodePaths: [NODE_MODULES],
|
||||
external: ["react", "react-dom", "react/jsx-runtime"],
|
||||
logLevel: "silent",
|
||||
plugins: [{
|
||||
name: "stub",
|
||||
setup(b) {
|
||||
b.onResolve({ filter: /.*/ }, (a) =>
|
||||
a.kind === "entry-point" || /^react(\/jsx-runtime)?$/.test(a.path) ? undefined : { path: a.path, namespace: "stub" });
|
||||
b.onLoad({ filter: /.*/, namespace: "stub" }, (a) => ({ contents: stubFor(source, a.path), loader: "js", resolveDir: NODE_MODULES }));
|
||||
},
|
||||
}],
|
||||
});
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
const mod = { exports: {} };
|
||||
new Function("module", "exports", "require", result.outputFiles[0].text)(mod, mod.exports, require);
|
||||
const Page = mod.exports.default;
|
||||
const React = require("react");
|
||||
const { renderToStaticMarkup } = require("react-dom/server");
|
||||
const html = renderToStaticMarkup(React.createElement(Page));
|
||||
const main = /<main[\s\S]*<\/main>/.exec(html);
|
||||
return main ? main[0] : html;
|
||||
}
|
||||
|
||||
function wrap(title, entry, body) {
|
||||
return `<!doctype html>
|
||||
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${title} ${entry.version} – Archivkopie</title>
|
||||
<style>body{font:16px/1.6 system-ui,sans-serif;max-width:820px;margin:0 auto;padding:24px;color:#111}
|
||||
.archiv{background:#f3f4f6;border:1px solid #d1d5db;padding:8px 12px;border-radius:6px;font-size:13px;margin-bottom:24px}
|
||||
svg{display:none}h1{font-size:28px}</style></head>
|
||||
<body><div class="archiv">Archivkopie aus WOMS · ${title} ${entry.version} · gültig ab ${entry.validFrom} · Quelle ${entry.source}</div>
|
||||
${body}
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
export async function buildFile(entry, title) {
|
||||
const [repo, rest] = entry.source.split("@");
|
||||
const [hash, file] = rest.split(":");
|
||||
const raw = gitShow(repo, hash, file);
|
||||
const name = `${entry.documentKey}-${entry.version}-${hash}.html`;
|
||||
if (file.endsWith(".html")) return { name, content: raw };
|
||||
const body = await renderTsx(raw.toString("utf8"));
|
||||
return { name, content: Buffer.from(wrap(title, entry, body), "utf8") };
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const { versions } = JSON.parse(fs.readFileSync(path.join(HERE, "legal-seed.json"), "utf8"));
|
||||
const { DOCUMENTS } = await import("./setup-legal.mjs");
|
||||
const titles = Object.fromEntries(DOCUMENTS.map((d) => [d.key, d.title]));
|
||||
|
||||
if (dryRun) {
|
||||
if (outDir) fs.mkdirSync(outDir, { recursive: true });
|
||||
for (const v of versions) {
|
||||
const f = await buildFile(v, titles[v.documentKey]);
|
||||
if (outDir) fs.writeFileSync(path.join(outDir, f.name), f.content);
|
||||
console.log(`${v.documentKey} ${v.version} ${v.status} ab ${v.validFrom}: ${f.name} (${f.content.length} Bytes)`);
|
||||
}
|
||||
for (const p of PROVIDERS) console.log(`Anbieter: ${p.provider} – ${p.purpose}`);
|
||||
return;
|
||||
}
|
||||
if (!apiKey) {
|
||||
console.error("APPWRITE_API_KEY required (oder --dry-run)");
|
||||
process.exit(1);
|
||||
}
|
||||
const { Client, Databases, Storage, ID, Query } = await import("node-appwrite");
|
||||
const { InputFile } = await import("node-appwrite/file");
|
||||
const client = new Client().setEndpoint(endpoint).setProject(projectId).setKey(apiKey);
|
||||
const db = new Databases(client);
|
||||
const storage = new Storage(client);
|
||||
|
||||
let created = 0;
|
||||
for (const v of versions) {
|
||||
const found = await db.listDocuments(databaseId, "legalVersions", [Query.equal("source", v.source), Query.equal("documentKey", v.documentKey), Query.limit(1)]);
|
||||
if (found.total > 0) {
|
||||
console.log("vorhanden:", v.documentKey, v.version);
|
||||
continue;
|
||||
}
|
||||
const f = await buildFile(v, titles[v.documentKey]);
|
||||
const file = await storage.createFile("legal-files", ID.unique(), InputFile.fromBuffer(f.content, f.name));
|
||||
const doc = { ...v, validTo: null, fileIds: [file.$id] };
|
||||
await db.createDocument(databaseId, "legalVersions", ID.unique(), doc);
|
||||
created++;
|
||||
console.log("angelegt:", v.documentKey, v.version);
|
||||
}
|
||||
|
||||
// validTo der abgeloesten Versionen: Tag vor der naechsten nicht-Entwurf-Version, nie vor dem eigenen Beginn
|
||||
const all = (await db.listDocuments(databaseId, "legalVersions", [Query.limit(5000)])).documents;
|
||||
for (const v of all.filter((x) => x.status === "superseded" && !x.validTo)) {
|
||||
const next = all
|
||||
.filter((x) => x.documentKey === v.documentKey && x.status !== "draft" && x.$id !== v.$id &&
|
||||
(x.validFrom > v.validFrom || (x.validFrom === v.validFrom && x.version > v.version)))
|
||||
.sort((a, b) => (a.validFrom + a.version).localeCompare(b.validFrom + b.version))[0];
|
||||
if (!next) continue;
|
||||
const d = new Date(`${next.validFrom}T00:00:00Z`);
|
||||
d.setUTCDate(d.getUTCDate() - 1);
|
||||
const before = d.toISOString().slice(0, 10);
|
||||
await db.updateDocument(databaseId, "legalVersions", v.$id, { validTo: before < v.validFrom ? v.validFrom : before });
|
||||
}
|
||||
|
||||
const existing = (await db.listDocuments(databaseId, "providerContracts", [Query.limit(5000)])).documents;
|
||||
for (const p of PROVIDERS) {
|
||||
if (existing.some((c) => c.provider === p.provider && c.purpose === p.purpose)) continue;
|
||||
await db.createDocument(databaseId, "providerContracts", ID.unique(), { ...p, fileIds: [] });
|
||||
console.log("Anbieter angelegt:", p.provider, p.purpose);
|
||||
}
|
||||
console.log(`Fertig: ${created} Versionen angelegt.`);
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
console.error(e.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
+10
-6
@@ -10,6 +10,8 @@
|
||||
* APPWRITE_API_KEY=... node scripts/setup-legal.mjs
|
||||
*/
|
||||
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const dryRun = process.argv.includes("--dry-run");
|
||||
const endpoint = process.env.APPWRITE_ENDPOINT || "https://appwrite.webklar.com/v1";
|
||||
const projectId = process.env.APPWRITE_PROJECT_ID || "6a1058610003c5a13a05";
|
||||
@@ -83,8 +85,7 @@ export const DOCUMENTS = [
|
||||
{ key: "agb", title: "AGB", liveUrl: "https://webklar.com/agb" },
|
||||
{ key: "abo-bedingungen", title: "Abo-Bedingungen", liveUrl: "https://project.webklar.com/abo-bedingungen.html" },
|
||||
{ key: "avv", title: "AVV (Auftragsverarbeitung)", liveUrl: "https://project.webklar.com/abo-bedingungen.html" },
|
||||
{ key: "datenschutz-website", title: "Datenschutz Website", liveUrl: "https://webklar.com/datenschutz" },
|
||||
{ key: "datenschutz-portal", title: "Datenschutz Kundenportal", liveUrl: "https://project.webklar.com/datenschutz.html" },
|
||||
{ key: "datenschutz", title: "Datenschutzerklärung (Website und Portal)", liveUrl: "https://project.webklar.com/datenschutz.html" },
|
||||
{ key: "impressum", title: "Impressum", liveUrl: "https://webklar.com/impressum" },
|
||||
{ key: "widerruf", title: "Widerrufsbelehrung und -formular", liveUrl: "https://project.webklar.com/widerrufen.html" },
|
||||
].map((d, i) => ({ ...d, sort: i + 1 }));
|
||||
@@ -173,7 +174,10 @@ async function run() {
|
||||
console.log("Fertig.");
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
console.error(e.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
// Nur direkt aufgerufen ausfuehren; seed-legal.mjs importiert DOCUMENTS.
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
run().catch((e) => {
|
||||
console.error(e.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user