diff --git a/server/pruef-fassungen.mjs b/server/pruef-fassungen.mjs new file mode 100644 index 00000000..a201c6d6 --- /dev/null +++ b/server/pruef-fassungen.mjs @@ -0,0 +1,338 @@ +/* ===================================================================== + Dateifassungen (15.09.2026) + + Stufe 9: „Dateiversionen statt Überschreiben". + + --------------------------------------------------------------------- + GEMESSEN, BEVOR ETWAS GEBAUT WURDE + + Der Plan sagt „statt Überschreiben" — überschrieben wurde aber nie: + `name_datei` ist eindeutig und zufällig, jedes Hochladen legt eine + neue Zeile an. Der wirkliche Mangel ist ein anderer, und er ist + gefährlicher: + + Zwei Dateien gleichen Namens standen BEZIEHUNGSLOS nebeneinander. + Die alte kann „freigegeben" sein, die neue „entwurf" — und wer die + Liste ansieht, lädt die freigegebene herunter. Also die falsche. + Kein Fehler, keine Meldung, nur die alte Fassung in der Hand. + + --------------------------------------------------------------------- + DIE DREI FRAGEN + + 1. Wird die richtige Vorgängerin erkannt? + 2. Wird bei Zweifel NICHT geraten? ← die wichtigere Hälfte + 3. Sieht man der alten Fassung an, dass sie alt ist? + + Die zweite ist der Grund, warum die Bedingung eng ist: Eine + Automatik, die falsch verknüpft, erfindet einen Zusammenhang zwischen + zwei Dateien, die nichts miteinander zu tun haben. + + Aufrufen mit: node server/pruef-fassungen.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4415, "pruef-fassungen"); + +const ordner = mkdtempSync(join(tmpdir(), "ws-fass-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "pruef-fassungen"); +await new Promise((r) => setTimeout(r, 700)); + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const HAUS = "workspace.dogfather-universe.com"; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); + +const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); + +let idDogi, idLuna, idMax; +{ + const d = oeffne(); + const schluessel = randomBytes(32).toString("hex"); + d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", schluessel, jetzt); + const anlegen = (name, rolle, code) => { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, createHmac("sha256", schluessel).update(code).digest("hex"), jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; + }; + idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); + idLuna = anlegen("Luna", "creator", "CODE-CREA-0001"); + idMax = anlegen("Max", "creator", "CODE-CREA-0002"); + d.close(); +} + +function ruf(pfad, methode, kopf = {}, koerper = null) { + return new Promise((fertig, schief) => { + const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, + headers: { Host: HAUS, ...kopf } }, (antwort) => { + let text = ""; + antwort.on("data", (s) => { text += s; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers })); + }); + a.on("error", schief); + if (koerper) a.write(koerper); + a.end(); + }); +} +const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } }; + +const anm = await ruf("/workspace/api/anmelden", "POST", + { "Content-Type": "application/json" }, + JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" })); +const kDogi = [].concat(anm.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "); +ok(!!kDogi, "DogFather ist angemeldet"); + +/** Laedt eine Datei hoch und gibt die Antwort zurueck. */ +async function hochladen(name, inhalt, creatorId) { + return ruf("/workspace/api/dateien", "POST", { + cookie: kDogi, + "content-type": "text/plain", + "x-dateiname": encodeURIComponent(name), + ...(creatorId ? { "x-creator": String(creatorId) } : {}), + "content-length": Buffer.byteLength(inhalt), + }, inhalt); +} +const liste = async () => json(await ruf("/workspace/api/dateien", "GET", { cookie: kDogi })).dateien || []; + +/* Erst nachsehen, wie der Weg die Angaben erwartet -- eine Pruefung, + die am Hochladen scheitert, prueft nichts. */ +{ + const probe = await hochladen("probe.txt", "eins", idLuna); + ok(probe.code === 201, `eine Datei laesst sich hochladen (${probe.code}) ${probe.code === 201 ? "" : probe.text.slice(0, 120)}`); +} + +/* ======================================================================= + 1. DIE ZWEITE FASSUNG WIRD ERKANNT + ======================================================================= */ +melde(""); +melde("=== 1. Die zweite Fassung ==="); +{ + const eins = await hochladen("vertrag.pdf", "Fassung eins", idLuna); + ok(eins.code === 201, `erste Fassung hochgeladen (${eins.code})`); + ok(json(eins).ersetzt_id === null, "sie ersetzt nichts — es gab noch nichts"); + + const zwei = await hochladen("vertrag.pdf", "Fassung zwei", idLuna); + ok(zwei.code === 201, `zweite Fassung hochgeladen (${zwei.code})`); + ok(json(zwei).ersetzt_id === json(eins).id, + `und sie zeigt auf die erste (${json(zwei).ersetzt_id} → ${json(eins).id})`); + + const drei = await hochladen("vertrag.pdf", "Fassung drei", idLuna); + ok(json(drei).ersetzt_id === json(zwei).id, + "die dritte auf die zweite — eine Kette, kein Stern"); + + const alle = await liste(); + const d3 = alle.find((x) => x.id === json(drei).id); + const d2 = alle.find((x) => x.id === json(zwei).id); + const d1 = alle.find((x) => x.id === json(eins).id); + ok(d3?.fassung === 3, `die neueste ist Fassung ${d3?.fassung}`); + ok(d2?.fassung === 2 && d1?.fassung === 1, + `die beiden davor sind ${d2?.fassung} und ${d1?.fassung}`); + ok(d1?.ersetzt_durch === d2?.id && d2?.ersetzt_durch === d3?.id, + "jede weiss, wer sie ersetzt hat"); + /* DAS IST DER GRUND FUER DAS GANZE: Der alten sieht man an, dass sie + alt ist. */ + ok(d3?.ersetzt_durch === null, "und die neueste wird von niemandem ersetzt"); +} + +/* ======================================================================= + 2. BEI ZWEIFEL WIRD NICHT GERATEN + + Die wichtigere Haelfte. Eine Automatik, die falsch verknuepft, + erfindet einen Zusammenhang zwischen zwei Dateien, die nichts + miteinander zu tun haben. + ======================================================================= */ +melde(""); +melde("=== 2. Nicht geraten ==="); +{ + /* Anderer Creator, gleicher Name -- das ist NICHT dieselbe Datei. */ + const fremd = await hochladen("vertrag.pdf", "Vertrag von Max", idMax); + ok(fremd.code === 201, `dieselbe Benennung bei einem anderen Creator (${fremd.code})`); + ok(json(fremd).ersetzt_id === null, + `wird nicht verknuepft (${json(fremd).ersetzt_id}) — es ist Max' Vertrag, nicht Lunas`); + + /* Anderer Name, gleicher Creator -- auch nicht. */ + const anders = await hochladen("konzept.pdf", "Etwas anderes", idLuna); + ok(json(anders).ersetzt_id === null, "und ein anderer Name erst recht nicht"); + + /* MEHRDEUTIG: Gibt es zwei unersetzte Dateien gleichen Namens, wird + GAR NICHT verknuepft. Hergestellt wird der Fall ueber den Weg, den + es dafuer gibt -- die Verknuepfung loesen. */ + const a = await hochladen("doppelt.txt", "A", idLuna); + const b = await hochladen("doppelt.txt", "B", idLuna); + ok(json(b).ersetzt_id === json(a).id, "zwei gleiche Namen ergeben zunaechst eine Kette"); + const geloest = await ruf(`/workspace/api/dateien/${json(b).id}/fassung`, "DELETE", { cookie: kDogi }); + ok(geloest.code === 200, `die Verknuepfung laesst sich loesen (${geloest.code})`); + + const c = await hochladen("doppelt.txt", "C", idLuna); + ok(json(c).ersetzt_id === null, + `jetzt gibt es zwei Unersetzte gleichen Namens — und es wird NICHT geraten (${json(c).ersetzt_id})`); +} + +/* ======================================================================= + 3. DIE VERKNUEPFUNG LOESEN -- UND NUR SIE + ======================================================================= */ +melde(""); +melde("=== 3. Loesen ==="); +{ + const eins = await hochladen("loesen.txt", "eins", idLuna); + const zwei = await hochladen("loesen.txt", "zwei", idLuna); + ok(json(zwei).ersetzt_id === json(eins).id, "verknuepft"); + + const vorher = (await liste()).length; + const a = await ruf(`/workspace/api/dateien/${json(zwei).id}/fassung`, "DELETE", { cookie: kDogi }); + ok(a.code === 200, `geloest (${a.code})`); + + const nachher = await liste(); + ok(nachher.length === vorher, `KEINE Datei ist dabei verschwunden (${vorher} → ${nachher.length})`); + const d2 = nachher.find((x) => x.id === json(zwei).id); + const d1 = nachher.find((x) => x.id === json(eins).id); + ok(d2?.fassung === 1 && d2?.ersetzt_id === null, "die zweite steht wieder fuer sich"); + ok(d1?.ersetzt_durch === null, "und die erste gilt nicht mehr als ueberholt"); + + /* Ein zweites Loesen ist kein Fehler des Nutzers, aber auch kein + Erfolg -- es gibt nichts mehr zu loesen. */ + const nochmal = await ruf(`/workspace/api/dateien/${json(zwei).id}/fassung`, "DELETE", { cookie: kDogi }); + ok(nochmal.code === 409, `ein zweites Loesen sagt "da war nichts" (${nochmal.code})`); + + const erfunden = await ruf("/workspace/api/dateien/999999/fassung", "DELETE", { cookie: kDogi }); + ok(erfunden.code === 404, `eine erfundene Nummer ergibt 404 (${erfunden.code})`); + + const ohne = await ruf(`/workspace/api/dateien/${json(eins).id}/fassung`, "DELETE"); + ok(ohne.code === 401 || ohne.code === 404, + `ohne Anmeldung geht es nicht (${ohne.code})`); +} + +/* ======================================================================= + 4. WIRD DIE ALTE FASSUNG GELOESCHT, BLEIBT DIE NEUE + ======================================================================= */ +melde(""); +melde("=== 4. Wenn die alte verschwindet ==="); +{ + const eins = await hochladen("weg.txt", "eins", idLuna); + const zwei = await hochladen("weg.txt", "zwei", idLuna); + ok(json(zwei).ersetzt_id === json(eins).id, "verknuepft"); + + const d = oeffne(); + d.prepare("PRAGMA foreign_keys = ON").run(); + d.prepare("DELETE FROM dateien WHERE id = ?").run(json(eins).id); + const uebrig = d.prepare("SELECT id, ersetzt_id FROM dateien WHERE id = ?").get(json(zwei).id); + d.close(); + + ok(!!uebrig, "die neue Fassung gibt es noch, obwohl die alte weg ist"); + ok(uebrig.ersetzt_id === null, + `und der Verweis steht auf null statt ins Leere (${uebrig.ersetzt_id})`); + + const nach = (await liste()).find((x) => x.id === json(zwei).id); + ok(nach?.fassung === 1, + `sie zaehlt jetzt wieder als erste Fassung (${nach?.fassung}) — die Nummer ist gerechnet, nicht gespeichert`); +} + +/* ======================================================================= + 5. EIN RING WUERDE DIE SEITE NICHT EINFRIEREN + + Kann durch die Wege oben nicht entstehen -- aber ein Fehler anderswo + koennte ihn erzeugen, und ein Stillstand ist schlimmer als ein + Fehler. Deshalb eine Schranke in der Zaehlung, und deshalb hier der + Beweis, dass sie greift. + ======================================================================= */ +melde(""); +melde("=== 5. Ein Ring haelt die Seite nicht an ==="); +{ + const a = await hochladen("ring.txt", "a", idLuna); + const b = await hochladen("ring.txt", "b", idLuna); + const d = oeffne(); + d.prepare("UPDATE dateien SET ersetzt_id = ? WHERE id = ?").run(json(b).id, json(a).id); + d.close(); + + const beginn = Date.now(); + const alle = await liste(); + const dauer = Date.now() - beginn; + ok(alle.length > 0, `die Liste kommt trotzdem (${alle.length} Dateien)`); + ok(dauer < 5000, `und zwar in ${dauer} ms statt nie`); + + /* Aufraeumen, damit die naechste Zeile nicht an diesem Ring haengt. */ + const dd = oeffne(); + dd.prepare("UPDATE dateien SET ersetzt_id = NULL WHERE id = ?").run(json(a).id); + dd.close(); +} + +/* ======================================================================= + 6. IM BROWSER + ======================================================================= */ +melde(""); +melde("=== 6. Im Browser ==="); +{ + const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const browser = await chromium.launch(); + const kontext = await browser.newContext({ viewport: { width: 1280, height: 900 } }); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text()); }); + seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message)); + + await seite.goto(`http://127.0.0.1:${PORT}/workspace/`, { waitUntil: "domcontentloaded" }); + await seite.click('.rolle[data-rolle="admin"]'); + await seite.fill("#code", "CODE-DOGI-0001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 20000 }); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/dateien.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(900); + + const stand = await seite.evaluate(() => ({ + nummern: document.querySelectorAll(".fassung__nummer").length, + alt: document.querySelectorAll(".fassung__alt").length, + loesen: document.querySelectorAll(".fassung__loesen").length, + text: [...document.querySelectorAll(".fassung")].map((f) => f.innerText.replace(/\s+/g, " ").trim()).slice(0, 4), + })); + ok(stand.nummern >= 2, `${stand.nummern} Dateien tragen eine Fassungsnummer`); + ok(stand.alt >= 1, `${stand.alt} sind als „nicht mehr aktuell“ gekennzeichnet`); + ok(stand.loesen >= 1, `${stand.loesen} lassen sich wieder lösen`); + ok(stand.text.some((t) => /Fassung \d/.test(t)), + `die Beschriftung steht da: ${stand.text.join(" | ").slice(0, 110)}`); + + /* GEGENPROBE: Der Loese-Knopf steht NUR an aktuellen Fassungen. Wer + eine ueberholte loest, macht daraus eine zweite aktuelle. */ + const falsch = await seite.evaluate(() => + [...document.querySelectorAll(".fassung")] + .filter((f) => f.querySelector(".fassung__alt") && f.querySelector(".fassung__loesen")).length); + ok(falsch === 0, `kein Löse-Knopf an einer überholten Fassung (${falsch})`); + + ok(konsole.length === 0, + konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser"); + await browser.close(); +} + +/* ------------------------------------------------------------------- */ +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-dateien.js b/server/workspace-dateien.js index 94a7be86..3d07443a 100644 --- a/server/workspace-dateien.js +++ b/server/workspace-dateien.js @@ -259,6 +259,40 @@ function fassungenSetzen(liste) { } } +/* ===================================================================== + DIE VERKNUEPFUNG WIEDER LOESEN (15.09.2026) + + Der Server verknuepft von selbst, wenn Name und Creator gleich sind. + Meistens stimmt das; manchmal nicht -- zwei verschiedene Dateien + koennen denselben Namen tragen. + + EINE AUTOMATIK OHNE AUSWEG IST EINE BEHAUPTUNG. Deshalb dieser Weg: + ein Klick, und die Datei steht wieder fuer sich. NUR die Verknuepfung + geht, nie eine Datei -- der Weg kann also nichts anrichten, was sich + nicht in einem zweiten Klick zuruecknehmen liesse. + ===================================================================== */ +dateienRouter.delete("/workspace/api/dateien/:id(\\d+)/fassung", (req, res) => { + try { + const id = Number(req.params.id); + const { wo, werte } = sichtbar(req.sicht || req.person); + const d = db().prepare( + `SELECT d.id, d.name_original, d.ersetzt_id ${VERBUND} WHERE ${wo} AND d.id = ?`) + .get(...werte, id); + if (!d) return res.status(404).json({ fehler: "nicht_gefunden" }); + if (!d.ersetzt_id) return res.status(409).json({ fehler: "keine_fassung" }); + + db().prepare("UPDATE dateien SET ersetzt_id = NULL WHERE id = ?").run(id); + protokolliere("datei_fassung_geloest", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + detail: `#${id} ${d.name_original} war Fassung von #${d.ersetzt_id}`.slice(0, 120), + }); + res.json({ ok: true }); + } catch (fehler) { + console.error("[dateien] Fassung loesen:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + /* ---------- Liste ------------------------------------------------------- */ dateienRouter.get("/workspace/api/dateien", (req, res) => { diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 9d97e4b3..90c317de 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609151252", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609151258", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609151252", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609151258", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151252", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151258", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151252", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151258", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/dateien.css b/workspace/assets/css/dateien.css index 63df1a86..a3faab7a 100644 --- a/workspace/assets/css/dateien.css +++ b/workspace/assets/css/dateien.css @@ -176,3 +176,31 @@ .marke-person--leer { color: var(--text-still); font-weight: 400; font-style: italic; } @media (prefers-reduced-motion: reduce) { .wahl__knopf { transition: none; } } + +/* ===================================================================== + FASSUNGEN (15.09.2026) + + "Fassung 2" ist eine Nummer und bleibt ruhig. "nicht mehr aktuell" + ist eine Warnung und darf auffallen -- aber nicht schreien: Sie steht + an einer Datei, die niemand falsch gemacht hat, sie ist nur alt. + ===================================================================== */ + +.fassung { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 9px; } +.fassung__nummer { + font-size: .78rem; font-weight: 640; color: var(--text-still); + font-variant-numeric: tabular-nums; +} +.fassung__alt { + font-size: .78rem; font-weight: 640; + padding: 2px 9px; border-radius: 999px; + color: #d8a76a; + border: 1px solid color-mix(in srgb, #cc9451 45%, transparent); +} +.fassung__loesen { + font: inherit; font-size: .76rem; + padding: 2px 9px; border-radius: 999px; cursor: pointer; + color: var(--text-still); + background: none; + border: 1px solid var(--rand); +} +.fassung__loesen:hover { color: var(--text); border-color: var(--rand-hell); } diff --git a/workspace/assets/js/dateien.js b/workspace/assets/js/dateien.js index c7a4d3f4..7ecc77cb 100644 --- a/workspace/assets/js/dateien.js +++ b/workspace/assets/js/dateien.js @@ -238,6 +238,46 @@ if (d.notiz) zeile2.append(el('span', null, '· ' + d.notiz)); k.append(zeile2); + /* ---- WELCHE FASSUNG IST DAS? (15.09.2026, Stufe 9) -------------- + + ZWEI VERSCHIEDENE AUSSAGEN, und sie duerfen nicht gleich aussehen: + + "Fassung 2" eine Nummer, ruhig + "nicht mehr aktuell" eine WARNUNG -- wer DIESE Datei + herunterlaedt, bekommt die falsche + + Die zweite ist der eigentliche Grund fuer das Ganze: Eine alte + Fassung kann "freigegeben" sein, waehrend die neue noch + "entwurf" ist. Ohne den Hinweis laedt man die freigegebene -- und + merkt nichts davon. */ + if (d.fassung > 1 || d.ersetzt_durch) { + const f = el('div', 'fassung'); + if (d.fassung > 1) f.append(el('span', 'fassung__nummer', 'Fassung ' + d.fassung)); + if (d.ersetzt_durch) { + const w = el('span', 'fassung__alt', 'nicht mehr aktuell'); + w.title = 'Es gibt eine neuere Fassung dieser Datei.'; + f.append(w); + } + /* Nur an der AKTUELLEN Fassung: Wer eine ueberholte loest, macht + daraus eine zweite aktuelle -- und hat das Problem zurueck, + das die Verknuepfung geloest hat. */ + if (d.fassung > 1 && !d.ersetzt_durch) { + const k2 = el('button', 'fassung__loesen', 'eigene Datei'); + k2.type = 'button'; + k2.title = 'Das ist keine neue Fassung, sondern eine eigenständige Datei.'; + k2.addEventListener('click', async () => { + k2.disabled = true; + try { + const a = await hole('/workspace/api/dateien/' + d.id + '/fassung', + { method: 'DELETE' }); + if (a.ok) await laden(); else melde('Ging nicht.'); + } finally { k2.disabled = false; } + }); + f.append(k2); + } + k.append(f); + } + /* Wer sie sehen darf -- sichtbar fuer alle, damit niemand raten muss, wer eine Datei schon hat. */ const frei = el('div', 'datei__freigaben'); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 689d73b1..740117aa 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - +
@@ -252,10 +252,10 @@ - - - - - + + + + +