Reaktion auf mehrere Bug-Reports und Feature-Wuensche zum bestehenden Ticket-System (v1.1.0): 'Logs gehen nicht', 'Ticketverwaltung fehlt', 'Lizenzen koennen nicht vom Dashboard geloescht werden', plus eine Reihe konkreter neuer Anforderungen. ## Bugfixes - /logs: behob einen Absturz (jinja2.exceptions.UndefinedError: 'global_check_interval' is undefined) UND die eigentliche Ursache dahinter -- logs.html war unveraendert aus dem TESM-Fork uebernommen (PoE-Geraete-Polling-Text/-Farblogik, ein Template-Feld, das app.py nie befuellt hat) und ausserdem schrieb auf diesem Server ueberhaupt nichts in live.log (kein poe.sh-Aequivalent vorhanden). Live-Log zeigt jetzt echte Ticket-/Lizenzaktionen, mit passendem Hinweistext und Faerbung. - Lizenz-Loeschung war bereits vorhanden, aber nur ueber Ticket- -> Lizenz-Detailseite erreichbar -- jetzt zusaetzlich inline in der Lizenzhistorie jedes Tickets. ## Ticketverwaltung (neu: eigene Seite /tickets) - Volle CRUD: Liste mit Suche, Bearbeiten (Typ/Module/Laufzeit/Lifetime, wirkt nur auf die naechste Ausstellung), Loeschen (nur wenn keine offene Lizenz mehr besteht, dann inkl. Historie). - Dashboard (/) zeigt seitdem NICHT mehr die Ticket-Tabelle, sondern eine rein informative Kachel-Uebersicht der aktuell AKTIVEN Lizenzen, exakt im Kachel-Stil von TESMs eigenem Geraete-Dashboard: Ampelfarben (gruen >90 Tage, orange 30-90, rot <30/abgelaufen), gruppiert, Firma + Hostname der Kundeninstanz. Hostname kommt neu vom TESM-Client per Activate/ Heartbeat (srv/tesm/licensing.py, abwaerts-kompatibel: alte Clients ohne das Feld ueberschreiben nie einen bereits bekannten Hostnamen). ## Kundenstammdaten - Neue Felder: Strasse/Hausnummer/PLZ/Ort/Ansprechpartner. E-Mail ist beim Anlegen jetzt Pflicht; beim Bearbeiten bewusst nur validiert, wenn ausgefuellt (sonst waeren Bestandskunden aus der Zeit vor dieser Umstellung fuer JEDE Aenderung blockiert gewesen). ## Kundenportal (neu, per Magic-Link, kein Login) - /portal/<token>: zeigt einem Kunden ALLE seine Tickets/Lizenzen auf einen Blick (ergaenzt den bestehenden Pro-Ticket-Link /self-service/<ticket_id>) und erlaubt ihm, seine eigenen Kontakt-/ Anschriftdaten selbst zu pflegen -- Firmenname und kommerzielle Ticket-Eckdaten bleiben bewusst admin-verwaltet. token = eigene Spalte license_customers.portal_token, rueckwirkend fuer Bestandskunden erzeugt. ## Lifetime-Lizenzen - Neues Ticket-Flag 'Lifetime': ignoriert die Laufzeit fuer jede REGULAERE Ausstellung (Sentinel-Ablaufdatum 2099-12-31, licensing. LIFETIME_EXPIRES_AT) -- eine zusaetzlich erzeugte Trial-Lizenz aus demselben Ticket bleibt immer die normale 30-Tage-Variante. Trial ALS TICKET-GRUNDTYP + Lifetime wird serverseitig verhindert (haette eine dauerhafte, nie ablaufende Lizenz ganz ohne Module ergeben). ## Absicherung nach Review Ein Adversarial-Review-Durchlauf ueber den kompletten Diff deckte vor dem Deploy zusaetzlich auf und wurde behoben: - TOCTOU-Race: zwei nahezu gleichzeitige 'Lizenz erstellen'-Anfragen fuer dasselbe Ticket (z.B. Doppelklick im Self-Service) konnten beide durchkommen. Jetzt durch einen partiellen Unique-Index (idx_one_open_license_per_ticket) auf DB-Ebene ausgeschlossen; die zweite Anfrage bekommt sauber die bereits erstellte Lizenz zurueck statt eine zweite zu erzeugen. - Analoger Unique-Index auf license_customers.portal_token als Backstop fuer den neuen Magic-Link. - tickets.html nutzte noch die alte, feste '<=30 Tage'-Schwelle statt der gemeinsamen Ampelfarbe -- Lizenzen im 30-90-Tage-Bereich blieben dort unmarkiert, obwohl das Dashboard sie schon als orange zeigte. SCHEMA_VERSION 2 -> 3 (neue Spalten/Tabellen, ueber install.sh mit Backup+Health-Check abgesichert). Verifiziert: vollstaendige Migration gegen eine echte Kopie der Live-Datenbank, anschliessend 22 Funktionstests direkt gegen die echten Routen (Validierung, Lifetime-Ausstellung, Sperr-/Kaskadenregeln bei Loeschungen, Kundenportal inkl. ungueltiger Eingaben) sowie ein Playwright- Seitenladetest ohne Konsolenfehler -- beides nach den Review-Fixes erneut komplett durchlaufen, inkl. direktem Nachweis, dass der neue Unique-Index eine doppelte offene Lizenz pro Ticket tatsaechlich auf DB-Ebene ablehnt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
120 lines
4.7 KiB
HTML
120 lines
4.7 KiB
HTML
{% extends "base.html" %}
|
|
{% set active_page = "index" %}
|
|
{% block page_title %}Dashboard{% endblock %}
|
|
{% block page_sub %}<div class="topbar-sub">Übersicht aktiver Kundeninstanzen</div>{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
{% if buckets is none %}
|
|
<div class="card card-pad" style="max-width:560px; margin:40px auto; text-align:center;">
|
|
<p class="text-faint" style="font-size:13px;">
|
|
{% if not current_user.is_authenticated %}Bitte anmelden.{% else %}Keine Berechtigung, Lizenzen anzusehen.{% endif %}
|
|
</p>
|
|
</div>
|
|
{% else %}
|
|
|
|
<div class="section-head">
|
|
<div>
|
|
<h2 style="font-size:16px;">Aktive Lizenzen</h2>
|
|
<div class="hint">
|
|
Rein informative Übersicht nach Restlaufzeit der Ablauffristen (grün > 90 Tage, orange 30-90 Tage, rot < 30 Tage). Tickets anlegen, bearbeiten oder Lizenzen ausstellen/widerrufen geschieht unter <a href="{{ url_for('tickets_list') }}" style="color:var(--accent-strong); font-weight:600;">Tickets</a>.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-row">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Rot (< 30 Tage)</div>
|
|
<div class="stat-value offline">{{ stats.red }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Orange (30-90 Tage)</div>
|
|
<div class="stat-value" style="color:var(--warning);">{{ stats.orange }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Grün (> 90 Tage / Lifetime)</div>
|
|
<div class="stat-value online">{{ stats.green }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Aktiv gesamt</div>
|
|
<div class="stat-value">{{ stats.total }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% macro license_tile(l) %}
|
|
{% set ds = {'red': 'offline', 'orange': 'unbekannt', 'green': 'online'}[l.color] %}
|
|
<div class="device-card is-readonly" data-status="{{ ds }}">
|
|
<div class="dc-top">
|
|
<div class="dc-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="16" r="1.5"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>
|
|
</div>
|
|
<span class="pill {{ {'red': 'offline', 'orange': 'unknown', 'green': 'online'}[l.color] }}">
|
|
{% if l.is_lifetime %}Lifetime{% elif l.is_expired %}Abgelaufen{% else %}{{ l.days_left }} Tage{% endif %}
|
|
</span>
|
|
</div>
|
|
<div class="dc-name">{{ l.customer_name }}</div>
|
|
<div class="dc-ip">{{ l.hostname or 'Hostname unbekannt' }}</div>
|
|
<div class="text-faint" style="font-size:11px; margin-top:2px;">{{ type_labels.get(l.type, l.type) }}</div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro section_chevron() %}<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>{% endmacro %}
|
|
|
|
{% if stats.total %}
|
|
|
|
{% if buckets.red %}
|
|
<div class="dash-section" data-status="red">
|
|
<div class="dash-section-title">{{ section_chevron() }}Rot <span class="pill offline">{{ buckets.red|length }}</span></div>
|
|
<div class="device-grid">
|
|
{% for l in buckets.red %}{{ license_tile(l) }}{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if buckets.orange %}
|
|
<div class="dash-section" data-status="orange">
|
|
<div class="dash-section-title">{{ section_chevron() }}Orange <span class="pill unknown">{{ buckets.orange|length }}</span></div>
|
|
<div class="device-grid">
|
|
{% for l in buckets.orange %}{{ license_tile(l) }}{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if buckets.green %}
|
|
<div class="dash-section" data-status="green">
|
|
<div class="dash-section-title">{{ section_chevron() }}Grün <span class="pill online">{{ buckets.green|length }}</span></div>
|
|
<div class="device-grid">
|
|
{% for l in buckets.green %}{{ license_tile(l) }}{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<div class="card card-pad" style="text-align:center; color:var(--text-faint);">
|
|
Aktuell keine aktivierte Lizenz. Neue Tickets/Lizenzen unter <a href="{{ url_for('tickets_list') }}" style="color:var(--accent-strong); font-weight:600;">Tickets</a> anlegen.
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const collapsed = new Set();
|
|
function apply() {
|
|
document.querySelectorAll(".dash-section").forEach(section => {
|
|
section.classList.toggle("collapsed", collapsed.has(section.dataset.status));
|
|
});
|
|
}
|
|
document.querySelectorAll(".dash-section-title").forEach(title => {
|
|
title.addEventListener("click", function () {
|
|
const key = this.closest(".dash-section").dataset.status;
|
|
if (collapsed.has(key)) collapsed.delete(key); else collapsed.add(key);
|
|
apply();
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|