Files
tickte-system/scripts/setup-legal.mjs
T
ClaudeandClaude Opus 5.5 3d67aa88cf Öffnungszeiten: Team-Panel, Ersatz-HTML und Rechtliches-Karte
Panel unter der Zeile Öffnungszeiten live (Header-Selektor, Kontakt-/Hinweis-Leiste aus, Speichern mit teamVersion,
Ersatz-HTML kopieren je Plan), Meldung zum Ersatz-Ticket nach dem Abschalten, Hindernis an aktionen, Statustext der Zeiten,
Team-API (GET ersatz, PATCH team), Karte Leistungsbeschreibung Öffnungszeiten live in setup-legal (Schlüssel höchstens 36 Zeichen).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:12:04 +00:00

211 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* WOMS Rechtliches – Appwrite-Setup (idempotent).
* Legt die Sammlungen legalDocuments, legalVersions, providerContracts, den Bucket
* legal-files und die festen Rechtstext-Karten an. Versionen und Dateien bekommen
* bewusst keine Loeschrechte: das Archiv soll vollstaendig bleiben.
*
* Usage:
* node scripts/setup-legal.mjs --dry-run
* APPWRITE_API_KEY=... node scripts/setup-legal.mjs
*/
import { pathToFileURL } from "node:url";
import { staffPermissions } from "../src/lib/staffAccess.js";
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";
const databaseId = process.env.APPWRITE_DATABASE_ID || "woms-database";
const apiKey = process.env.APPWRITE_API_KEY;
// Nur Mitarbeiter (Labels), nie "users": das waeren auch alle Kunden-Konten des Portals
const READ_WRITE = staffPermissions("read", "create", "update");
const CRUD = staffPermissions("read", "create", "update", "delete");
const date = (key) => ({ key, type: "string", size: 10 });
export const COLLECTIONS = [
{
id: "legalDocuments",
name: "Rechtstexte",
perms: READ_WRITE,
attrs: [
{ key: "key", type: "string", size: 64, required: true },
{ key: "title", type: "string", size: 200, required: true },
{ key: "liveUrl", type: "string", size: 500 },
{ key: "sort", type: "integer", min: 0, max: 1000 },
],
indexes: [{ key: "idx_sort", type: "key", attributes: ["sort"] }],
},
{
id: "legalVersions",
name: "Rechtstext-Versionen",
perms: READ_WRITE,
attrs: [
{ key: "documentKey", type: "string", size: 64, required: true },
{ key: "version", type: "string", size: 32, required: true },
date("validFrom"),
date("validTo"),
{ key: "status", type: "enum", elements: ["draft", "current", "superseded"], required: true },
{ key: "changeNote", type: "string", size: 5000 },
{ key: "fileIds", type: "string[]", size: 64 },
{ key: "source", type: "string", size: 300 },
],
indexes: [
{ key: "idx_documentKey", type: "key", attributes: ["documentKey"] },
{ key: "idx_source", type: "key", attributes: ["source"] },
],
},
{
id: "providerContracts",
name: "Anbieter-Verträge",
perms: CRUD,
attrs: [
{ key: "provider", type: "string", size: 200, required: true },
{ key: "purpose", type: "string", size: 300 },
{ key: "cost", type: "float" },
{ key: "costInterval", type: "enum", elements: ["month", "year"] },
date("startDate"),
{ key: "termMonths", type: "integer", min: 0, max: 1200 },
{ key: "renewalMonths", type: "integer", min: 0, max: 1200 },
{ key: "noticeValue", type: "integer", min: 0, max: 3650 },
{ key: "noticeUnit", type: "enum", elements: ["days", "months"] },
date("cancelledAt"),
date("endsAt"),
{ key: "note", type: "string", size: 5000 },
{ key: "fileIds", type: "string[]", size: 64 },
],
indexes: [{ key: "idx_provider", type: "key", attributes: ["provider"] }],
},
];
export const BUCKET = {
id: "legal-files",
name: "Rechtliches Dateien",
perms: staffPermissions("read", "create"),
maxSize: 20 * 1024 * 1024,
extensions: ["pdf", "html", "htm", "png", "jpg", "jpeg", "webp", "doc", "docx"],
};
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", 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" },
// Terminplaner (TP2): live ist die Alias-Seite ohne Fassungsdatum (Portal, Aufgabe F1); die Versionen (PDF/HTML der Fassung) laedt der Inhaber hoch
{ key: "leistungsbeschreibung-terminplaner", title: "Leistungsbeschreibung Terminplaner", liveUrl: "https://project.webklar.com/rechtliches/leistungsbeschreibung-terminplaner.html" },
{ key: "avv-terminplaner", title: "AVV Terminplaner (Auftragsverarbeitung)", liveUrl: "https://project.webklar.com/rechtliches/avv-terminplaner.html" },
{ key: "widerrufsbelehrung-funktionen", title: "Widerrufsbelehrung und Muster-Widerrufsformular (Funktionen)", liveUrl: "https://project.webklar.com/rechtliches/widerrufsbelehrung-funktionen.html" },
// Oeffnungszeiten live: Schluessel hoechstens 36 Zeichen (WOMS-Grenze fuer Dokument-IDs); die Fassungen (PDF/HTML) laedt der Inhaber hoch
{ key: "leistungsbeschreibung-zeiten", title: "Leistungsbeschreibung Öffnungszeiten live", liveUrl: "https://project.webklar.com/vertragsunterlagen/zeiten-leistungsbeschreibung.html" },
].map((d, i) => ({ ...d, sort: i + 1 }));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/** Wartet, bis Appwrite alle Attribute einer Sammlung fertig angelegt hat. */
export async function waitForAttributes(db, collectionId, { intervalMs = 1000, timeoutMs = 120000 } = {}) {
const until = Date.now() + timeoutMs;
for (;;) {
const { attributes } = await db.listAttributes(databaseId, collectionId);
const failed = attributes.filter((a) => a.status === "failed" || a.status === "stuck");
if (failed.length) {
throw new Error(`Attribut fehlgeschlagen in ${collectionId}: ${failed.map((a) => `${a.key} (${a.error || a.status})`).join(", ")}`);
}
if (attributes.every((a) => a.status === "available")) return;
if (Date.now() > until) throw new Error(`Attribute in ${collectionId} nach ${timeoutMs} ms nicht verfuegbar`);
await sleep(intervalMs);
}
}
async function createAttribute(db, collectionId, a) {
const req = Boolean(a.required);
switch (a.type) {
case "integer":
return db.createIntegerAttribute(databaseId, collectionId, a.key, req, a.min, a.max);
case "float":
return db.createFloatAttribute(databaseId, collectionId, a.key, req);
case "enum":
return db.createEnumAttribute(databaseId, collectionId, a.key, a.elements, req);
case "string[]":
return db.createStringAttribute(databaseId, collectionId, a.key, a.size, req, undefined, true);
default:
return db.createStringAttribute(databaseId, collectionId, a.key, a.size, req);
}
}
async function run() {
if (dryRun) {
for (const c of COLLECTIONS) {
console.log(`Sammlung ${c.id} (${c.perms.join(", ")}): ${c.attrs.map((a) => a.key).join(", ")}`);
}
console.log(`Bucket ${BUCKET.id}: ${BUCKET.extensions.join(", ")}, max ${BUCKET.maxSize} Bytes`);
for (const d of DOCUMENTS) console.log(`Rechtstext ${d.key}: ${d.title}`);
return;
}
if (!apiKey) {
console.error("APPWRITE_API_KEY required (oder --dry-run)");
process.exit(1);
}
const { Client, Databases, Storage } = await import("node-appwrite");
const client = new Client().setEndpoint(endpoint).setProject(projectId).setKey(apiKey);
const db = new Databases(client);
const storage = new Storage(client);
for (const c of COLLECTIONS) {
try {
await db.getCollection(databaseId, c.id);
await db.updateCollection(databaseId, c.id, c.name, c.perms, false, true);
console.log("Sammlung vorhanden:", c.id);
} catch (e) {
if (e.code !== 404) throw e;
await db.createCollection(databaseId, c.id, c.name, c.perms, false, true);
console.log("Sammlung angelegt:", c.id);
}
const existing = new Set((await db.listAttributes(databaseId, c.id)).attributes.map((a) => a.key));
for (const a of c.attrs) {
if (existing.has(a.key)) continue;
await createAttribute(db, c.id, a);
console.log(" Attribut", a.key);
await sleep(400);
}
await waitForAttributes(db, c.id);
const indexes = new Set((await db.listIndexes(databaseId, c.id)).indexes.map((i) => i.key));
for (const idx of c.indexes || []) {
if (indexes.has(idx.key)) continue;
await db.createIndex(databaseId, c.id, idx.key, idx.type, idx.attributes);
console.log(" Index", idx.key);
}
}
try {
await storage.getBucket(BUCKET.id);
console.log("Bucket vorhanden:", BUCKET.id);
} catch (e) {
if (e.code !== 404) throw e;
await storage.createBucket(BUCKET.id, BUCKET.name, BUCKET.perms, false, true, BUCKET.maxSize, BUCKET.extensions);
console.log("Bucket angelegt:", BUCKET.id);
}
for (const d of DOCUMENTS) {
try {
await db.getDocument(databaseId, "legalDocuments", d.key);
} catch (e) {
if (e.code !== 404) throw e;
await db.createDocument(databaseId, "legalDocuments", d.key, d);
console.log("Rechtstext angelegt:", d.key);
}
}
console.log("Fertig.");
}
// 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);
});
}