Workspace: hellere Kategoriekacheln + Dateien gezielt an mehrere Personen
ZWEI WUENSCHE VON FILIPE.
1) KACHELN BESSER SICHTBAR
Erster Versuch war zu zaghaft: die Flaeche ging nur von RGB(17,24,37)
auf (27,36,51), der sichtbare Gewinn kam fast nur vom staerkeren Rand.
Nachgemessen an echten Bildpunkten -- Seite liegt bei RGB(5,7,13).
Jetzt RGB(38,49,67), dazu kraeftigerer Rand und hellerer Symbolring.
Der Beschreibungstext stand auf --text-still und lag damit bei 3,6:1 auf
der neuen Flaeche -- zu blass fuer laufenden Text. Jetzt --text-leise,
gemessen 6,2:1.
Leere Kategorien waren mit opacity .55 fast unlesbar. Jetzt .82 -- sie
sollen erkennbar bleiben, nur zurueckhaltender.
2) DATEIEN AN MEHRERE PERSONEN GEZIELT FREIGEBEN
Bisher hatte eine Datei genau EINEN Bereich (creator_id). Damit liess
sie sich nicht zweien geben, ohne sie zweimal hochzuladen.
Neue Tabelle datei_personen (datei_id + person_id). creator_id bleibt
und behaelt seine Bedeutung: Es sagt, zu wessen BEREICH eine Datei
gehoert -- die neue Tabelle sagt, WER sie sehen darf. Zwei verschiedene
Fragen, deshalb zwei Felder.
Auswahl als einzelne Schalter, nicht als <select multiple>: Dort
verliert man mit einem Fehlklick die ganze Auswahl, und auf dem Handy
ist sie kaum bedienbar. Jeder Name ist ein Schalter, der sichtbar an
oder aus ist, eingefaerbt nach Rolle -- man sieht auf einen Blick, ob
eine Datei an Creator, Scouts oder beide geht.
Wer wen auswaehlen darf:
- DogFather jeden aktiven Menschen ausser sich selbst
- ein Scout NUR die Creator, die er betreut -- sonst koennte er sich
ueber eine Freigabe Zugang zu fremden Bereichen verschaffen
- ein Creator gar niemanden
Jede Id wird beim Speichern erneut gegen die erlaubte Auswahl geprueft.
Geprueft: Sam schickt die Ids 3, 6 und 7 mit (alles Creator, die er
NICHT betreut) -- die Datei landet bei niemandem. Kein Fehler, kein
Zugang: die Ids fallen still durch das Raster.
Weiter geprueft:
- Chef sieht alle drei Testdateien, Luna nur ihre, Sam nur seine,
Patrick keine
- Creator bekommt 403 beim Aendern von Freigaben
- Freigaben sind an jeder Datei sichtbar ("Sichtbar fuer ..."), auch
fuer die, die sie nicht aendern duerfen -- niemand soll raten muessen
- Die Auswahl wird nach dem Hochladen geleert, sonst bekaeme die
naechste Datei stillschweigend dasselbe Publikum
This commit is contained in:
@@ -29,7 +29,7 @@ import { randomBytes } from "node:crypto";
|
||||
import { join, extname, basename } from "node:path";
|
||||
import { mkdirSync, createReadStream, writeFileSync, unlinkSync, statSync } from "node:fs";
|
||||
import {
|
||||
db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator,
|
||||
db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds,
|
||||
} from "./workspace.js";
|
||||
|
||||
export const dateienRouter = express.Router();
|
||||
@@ -69,13 +69,71 @@ dateienRouter.use("/workspace/api/dateien", angemeldet);
|
||||
vergisst. */
|
||||
export function sichtbar(person) {
|
||||
if (person.rolle === "admin") return { wo: "1=1", werte: [] };
|
||||
|
||||
/* Ausdrueckliche Freigabe -- gilt fuer jede Rolle gleich. Wer in der
|
||||
Liste steht, sieht die Datei, egal ob Creator oder Scout. */
|
||||
const freigabe = "EXISTS (SELECT 1 FROM datei_personen dp WHERE dp.datei_id = d.id AND dp.person_id = ?)";
|
||||
|
||||
if (person.rolle === "creator") {
|
||||
return { wo: "(d.creator_id = ? OR d.hochgeladen_von = ?)", werte: [person.id, person.id] };
|
||||
return {
|
||||
wo: `(d.creator_id = ? OR d.hochgeladen_von = ? OR ${freigabe})`,
|
||||
werte: [person.id, person.id, person.id],
|
||||
};
|
||||
}
|
||||
const b = betreutWo(person, "d.creator_id");
|
||||
return b
|
||||
? { wo: `(d.hochgeladen_von = ? OR ${b.wo})`, werte: [person.id, ...b.werte] }
|
||||
: { wo: "d.hochgeladen_von = ?", werte: [person.id] };
|
||||
? { wo: `(d.hochgeladen_von = ? OR ${freigabe} OR ${b.wo})`,
|
||||
werte: [person.id, person.id, ...b.werte] }
|
||||
: { wo: `(d.hochgeladen_von = ? OR ${freigabe})`, werte: [person.id, person.id] };
|
||||
}
|
||||
|
||||
/* Wem eine Datei freigegeben ist -- fuer die Anzeige. */
|
||||
export function freigabenZu(ids) {
|
||||
if (!ids.length) return new Map();
|
||||
const platz = ids.map(() => "?").join(",");
|
||||
const karte = new Map();
|
||||
for (const z of db().prepare(`
|
||||
SELECT dp.datei_id, p.id, p.name, p.rolle
|
||||
FROM datei_personen dp JOIN personen p ON p.id = dp.person_id
|
||||
WHERE dp.datei_id IN (${platz})
|
||||
ORDER BY p.rolle, p.name`).all(...ids)) {
|
||||
if (!karte.has(z.datei_id)) karte.set(z.datei_id, []);
|
||||
karte.get(z.datei_id).push({ id: z.id, name: z.name, rolle: z.rolle });
|
||||
}
|
||||
return karte;
|
||||
}
|
||||
|
||||
/* Wen darf diese Person ueberhaupt auswaehlen? DogFather jeden aktiven
|
||||
Menschen ausser sich selbst, ein Scout nur die Creator, die er
|
||||
betreut -- sonst koennte er sich ueber eine Freigabe Zugang zu
|
||||
fremden Bereichen verschaffen. */
|
||||
export function moeglicheEmpfaenger(person) {
|
||||
if (person.rolle === "admin") {
|
||||
return db().prepare(
|
||||
"SELECT id, name, rolle FROM personen WHERE aktiv = 1 AND id <> ? ORDER BY rolle, name")
|
||||
.all(person.id);
|
||||
}
|
||||
if (person.rolle === "scout") {
|
||||
const ids = betreuteIds(person);
|
||||
if (!ids.length) return [];
|
||||
return db().prepare(
|
||||
`SELECT id, name, rolle FROM personen WHERE aktiv = 1 AND id IN (${ids.map(() => "?").join(",")})
|
||||
ORDER BY name`).all(...ids);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/* Freigaben setzen. Prueft jede Id gegen die erlaubte Auswahl -- ein
|
||||
geschickter Aufruf soll keine Person erreichen, die im Formular gar
|
||||
nicht zur Wahl stand. */
|
||||
export function freigabenSetzen(dateiId, ids, person) {
|
||||
const erlaubt = new Set(moeglicheEmpfaenger(person).map((p) => p.id));
|
||||
const sauber = [...new Set(ids.map(Number).filter((n) => Number.isInteger(n) && erlaubt.has(n)))];
|
||||
const d = db();
|
||||
d.prepare("DELETE FROM datei_personen WHERE datei_id = ?").run(dateiId);
|
||||
const einf = d.prepare("INSERT OR IGNORE INTO datei_personen (datei_id, person_id) VALUES (?,?)");
|
||||
for (const pid of sauber) einf.run(dateiId, pid);
|
||||
return sauber.length;
|
||||
}
|
||||
|
||||
const SPALTEN = `
|
||||
@@ -93,11 +151,16 @@ const VERBUND = `
|
||||
dateienRouter.get("/workspace/api/dateien", (req, res) => {
|
||||
try {
|
||||
const { wo, werte } = sichtbar(req.person);
|
||||
res.json({
|
||||
dateien: db().prepare(`
|
||||
const liste = db().prepare(`
|
||||
SELECT ${SPALTEN} ${VERBUND}
|
||||
WHERE ${wo}
|
||||
ORDER BY d.erstellt DESC`).all(...werte),
|
||||
ORDER BY d.erstellt DESC`).all(...werte);
|
||||
const karte = freigabenZu(liste.map((d) => d.id));
|
||||
for (const d of liste) d.freigaben = karte.get(d.id) || [];
|
||||
res.json({
|
||||
dateien: liste,
|
||||
empfaenger: moeglicheEmpfaenger(req.person),
|
||||
darf_freigeben: req.person.rolle !== "creator",
|
||||
max_bytes: MAX_BYTES,
|
||||
});
|
||||
} catch (fehler) {
|
||||
@@ -195,9 +258,19 @@ dateienRouter.post("/workspace/api/dateien",
|
||||
(req.get("content-type") || "application/octet-stream").slice(0, 100),
|
||||
notiz, creatorId, req.person.id, jetzt());
|
||||
|
||||
/* Ausdrueckliche Freigaben. Der Kopf traegt eine Liste von Ids --
|
||||
geprueft wird jede einzelne gegen die erlaubte Auswahl. */
|
||||
let anzahl = 0;
|
||||
const roh = req.get("x-personen");
|
||||
if (roh) {
|
||||
anzahl = freigabenSetzen(Number(lastInsertRowid),
|
||||
String(roh).split(",").map((x) => x.trim()).filter(Boolean), req.person);
|
||||
}
|
||||
|
||||
protokolliere("datei_hochgeladen", {
|
||||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||||
detail: `#${lastInsertRowid} ${name} (${Math.round(req.body.length / 1024)} KB)`.slice(0, 120),
|
||||
detail: `#${lastInsertRowid} ${name} (${Math.round(req.body.length / 1024)} KB)`
|
||||
+ (anzahl ? `, freigegeben fuer ${anzahl}` : "").slice(0, 40),
|
||||
});
|
||||
res.status(201).json({ id: Number(lastInsertRowid) });
|
||||
} catch (fehler) {
|
||||
@@ -255,6 +328,24 @@ dateienRouter.patch("/workspace/api/dateien/:id", gleicheHerkunft, (req, res) =>
|
||||
const datei = db().prepare(`SELECT d.* ${VERBUND} WHERE ${wo} AND d.id = ?`).get(...werte, id);
|
||||
if (!datei) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
/* Freigaben aendern. Steht vor den Feldern, weil es eine eigene
|
||||
Tabelle ist und nicht in das UPDATE unten passt. Ein Creator darf
|
||||
das nie -- er wuerde sich sonst selbst Publikum verschaffen. */
|
||||
if (req.body?.freigaben !== undefined) {
|
||||
if (req.person.rolle === "creator") {
|
||||
return res.status(403).json({ fehler: "Freigeben darf nur DogFather oder ein Scout." });
|
||||
}
|
||||
if (!Array.isArray(req.body.freigaben)) {
|
||||
return res.status(400).json({ fehler: "Ungültige Auswahl." });
|
||||
}
|
||||
const n = freigabenSetzen(id, req.body.freigaben, req.person);
|
||||
protokolliere("datei_freigaben", {
|
||||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||||
detail: `#${id} an ${n} Person(en)`.slice(0, 120),
|
||||
});
|
||||
if (Object.keys(req.body).length === 1) return res.json({ ok: true });
|
||||
}
|
||||
|
||||
const setz = [];
|
||||
const daten = [];
|
||||
|
||||
|
||||
@@ -237,6 +237,20 @@ export function db() {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_eintraege_bereich ON eintraege (bereich, creator_id);
|
||||
|
||||
/* Wer darf welche Datei sehen. Bisher hatte eine Datei genau
|
||||
EINEN Bereich (dateien.creator_id) -- damit liess sich eine
|
||||
Datei nicht zweien geben, ohne sie zweimal hochzuladen.
|
||||
Diese Tabelle erlaubt beliebig viele Empfaenger, Creator wie
|
||||
Scouts. creator_id bleibt bestehen: Es sagt weiterhin, zu
|
||||
wessen BEREICH eine Datei gehoert, waehrend hier steht, WER sie
|
||||
sehen darf. Zwei verschiedene Fragen. */
|
||||
CREATE TABLE IF NOT EXISTS datei_personen (
|
||||
datei_id INTEGER NOT NULL REFERENCES dateien(id) ON DELETE CASCADE,
|
||||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (datei_id, person_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_datei_personen ON datei_personen (person_id);
|
||||
|
||||
/* Wissens-Bibliothek: Anleitungen als PDF, hinter dem Login.
|
||||
Jede PDF hat GENAU EINE Hauptkategorie -- so gibt es sie nur
|
||||
einmal. Zusaetzliche Themen laufen ueber Tags, damit dieselbe
|
||||
|
||||
@@ -120,3 +120,64 @@
|
||||
font-size: .88rem; color: var(--text-leise); line-height: 1.5;
|
||||
}
|
||||
.nur-lesen strong { color: var(--text); }
|
||||
|
||||
/* ===================================================================
|
||||
Wer darf die Datei sehen (28.08.2026).
|
||||
|
||||
Kleine Schalter statt <select multiple>: Dort verliert man mit einem
|
||||
Fehlklick die ganze Auswahl, und auf dem Handy ist sie kaum
|
||||
bedienbar. Hier ist jeder Name ein eigener Schalter, der sichtbar an
|
||||
oder aus ist.
|
||||
=================================================================== */
|
||||
|
||||
.hochlade-zeile { display: grid; gap: 16px; margin-bottom: 22px; }
|
||||
|
||||
.wahl, .datei__wahl { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 8px; }
|
||||
.datei__wahl { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--rand); }
|
||||
|
||||
.wahl__knopf {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 7px 13px; cursor: pointer;
|
||||
background: rgba(255, 255, 255, .04);
|
||||
border: 1px solid var(--rand); border-radius: 999px;
|
||||
color: var(--text-leise); font: inherit; font-size: .84rem;
|
||||
transition: color .16s ease, border-color .16s ease, background .16s ease;
|
||||
}
|
||||
.wahl__knopf:hover { color: var(--text); border-color: rgba(255, 255, 255, .24); }
|
||||
.wahl__rolle {
|
||||
font-size: .7rem; font-weight: 600; letter-spacing: .03em;
|
||||
color: var(--text-still);
|
||||
}
|
||||
/* Ausgewählt trägt die Farbe der Rolle -- man sieht auf einen Blick,
|
||||
ob eine Datei an Creator, Scouts oder beide geht. */
|
||||
.wahl__knopf[data-an="ja"] { color: #fff; font-weight: 600; }
|
||||
.wahl__knopf[data-an="ja"][data-rolle="creator"] {
|
||||
border-color: rgba(63, 189, 245, .55); background: rgba(63, 189, 245, .16);
|
||||
}
|
||||
.wahl__knopf[data-an="ja"][data-rolle="scout"] {
|
||||
border-color: rgba(138, 118, 255, .55); background: rgba(138, 118, 255, .16);
|
||||
}
|
||||
.wahl__knopf[data-an="ja"][data-rolle="admin"] {
|
||||
border-color: rgba(232, 192, 125, .55); background: rgba(232, 192, 125, .16);
|
||||
}
|
||||
.wahl__knopf[data-an="ja"] .wahl__rolle { color: rgba(255, 255, 255, .75); }
|
||||
|
||||
.datei__freigaben {
|
||||
display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
|
||||
margin-top: 10px; padding-top: 10px;
|
||||
border-top: 1px solid var(--rand);
|
||||
}
|
||||
.datei__schild {
|
||||
font-size: .7rem; font-weight: 700; letter-spacing: .07em;
|
||||
text-transform: uppercase; color: var(--text-still);
|
||||
}
|
||||
.marke-person {
|
||||
padding: 3px 10px; border-radius: 999px;
|
||||
font-size: .76rem; font-weight: 600;
|
||||
color: var(--text-leise);
|
||||
background: rgba(255, 255, 255, .05);
|
||||
border: 1px solid var(--rand);
|
||||
}
|
||||
.marke-person--leer { color: var(--text-still); font-weight: 400; font-style: italic; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) { .wahl__knopf { transition: none; } }
|
||||
|
||||
@@ -76,13 +76,18 @@
|
||||
padding: 15px 16px;
|
||||
text-decoration: none; color: var(--text);
|
||||
border: 1px solid transparent; border-radius: 13px;
|
||||
/* Deutlich heller als der Seitenhintergrund (#05070d) -- die Kacheln
|
||||
sollen als Flaechen erkennbar sein, nicht im Dunkeln verschwinden.
|
||||
Gemessen: Seite liegt bei RGB(5,7,13), die Kachel bei RGB(38,49,67) --
|
||||
der erste Versuch mit (27,36,51) war zu zaghaft, da kam der sichtbare
|
||||
Gewinn fast nur vom Rand. */
|
||||
background:
|
||||
linear-gradient(#111825, #0c1320) padding-box,
|
||||
linear-gradient(#263143, #1c2637) padding-box,
|
||||
linear-gradient(150deg,
|
||||
color-mix(in srgb, var(--w) 42%, transparent),
|
||||
rgba(255, 255, 255, .06) 55%,
|
||||
color-mix(in srgb, var(--w) 16%, transparent)) border-box;
|
||||
transition: transform .16s ease, box-shadow .2s ease;
|
||||
color-mix(in srgb, var(--w) 62%, transparent),
|
||||
rgba(255, 255, 255, .12) 55%,
|
||||
color-mix(in srgb, var(--w) 26%, transparent)) border-box;
|
||||
transition: transform .16s ease, box-shadow .2s ease, background .16s ease;
|
||||
}
|
||||
.kachel::after {
|
||||
content: ""; position: absolute; left: 14px; right: 14px; top: 1px; height: 1px;
|
||||
@@ -91,20 +96,24 @@
|
||||
}
|
||||
.kachel:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px -12px color-mix(in srgb, var(--w) 85%, transparent);
|
||||
background:
|
||||
linear-gradient(#2f3b50, #232e42) padding-box,
|
||||
linear-gradient(150deg, var(--w), rgba(255, 255, 255, .16) 55%,
|
||||
color-mix(in srgb, var(--w) 40%, transparent)) border-box;
|
||||
box-shadow: 0 8px 26px -11px color-mix(in srgb, var(--w) 90%, transparent);
|
||||
}
|
||||
.kachel:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--w) 32%, transparent);
|
||||
}
|
||||
.kachel[data-leer="ja"] { opacity: .55; }
|
||||
.kachel[data-leer="ja"] { opacity: .82; }
|
||||
.kachel[data-leer="ja"]:hover { opacity: 1; }
|
||||
|
||||
.kachel__ring {
|
||||
flex: none; display: grid; place-items: center;
|
||||
width: 42px; height: 42px; border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--w) 13%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--w) 30%, transparent);
|
||||
background: color-mix(in srgb, var(--w) 22%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--w) 45%, transparent);
|
||||
}
|
||||
.kachel__symbol {
|
||||
width: 21px; height: 21px;
|
||||
@@ -116,14 +125,16 @@
|
||||
.kachel__beschreibung {
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
||||
overflow: hidden; margin-top: 3px;
|
||||
font-size: .78rem; color: var(--text-still); line-height: 1.45;
|
||||
/* War --text-still (gemessen 3,6:1 auf der neuen Flaeche) -- zu blass
|
||||
fuer laufenden Text. --text-leise liegt bei 6,4:1. */
|
||||
font-size: .79rem; color: var(--text-leise); line-height: 1.45;
|
||||
}
|
||||
.kachel__zahl {
|
||||
flex: none; min-width: 26px; padding: 2px 8px; align-self: flex-start;
|
||||
text-align: center; font-size: .75rem; font-weight: 700;
|
||||
color: color-mix(in srgb, var(--w) 55%, #ffffff);
|
||||
background: color-mix(in srgb, var(--w) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--w) 26%, transparent);
|
||||
color: color-mix(in srgb, var(--w) 40%, #ffffff);
|
||||
background: color-mix(in srgb, var(--w) 20%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--w) 40%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,54 @@
|
||||
}
|
||||
const datum = (iso) => (iso ? iso.slice(0, 10).split('-').reverse().join('.') : '—');
|
||||
|
||||
/* ---------- Wer darf die Datei sehen ------------------------------------
|
||||
Kleine Schalter statt einer Mehrfachliste: In einer <select multiple>
|
||||
verliert man mit einem Fehlklick die ganze Auswahl, und auf dem Handy
|
||||
ist sie kaum bedienbar. Hier ist jeder Name ein eigener Schalter. */
|
||||
|
||||
let empfaenger = [];
|
||||
let dartFreigeben = false;
|
||||
|
||||
function baueWahl() {
|
||||
const ziel = $('wahl');
|
||||
if (!ziel) return;
|
||||
const vorher = new Set([...ziel.querySelectorAll('.wahl__knopf[data-an="ja"]')]
|
||||
.map((b) => b.dataset.id));
|
||||
ziel.textContent = '';
|
||||
if (!empfaenger.length) {
|
||||
ziel.append(el('p', 'leise', dartFreigeben
|
||||
? 'Es gibt noch niemanden, dem du eine Datei geben könntest.'
|
||||
: ''));
|
||||
return;
|
||||
}
|
||||
for (const p of empfaenger) {
|
||||
const b = el('button', 'wahl__knopf');
|
||||
b.type = 'button';
|
||||
b.dataset.id = String(p.id);
|
||||
b.dataset.rolle = p.rolle;
|
||||
if (vorher.has(String(p.id))) b.dataset.an = 'ja';
|
||||
b.append(el('span', 'wahl__name', p.name));
|
||||
b.append(el('span', 'wahl__rolle', ROLLENKURZ[p.rolle] || p.rolle));
|
||||
b.addEventListener('click', () => {
|
||||
if (b.dataset.an === 'ja') delete b.dataset.an; else b.dataset.an = 'ja';
|
||||
wahlHinweis();
|
||||
});
|
||||
ziel.append(b);
|
||||
}
|
||||
wahlHinweis();
|
||||
}
|
||||
|
||||
function wahlHinweis() {
|
||||
const n = document.querySelectorAll('.wahl__knopf[data-an="ja"]').length;
|
||||
const h = $('wahl-hinweis');
|
||||
if (!h) return;
|
||||
h.textContent = n
|
||||
? `${n} ${n === 1 ? 'Person' : 'Personen'} ausgewählt – nur sie sehen die Datei.`
|
||||
: 'Niemand ausgewählt – dann sieht sie nur DogFather.';
|
||||
}
|
||||
|
||||
const ROLLENKURZ = { admin: 'DogFather', creator: 'Creator', scout: 'Scout' };
|
||||
|
||||
/* ---------- Hochladen -------------------------------------------------- */
|
||||
|
||||
async function schicke(datei) {
|
||||
@@ -55,6 +103,10 @@
|
||||
'X-Dateiname': encodeURIComponent(datei.name),
|
||||
};
|
||||
if (ich.rolle === 'admin' && $('f-creator').value) kopf['X-Creator'] = $('f-creator').value;
|
||||
/* Wer die Datei sehen darf -- als Liste von Ids im Kopf. */
|
||||
const gewaehlt = [...document.querySelectorAll('.wahl__knopf[data-an="ja"]')]
|
||||
.map((b) => b.dataset.id);
|
||||
if (gewaehlt.length) kopf['X-Personen'] = gewaehlt.join(',');
|
||||
|
||||
const a = await hole('/workspace/api/dateien', { method: 'POST', headers: kopf, body: datei });
|
||||
if (!a.ok) {
|
||||
@@ -62,7 +114,11 @@
|
||||
melde(d.fehler || 'Hochladen hat nicht geklappt.');
|
||||
return;
|
||||
}
|
||||
/* Auswahl nach dem Hochladen loeschen -- sonst bekaeme die
|
||||
naechste Datei stillschweigend dasselbe Publikum. */
|
||||
for (const b of document.querySelectorAll('.wahl__knopf[data-an="ja"]')) delete b.dataset.an;
|
||||
await laden();
|
||||
wahlHinweis();
|
||||
} catch { /* umgeleitet */ } finally {
|
||||
$('fortschritt').hidden = true;
|
||||
$('feld-datei').value = '';
|
||||
@@ -170,14 +226,77 @@
|
||||
}
|
||||
if (d.notiz) zeile2.append(el('span', null, '· ' + d.notiz));
|
||||
k.append(zeile2);
|
||||
|
||||
/* Wer sie sehen darf -- sichtbar fuer alle, damit niemand raten
|
||||
muss, wer eine Datei schon hat. */
|
||||
const frei = el('div', 'datei__freigaben');
|
||||
if (d.freigaben?.length) {
|
||||
frei.append(el('span', 'datei__schild', 'Sichtbar für'));
|
||||
for (const p of d.freigaben) frei.append(el('span', 'marke-person', p.name));
|
||||
} else {
|
||||
frei.append(el('span', 'datei__schild', 'Sichtbar für'));
|
||||
frei.append(el('span', 'marke-person marke-person--leer', 'niemanden zugeteilt'));
|
||||
}
|
||||
|
||||
if (dartFreigeben && empfaenger.length) {
|
||||
const knopf = el('button', 'knopf-still', 'Ändern');
|
||||
knopf.type = 'button';
|
||||
knopf.addEventListener('click', () => freigabenBearbeiten(k, d));
|
||||
frei.append(knopf);
|
||||
}
|
||||
k.append(frei);
|
||||
return k;
|
||||
}
|
||||
|
||||
/* Auswahl direkt an der Datei -- ohne Seitenwechsel und ohne Fenster,
|
||||
das den Rest verdeckt. */
|
||||
function freigabenBearbeiten(karte, d) {
|
||||
if (karte.querySelector('.datei__wahl')) return;
|
||||
const box = el('div', 'datei__wahl');
|
||||
const jetzt = new Set((d.freigaben || []).map((p) => p.id));
|
||||
for (const p of empfaenger) {
|
||||
const b = el('button', 'wahl__knopf');
|
||||
b.type = 'button';
|
||||
b.dataset.id = String(p.id);
|
||||
b.dataset.rolle = p.rolle;
|
||||
if (jetzt.has(p.id)) b.dataset.an = 'ja';
|
||||
b.append(el('span', 'wahl__name', p.name));
|
||||
b.append(el('span', 'wahl__rolle', ROLLENKURZ[p.rolle] || p.rolle));
|
||||
b.addEventListener('click', () => {
|
||||
if (b.dataset.an === 'ja') delete b.dataset.an; else b.dataset.an = 'ja';
|
||||
});
|
||||
box.append(b);
|
||||
}
|
||||
const knoepfe = el('div', 'datei__knoepfe');
|
||||
const speichern = el('button', 'knopf knopf--klein', 'Übernehmen');
|
||||
speichern.type = 'button';
|
||||
speichern.addEventListener('click', async () => {
|
||||
speichern.disabled = true;
|
||||
const ids = [...box.querySelectorAll('.wahl__knopf[data-an="ja"]')].map((b) => Number(b.dataset.id));
|
||||
const a = await hole(`/workspace/api/dateien/${d.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ freigaben: ids }),
|
||||
});
|
||||
speichern.disabled = false;
|
||||
if (!a.ok) { melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.'); return; }
|
||||
await laden();
|
||||
});
|
||||
const abbrechen = el('button', 'knopf-still', 'Abbrechen');
|
||||
abbrechen.type = 'button';
|
||||
abbrechen.addEventListener('click', () => { box.remove(); knoepfe.remove(); });
|
||||
knoepfe.append(speichern, abbrechen);
|
||||
karte.append(box, knoepfe);
|
||||
}
|
||||
|
||||
async function laden() {
|
||||
try {
|
||||
const a = await hole('/workspace/api/dateien');
|
||||
if (!a.ok) { melde('Dateien konnten nicht geladen werden.'); return; }
|
||||
const daten = await a.json();
|
||||
empfaenger = daten.empfaenger || [];
|
||||
dartFreigeben = !!daten.darf_freigeben;
|
||||
baueWahl();
|
||||
maxBytes = daten.max_bytes || maxBytes;
|
||||
$('ablage-grenze').textContent = 'bis ' + groesse(maxBytes);
|
||||
|
||||
@@ -236,6 +355,9 @@
|
||||
$('f-creator').append(o);
|
||||
}
|
||||
} catch { /* Zuordnung bleibt leer */ }
|
||||
} else if (ich.rolle === 'scout') {
|
||||
$('zuordnung').hidden = false;
|
||||
$('f-creator').closest('.feld').hidden = true;
|
||||
}
|
||||
await laden();
|
||||
})();
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -134,7 +134,7 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/calls.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/calls.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+17
-8
@@ -7,10 +7,10 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -61,8 +61,17 @@
|
||||
</div>
|
||||
|
||||
<div class="hochlade-zeile" id="zuordnung" hidden>
|
||||
<label class="feld-schild" for="f-creator">Ablegen im Bereich von</label>
|
||||
<select id="f-creator"><option value="">— kein Bereich —</option></select>
|
||||
<div class="feld feld--breit">
|
||||
<label class="feld-schild" for="f-creator">Ablegen im Bereich von <span class="leise">(optional)</span></label>
|
||||
<select id="f-creator"><option value="">— kein Bereich —</option></select>
|
||||
</div>
|
||||
<!-- Wer die Datei sehen darf. Mehrfachauswahl: dieselbe Datei kann
|
||||
an mehrere Personen gehen, ohne sie mehrfach hochzuladen. -->
|
||||
<div class="feld feld--breit">
|
||||
<label class="feld-schild">Wer darf sie sehen?</label>
|
||||
<p class="leise" id="wahl-hinweis">Niemand ausgewählt – dann sieht sie nur DogFather.</p>
|
||||
<div class="wahl" id="wahl"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="fortschritt" id="fortschritt" hidden><span id="fortschritt-text"></span></p>
|
||||
@@ -79,7 +88,7 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
type="image/webp" media="(max-width: 899px)" />
|
||||
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="gate">
|
||||
@@ -130,6 +130,6 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script src="assets/js/gate.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/gate.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/personen.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/personen.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/profil.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/profil.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/report.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/report.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -124,7 +124,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -56,7 +56,7 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/start.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/start.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202608281303" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202608281314" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202608281314" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -145,7 +145,7 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202608281303" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202608281314" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202608281314" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user