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:
2026-08-28 13:14:11 +02:00
parent c0ac041122
commit 1f217041ae
18 changed files with 402 additions and 94 deletions
+99 -8
View File
@@ -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 = [];
+14
View File
@@ -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
+61
View File
@@ -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; } }
+24 -13
View File
@@ -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;
}
+122
View File
@@ -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();
})();
+5 -5
View File
@@ -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>
+6 -6
View File
@@ -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>
+6 -6
View File
@@ -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
View File
@@ -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>
+2 -2
View File
@@ -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>
+6 -6
View File
@@ -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>
+6 -6
View File
@@ -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>
+6 -6
View File
@@ -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>
+6 -6
View File
@@ -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>
+6 -6
View File
@@ -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>
+4 -4
View File
@@ -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>
+6 -6
View File
@@ -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>
+6 -6
View File
@@ -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>