Compare commits

...
3 Commits
15 changed files with 208 additions and 34 deletions

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm" name = "tesm"
version = "2.7.2" version = "2.7.4"
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
+19 -3
View File
@@ -272,6 +272,19 @@ def save_form(token: str):
if upload_file is None or not upload_file.filename: if upload_file is None or not upload_file.filename:
return {"error": "Es kam keine Datei an."}, 400 return {"error": "Es kam keine Datei an."}, 400
# Ein unterschriebenes Dokument ist abgeschlossen. Es als Vorlage fuer eine
# neue Fassung zu benutzen hiesse, eine Unterschrift unter einen anderen
# Text zu stellen -- die Oberflaeche blendet das aus, hier wird es
# abgewiesen.
if service.signaturstand(db(), mount, relative).get("status") in {
"unveraendert",
"veraendert",
}:
return {
"error": "Dieses Dokument ist unterschrieben und damit abgeschlossen. "
"Legen Sie eine neue Fassung aus der ursprünglichen Vorlage an."
}, 409
grenze = uploads.limit_bytes(db()) grenze = uploads.limit_bytes(db())
daten = upload_file.stream.read(grenze + 1) daten = upload_file.stream.read(grenze + 1)
if len(daten) > grenze: if len(daten) > grenze:
@@ -343,10 +356,13 @@ def save_form(token: str):
# da: der Name **in** der Unterschrift ist eine Angabe des # da: der Name **in** der Unterschrift ist eine Angabe des
# Unterschreibenden -- wie auf Papier. Wer die Datei abgelegt hat, steht # Unterschreibenden -- wie auf Papier. Wer die Datei abgelegt hat, steht
# ohnehin als Handelnder in der Zeile und ist belegt. # ohnehin als Handelnder in der Zeile und ist belegt.
beschreibung = f"{ziel_mount.label}:{wohin} sha256={pruefsumme}" # Zwei Zeilen: oben, was ein Mensch liest; unten die Pruefsumme, die
# niemand liest, aber jeder vergleichen koennen muss. In einer einzigen
# Zeile drueckte der Hash den Rest aus der Spalte.
kopf = f"{'Signiert' if unterschriften else 'Gespeichert'}: {wohin} ({ziel_mount.label})"
if unterschriften: if unterschriften:
namen = ", ".join(unterschriften) kopf += f" – angegeben von: {', '.join(unterschriften)}"
beschreibung = f"{beschreibung} -- angegeben unterschrieben von: {namen}" beschreibung = f"{kopf}{chr(10)}sha256={pruefsumme}"
_audit( _audit(
"fileshare.form_signed" if unterschriften else "fileshare.form_saved", "fileshare.form_signed" if unterschriften else "fileshare.form_saved",
f"{mount.label}:{relative}", f"{mount.label}:{relative}",
+4 -1
View File
@@ -696,7 +696,10 @@ def signaturdetail_lesen(detail: str) -> dict[str, Any]:
von: <Namen>``. Gelesen wird sie an zwei Stellen -- beim Uebernehmen von: <Namen>``. Gelesen wird sie an zwei Stellen -- beim Uebernehmen
alter Eintraege und wenn das Verzeichnis nichts weiss. alter Eintraege und wenn das Verzeichnis nichts weiss.
""" """
ort, _, rest = detail.partition(" sha256=") # Der Trenner ist heute ein Zeilenumbruch, frueher ein Leerzeichen -- die
# Zerlegung muss beides vertragen, sonst sind alte Eintraege stumm.
ort, _, rest = detail.partition("sha256=")
ort = ort.strip()
label, _, pfad = ort.partition(":") label, _, pfad = ort.partition(":")
summe = rest.split(" ", 1)[0].strip() if rest else "" summe = rest.split(" ", 1)[0].strip() if rest else ""
return { return {
+49
View File
@@ -64,6 +64,13 @@ CATEGORIES: tuple[Category, ...] = (
Category("users", "Benutzer", "Lokale Konten mit Passwort-Hash", admin_only=True), Category("users", "Benutzer", "Lokale Konten mit Passwort-Hash", admin_only=True),
Category("groups", "Gruppen und Rechte", "Rechtematrix", admin_only=True), Category("groups", "Gruppen und Rechte", "Rechtematrix", admin_only=True),
Category("settings", "Einstellungen", "Laufzeiteinstellungen der Anwendung", admin_only=True), Category("settings", "Einstellungen", "Laufzeiteinstellungen der Anwendung", admin_only=True),
Category(
"signatures",
"Unterschriften",
"Pruefsummen unterschriebener Dokumente. Ohne sie sagt die Vorschau nach einer "
"Wiederherstellung zu jeder alten Fassung \"unbekannt\" – auch zu einer "
"veraenderten.",
),
) )
CATEGORY_BY_KEY = {category.key: category for category in CATEGORIES} CATEGORY_BY_KEY = {category.key: category for category in CATEGORIES}
@@ -186,6 +193,16 @@ def build_export(
# Sicherung -- sonst wuerde ein Restore eine fremde Aktivierung erben. # Sicherung -- sonst wuerde ein Restore eine fremde Aktivierung erben.
if not str(row["key"]).startswith("license_") if not str(row["key"]).startswith("license_")
} }
if "signatures" in wanted:
# Das Verzeichnis der Unterschriften. Der Beleg selbst bleibt das
# Aenderungsprotokoll (Hash-Kette) und das Journal neben dem Archiv --
# beides wandert nicht in die Sicherung. Ohne diese Zeilen waere nach
# einer Wiederherstellung jede unterschriebene Fassung "unbekannt".
payload["signatures"] = Database.all(
conn,
"SELECT sha256, share_label, path, signed_at, actor, names "
"FROM document_signatures ORDER BY id",
)
return payload return payload
@@ -625,6 +642,38 @@ def apply_import(
(user_id, int(group_id)), (user_id, int(group_id)),
) )
if "signatures" in wanted:
for eintrag in payload.get("signatures") or []:
if not isinstance(eintrag, dict):
continue
summe = str(eintrag.get("sha256") or "").strip()
pfad = str(eintrag.get("path") or "").strip()
if not summe:
continue
# Zweimal einlesen darf nichts verdoppeln: verglichen wird ueber
# Pruefsumme und Ablageort, genau wie beim Journal.
vorhanden = Database.value(
conn,
"SELECT 1 FROM document_signatures WHERE sha256=? AND path=? LIMIT 1",
(summe, pfad),
)
if vorhanden:
bump("signatures", "skipped")
continue
conn.execute(
"INSERT INTO document_signatures (sha256, share_label, path, signed_at, "
"actor, names, audit_id) VALUES (?,?,?,?,?,?,NULL)",
(
summe,
str(eintrag.get("share_label") or ""),
pfad,
str(eintrag.get("signed_at") or stamp),
str(eintrag.get("actor") or ""),
str(eintrag.get("names") or ""),
),
)
bump("signatures", "created")
if "settings" in wanted: if "settings" in wanted:
for key, value in (payload.get("settings") or {}).items(): for key, value in (payload.get("settings") or {}).items():
if str(key).startswith("license_"): if str(key).startswith("license_"):
+5 -4
View File
@@ -104,10 +104,11 @@ TESM_SETTINGS: tuple[SettingSpec, ...] = (
True, True,
"bool", "bool",
"Unterschriften mit dem ablegenden Konto bezeugen", "Unterschriften mit dem ablegenden Konto bezeugen",
hint="Unter jede Unterschrift kommt eine Zeile, wer das Dokument in TESM abgelegt " hint="Eine Zeile unter der **ersten** Unterschrift nennt das Konto, das das "
"hat. Der Name in der Unterschrift ist eine Angabe des Unterschreibenden – wie " "Dokument in TESM abgelegt hat. In den Dokumenteigenschaften und im "
"auf Papier; das Konto daneben ist belegt. Eine Passwortabfrage gibt es dafür " "Änderungsprotokoll steht es ohnehin, unabhängig von diesem Schalter. Der Name in "
"nicht.", "der Unterschrift ist eine Angabe des Unterschreibenden – wie auf Papier; das "
"Konto ist belegt. Eine Passwortabfrage gibt es dafür nicht.",
section="fileshare", section="fileshare",
), ),
# -- Terminal ----------------------------------------------------------- # # -- Terminal ----------------------------------------------------------- #
@@ -84,7 +84,9 @@
<div class="card__title">{{ icon('pencil', size=17) }} Unterschriften</div> <div class="card__title">{{ icon('pencil', size=17) }} Unterschriften</div>
<div class="card__sub"> <div class="card__sub">
Der Name in der Unterschrift ist eine Angabe des Unterschreibenden – wie auf Der Name in der Unterschrift ist eine Angabe des Unterschreibenden – wie auf
Papier. Wer das Dokument in TESM abgelegt hat, ist dagegen belegt. Papier. Wer das Dokument abgelegt hat, ist dagegen belegt: in den
Dokumenteigenschaften und im Änderungsprotokoll, auf Wunsch zusätzlich
sichtbar unter der ersten Unterschrift.
</div> </div>
</div> </div>
</div> </div>
+2 -2
View File
@@ -542,8 +542,8 @@ if [[ "$APP" == "tesm" ]]; then
render_unit "$ROOT/deploy/systemd/tesm-monitor.service" "/etc/systemd/system/$MONITOR_UNIT" render_unit "$ROOT/deploy/systemd/tesm-monitor.service" "/etc/systemd/system/$MONITOR_UNIT"
fi fi
# Aufraeumen lief bisher nur von Hand -- also praktisch nie. Auf einer # Aufraeumen lief bisher nur von Hand -- also praktisch nie. Auf einer
# laufenden Installation standen deshalb hunderte abgelaufene Sitzungszeilen # laufenden Installation lagen deshalb hunderte abgelaufene Sitzungszeilen in
# in der Datenbank, und das Aenderungsprotokoll waere nie archiviert worden. # der Datenbank, und das Aenderungsprotokoll waere nie archiviert worden.
render_unit "$ROOT/deploy/systemd/$APP-cleanup.service" "/etc/systemd/system/$CLEANUP_UNIT" render_unit "$ROOT/deploy/systemd/$APP-cleanup.service" "/etc/systemd/system/$CLEANUP_UNIT"
render_unit "$ROOT/deploy/systemd/$APP-cleanup.timer" "/etc/systemd/system/$CLEANUP_TIMER" render_unit "$ROOT/deploy/systemd/$APP-cleanup.timer" "/etc/systemd/system/$CLEANUP_TIMER"
[[ $UNITS_CHANGED -eq 1 ]] && systemctl daemon-reload && info "Units neu geladen" \ [[ $UNITS_CHANGED -eq 1 ]] && systemctl daemon-reload && info "Units neu geladen" \
+2 -2
View File
@@ -6,8 +6,8 @@ Description=TESM -- taegliches Aufraeumen
# und die Archivierung des Aenderungsprotokolls. # und die Archivierung des Aenderungsprotokolls.
# #
# Vorher lief das nur, wenn jemand "tesm-admin housekeeping" aufrief oder in # Vorher lief das nur, wenn jemand "tesm-admin housekeeping" aufrief oder in
# der Diagnose darauf klickte -- also praktisch nie. Gemessen: 444 offene # der Diagnose darauf klickte -- also praktisch nie. Gemessen auf einer
# Sitzungszeilen bei drei Benutzern. # Installation nach vier Wochen: 747 Sitzungszeilen, 682 davon abgelaufen.
OnCalendar=*-*-* 03:30:00 OnCalendar=*-*-* 03:30:00
# Streuung, damit nicht jede Installation im Netz zur selben Sekunde loslegt. # Streuung, damit nicht jede Installation im Netz zur selben Sekunde loslegt.
RandomizedDelaySec=15m RandomizedDelaySec=15m
+2 -2
View File
@@ -6,8 +6,8 @@ Description=TESM-Lizenzserver -- taegliches Aufraeumen
# und die Archivierung des Aenderungsprotokolls. # und die Archivierung des Aenderungsprotokolls.
# #
# Vorher lief das nur, wenn jemand "tesm-license-admin housekeeping" aufrief oder in # Vorher lief das nur, wenn jemand "tesm-license-admin housekeeping" aufrief oder in
# der Diagnose darauf klickte -- also praktisch nie. Gemessen: 444 offene # der Diagnose darauf klickte -- also praktisch nie. Gemessen auf einer
# Sitzungszeilen bei drei Benutzern. # Installation nach vier Wochen: 747 Sitzungszeilen, 682 davon abgelaufen.
OnCalendar=*-*-* 03:30:00 OnCalendar=*-*-* 03:30:00
# Streuung, damit nicht jede Installation im Netz zur selben Sekunde loslegt. # Streuung, damit nicht jede Installation im Netz zur selben Sekunde loslegt.
RandomizedDelaySec=15m RandomizedDelaySec=15m
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-core" name = "tesm-core"
version = "2.7.2" version = "2.7.4"
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver" description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
@@ -148,7 +148,7 @@ def create_base_app(
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide #: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
#: Anwendungen. #: Anwendungen.
auth_tagline: str = "", auth_tagline: str = "",
app_version: str = "2.7.2", app_version: str = "2.7.4",
built_in_trusted_keys: tuple[str, ...] = (), built_in_trusted_keys: tuple[str, ...] = (),
enable_license: bool = True, enable_license: bool = True,
template_folder: str | None = None, template_folder: str | None = None,
@@ -1486,7 +1486,7 @@
font-size: 5.6cqw; font-size: 5.6cqw;
line-height: 1.15; line-height: 1.15;
color: #111; color: #111;
border-top: 1px solid rgba(0, 0, 0, 0.35); border-top: 1px solid rgba(0, 0, 0, 0.85);
padding-top: 1px; padding-top: 1px;
pointer-events: none; pointer-events: none;
white-space: nowrap; white-space: nowrap;
@@ -1581,3 +1581,62 @@
color: var(--danger); color: var(--danger);
background: rgba(201, 42, 42, 0.12); background: rgba(201, 42, 42, 0.12);
} }
/* -- Tabelle, die schmal zu Bloecken wird -------------------------------- */
/* Eine Protokollzeile besteht aus sieben Angaben, von denen eine lang ist:
das Detail. Auf einem breiten Schirm passt das nebeneinander. Wird es eng,
ist eine Spalte von zwei Zentimetern fuer eine Pruefsumme keine Loesung --
dann steht jede Angabe auf ihrer eigenen Zeile, beschriftet. */
.table--stapelbar .td--detail {
/* Zeilenumbrueche aus dem Eintrag bleiben stehen: die Pruefsumme steht
unter dem lesbaren Teil, nicht mitten darin. Und ein Hash ohne Trennstelle
darf notfalls hart umbrechen. */
white-space: pre-line;
overflow-wrap: anywhere;
max-width: 52ch;
}
@media (max-width: 1100px) {
.table--stapelbar thead {
display: none;
}
.table--stapelbar,
.table--stapelbar tbody,
.table--stapelbar tr,
.table--stapelbar td {
display: block;
width: auto;
}
.table--stapelbar tr {
padding: var(--sp-3) 0;
border-bottom: 1px solid var(--border);
}
.table--stapelbar td {
display: grid;
grid-template-columns: 8.5rem 1fr;
gap: var(--sp-2);
align-items: baseline;
border: 0;
padding: 2px var(--sp-3);
white-space: normal;
}
.table--stapelbar td::before {
content: attr(data-spalte);
color: var(--text-muted);
font-size: var(--fs-xs);
}
.table--stapelbar .td--detail {
max-width: none;
}
/* Leere Zellen brauchen keine Zeile. */
.table--stapelbar td:empty {
display: none;
}
}
@@ -280,7 +280,13 @@ async function nachbearbeiten(
const hoehe = (png.height / png.width) * breite; const hoehe = (png.height / png.width) * breite;
seite.drawImage(png, { x: eintrag.x, y: eintrag.y, width: breite, height: hoehe }); seite.drawImage(png, { x: eintrag.x, y: eintrag.y, width: breite, height: hoehe });
const zeilen = [`${eintrag.name} | ${eintrag.zeit}`]; const zeilen = [`${eintrag.name} | ${eintrag.zeit}`];
if (zeuge) zeilen.push(`In TESM abgelegt durch ${zeuge}`); // Der Zeugenvermerk gehoert zum Dokument, nicht zu jeder Unterschrift:
// unter drei Unterschriften stand dreimal derselbe Satz. Sichtbar
// erscheint er deshalb nur unter der ersten -- fest steht er ohnehin in
// den Dokumenteigenschaften und im Aenderungsprotokoll.
if (zeuge && eintrag === unterschriften[0]) {
zeilen.push(`In TESM abgelegt durch ${zeuge}`);
}
// Die Schrift waechst mit der Unterschrift: wer sie kleiner zieht, will // Die Schrift waechst mit der Unterschrift: wer sie kleiner zieht, will
// den ganzen Block kleiner haben, nicht eine Zeichnung mit einer // den ganzen Block kleiner haben, nicht eine Zeichnung mit einer
// Bildunterschrift in Originalgroesse daneben. // Bildunterschrift in Originalgroesse daneben.
@@ -302,15 +308,32 @@ async function nachbearbeiten(
color: PDFLib.rgb(0.1, 0.1, 0.1), color: PDFLib.rgb(0.1, 0.1, 0.1),
}); });
} }
// Kraeftig genug, um neben den Linien eines Formulars als eigener Strich
// erkennbar zu bleiben: ein halber Punkt in hellem Grau ging zwischen
// den Tabellenlinien schlicht unter.
seite.drawLine({ seite.drawLine({
start: { x: eintrag.x, y: eintrag.y - 1 }, start: { x: eintrag.x, y: eintrag.y - 1.5 },
end: { x: eintrag.x + textbreite, y: eintrag.y - 1 }, end: { x: eintrag.x + textbreite, y: eintrag.y - 1.5 },
thickness: 0.5, thickness: 0.9,
color: PDFLib.rgb(0.4, 0.4, 0.4), color: PDFLib.rgb(0.15, 0.15, 0.15),
}); });
} }
} }
if (unterschriften.length) {
// In den Dokumenteigenschaften -- dort sucht man so etwas, und dort steht
// es einmal statt unter jeder Unterschrift. Jeder Betrachter zeigt das
// an, auch ausserhalb von TESM.
const namen = unterschriften.map((eintrag) => eintrag.name);
const wann = unterschriften[unterschriften.length - 1].zeit;
dokument.setAuthor(namen.join(", "));
dokument.setSubject(
`Unterschrieben am ${wann}` + (zeuge ? ` | in TESM abgelegt durch ${zeuge}` : "")
);
dokument.setProducer("TESM");
dokument.setModificationDate(new Date());
}
if (abschliessen) { if (abschliessen) {
// Alle Felder werden Teil der Seite. Danach gibt es kein Formular mehr -- // Alle Felder werden Teil der Seite. Danach gibt es kein Formular mehr --
// gewollt: so kann niemand mehr etwas verstellen. // gewollt: so kann niemand mehr etwas verstellen.
@@ -664,7 +687,9 @@ async function pdfBetrachter(behaelter, seiten, url, basis, { formulare = true }
const text = document.createElement("div"); const text = document.createElement("div");
text.className = "viewer__unterschrifttext"; text.className = "viewer__unterschrifttext";
text.textContent = `${eintrag.name} | ${eintrag.zeit}`; text.textContent = `${eintrag.name} | ${eintrag.zeit}`;
if (zeuge) text.textContent += ` / In TESM abgelegt durch ${zeuge}`; if (zeuge && eintrag === unterschriften[0]) {
text.textContent += ` / In TESM abgelegt durch ${zeuge}`;
}
const griff = document.createElement("button"); const griff = document.createElement("button");
griff.type = "button"; griff.type = "button";
griff.className = "viewer__unterschriftgriff"; griff.className = "viewer__unterschriftgriff";
@@ -834,6 +859,8 @@ export function initFilePreview(wurzel = document) {
const padAbbrechen = stueck("signabbrechen"); const padAbbrechen = stueck("signabbrechen");
const padHinweis = stueck("signhinweis"); const padHinweis = stueck("signhinweis");
const basis = rumpf.dataset.viewerPdfjs || ""; const basis = rumpf.dataset.viewerPdfjs || "";
// Der Anmeldename, nicht der volle Name: er ist eindeutig, und genau das
// soll die Zeile belegen.
const konto = rumpf.dataset.viewerKonto || ""; const konto = rumpf.dataset.viewerKonto || "";
const bezeugen = rumpf.dataset.viewerZeuge === "1"; const bezeugen = rumpf.dataset.viewerZeuge === "1";
// Die Uhr des Servers, nicht die des Tablets: eine verstellte Uhr am Geraet // Die Uhr des Servers, nicht die des Tablets: eine verstellte Uhr am Geraet
@@ -877,6 +904,7 @@ export function initFilePreview(wurzel = document) {
leiste.hidden = art === "text"; leiste.hidden = art === "text";
if (speichern) speichern.hidden = art !== "pdf"; if (speichern) speichern.hidden = art !== "pdf";
if (unterschreibenKnopf) unterschreibenKnopf.hidden = art !== "pdf"; if (unterschreibenKnopf) unterschreibenKnopf.hidden = art !== "pdf";
if (speichern) speichern.hidden = art !== "pdf";
if (unterschriftPanel) unterschriftPanel.hidden = true; if (unterschriftPanel) unterschriftPanel.hidden = true;
}; };
@@ -918,6 +946,15 @@ export function initFilePreview(wurzel = document) {
if (!adresse) return; if (!adresse) return;
try { try {
const stand = await window.tesm.api(adresse); const stand = await window.tesm.api(adresse);
if (stand.status === "unveraendert" || stand.status === "veraendert") {
// Ein unterschriebenes Dokument ist abgeschlossen. Wer daraus eine
// neue Fassung machen koennte, koennte auch eine Unterschrift unter
// einen veraenderten Text setzen -- also gibt es hier nichts mehr zu
// bearbeiten. Der Server weist es zusaetzlich ab.
if (unterschreibenKnopf) unterschreibenKnopf.hidden = true;
if (speichern) speichern.hidden = true;
if (zeile) zeile.hidden = true;
}
if (stand.status === "unveraendert") { if (stand.status === "unveraendert") {
pdfBehaelter.classList.add("viewer__pdf--unveraendert"); pdfBehaelter.classList.add("viewer__pdf--unveraendert");
if (pruefzeile) { if (pruefzeile) {
@@ -44,8 +44,11 @@
<a class="btn btn--sm btn--ghost" href="{{ url_for('logs.audit_view') }}">Zurücksetzen</a> <a class="btn btn--sm btn--ghost" href="{{ url_for('logs.audit_view') }}">Zurücksetzen</a>
</form> </form>
{# "table--stapelbar": schmal wird aus jeder Zeile ein kleiner Block. Vorher
quetschte sich das Detail mit Pruefsumme in eine Spalte von zwei
Zentimetern und lief ueber zehn Zeilen -- lesbar war das nicht. #}
<div class="table-wrap"> <div class="table-wrap">
<table class="table table--compact" data-behavior="sortable"> <table class="table table--compact table--stapelbar" data-behavior="sortable">
<thead> <thead>
<tr> <tr>
<th class="sortable">Zeit</th> <th class="sortable">Zeit</th>
@@ -60,12 +63,16 @@
<tbody> <tbody>
{% for row in rows %} {% for row in rows %}
<tr> <tr>
<td class="fs-sm nowrap" data-sort-value="{{ row.ts }}">{{ row.ts | dt }}</td> <td class="fs-sm nowrap" data-spalte="Zeit" data-sort-value="{{ row.ts }}">
<td class="fs-sm fw-600">{{ row.actor }}</td> {{ row.ts | dt }}
<td class="mono fs-xs">{{ row.action }}</td> </td>
<td class="fs-sm">{{ row.target }}</td> <td class="fs-sm fw-600" data-spalte="Benutzer">{{ row.actor }}</td>
<td class="fs-sm muted break-all">{{ row.detail | audit_detail }}</td> <td class="mono fs-xs" data-spalte="Aktion">{{ row.action }}</td>
<td> <td class="fs-sm" data-spalte="Ziel">{{ row.target }}</td>
<td class="fs-sm muted break-all td--detail" data-spalte="Detail">
{{ row.detail | audit_detail }}
</td>
<td data-spalte="Stufe">
{# Ampel: Info grün, Hinweis gelb, Warnung rot. Vorher war Info {# Ampel: Info grün, Hinweis gelb, Warnung rot. Vorher war Info
grau und Hinweis blau -- dabei war nicht zu sehen, was harmlos grau und Hinweis blau -- dabei war nicht zu sehen, was harmlos
ist und was Aufmerksamkeit braucht. "kritisch" bekommt eine ist und was Aufmerksamkeit braucht. "kritisch" bekommt eine
@@ -76,7 +83,7 @@
{% elif row.severity == 'notice' %}{{ pill('Hinweis', 'warning') }} {% elif row.severity == 'notice' %}{{ pill('Hinweis', 'warning') }}
{% else %}{{ pill('Info', 'online') }}{% endif %} {% else %}{{ pill('Info', 'online') }}{% endif %}
</td> </td>
<td class="mono fs-xs faint">{{ row.ip or '&ndash;' | safe }}</td> <td class="mono fs-xs faint" data-spalte="Herkunft">{{ row.ip or '&ndash;' | safe }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-licensing" name = "tesm-licensing"
version = "2.7.2" version = "2.7.4"
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver" description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = ["cryptography>=42"] dependencies = ["cryptography>=42"]