Files
tesm-license/apps/tesm-license/src/tesm_license/templates/license/licenses.html
T
alientimandClaude Opus 5 f7805a2180 TESM-Lizenzserver 2.0.0 -- Neubau
Vollstaendiger Neubau der Anwendung. Der vorherige Stand bleibt unveraendert
im Zweig SONNET5 erhalten.

Aufbau: apps/tesm-license (Anwendung), packages/tesm-core (gemeinsamer Kern),
packages/tesm-licensing (Lizenzprotokoll), deploy (Installation, systemd,
privilegierter Helfer), docs, tests. Die verwaltete Anwendung liegt in ihrem
eigenen Repository; beide Repositorien bringen die gemeinsamen Pakete mit,
damit sich jedes allein installieren laesst.

Die wichtigsten Unterschiede zum Vorgaenger:

* Keine doppelte licensing.py -- ein Paket, das beide Anwendungen
  installieren, statt zweier Dateien, die byte-identisch bleiben sollen.
* Der Webprozess laeuft unprivilegiert; alles, was Root braucht, geht ueber
  einen einzigen Helfer mit Positivlisten fuer jedes Argument.
* CSRF-Schutz ueberhaupt -- der Vorgaenger hatte keinen.
* Rechte werden serverseitig geprueft, nicht nur im Template ausgeblendet.
* Keine Lizenz ohne master_endpoint: eine Ausstellung ohne Endpunkt wird
  abgelehnt statt eine Lizenz zu erzeugen, die sich nie aktivieren kann.
* Offline-Aktivierung in beide Richtungen; die Lizenz bleibt als
  "Aktivierung offen" markiert, bis sie zurueckkommt.
* Getrennte Signaturkontexte je Nachrichtenart, Nonce gegen Wiedereinspielung,
  seq gegen das Zurueckrollen auf eine aeltere Lizenz.
* Kein Hostname im Maschinen-Fingerabdruck.
* Verschachtelte Datenbankverbindungen sind ein Fehler, kein Deadlock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 18:01:23 +02:00

78 lines
3.6 KiB
HTML

{% extends "tesm_core/base.html" %}
{% from "tesm_core/macros.html" import pill, search_box, empty_state %}
{% block page_title %}Lizenzen{% endblock %}
{% block page_sub %}{{ licenses | length }} Lizenzen{% if active_filter %} &middot; Filter: {{ active_filter }}{% endif %}{% endblock %}
{% block topbar_extra %}
{{ search_box('licenses-table', 'Kunde, Lizenz-ID, Host ...') }}
{% if current_user.has('licenses.execute') %}
<a class="btn btn--sm" href="{{ url_for('licenses.manual_code') }}">
{{ icon('transfer', size=15) }} Offline-Code
</a>
{% endif %}
{% endblock %}
{% block content %}
<section class="card">
<div class="table-toolbar">
<div class="btn-group">
<a class="btn btn--sm {{ 'btn--subtle' if not active_filter }}" href="{{ url_for('licenses.overview') }}">alle</a>
{% for state in states %}
<a class="btn btn--sm {{ 'btn--subtle' if active_filter == state }}"
href="{{ url_for('licenses.overview', status=state) }}">{{ state }}</a>
{% endfor %}
</div>
</div>
<div class="table-wrap">
<table class="table" id="licenses-table" data-behavior="sortable">
<thead>
<tr>
<th class="sortable">Kunde</th>
<th class="sortable">Lizenz</th>
<th class="sortable">Typ</th>
<th class="sortable">Zustand</th>
<th class="sortable">Restlaufzeit</th>
<th class="sortable">Host</th>
<th class="sortable">Heartbeat</th>
</tr>
</thead>
<tbody>
{% for entry in licenses %}
<tr data-filter-text="{{ entry.customer_name }} {{ entry.license_id }} {{ entry.hostname }} {{ entry.type }}">
<td class="fw-600">
<a href="{{ url_for('customers.detail', customer_id=entry.customer_id) }}">{{ entry.customer_name }}</a>
{% if entry.role != 'customer' %}{{ pill(role_labels.get(entry.role, entry.role), 'accent') }}{% endif %}
</td>
<td class="mono fs-sm">
<a href="{{ url_for('licenses.detail', license_id=entry.license_id) }}">{{ entry.license_id[:12] }}&hellip;</a>
</td>
<td class="fs-sm">{{ type_labels.get(entry.type, entry.type) }}</td>
<td>
{% if entry.status == 'active' %}{{ pill('aktiv', 'online') }}
{% elif entry.status == 'issued' %}{{ pill('ausgestellt', 'info') }}
{% elif entry.status == 'revoked' %}{{ pill('widerrufen', 'danger') }}
{% else %}{{ pill('deaktiviert', '') }}{% endif %}
</td>
<td data-sort-value="{{ entry.days_left if entry.days_left is not none else 99999 }}">
{% if entry.lifetime %}{{ pill('unbefristet', 'online') }}
{% elif entry.is_expired %}{{ pill('abgelaufen', 'danger') }}
{% elif entry.color == 'red' %}{{ pill(entry.days_left ~ ' Tage', 'danger') }}
{% elif entry.color == 'orange' %}{{ pill(entry.days_left ~ ' Tage', 'warning') }}
{% else %}{{ pill(entry.days_left ~ ' Tage', 'online') }}{% endif %}
</td>
<td class="fs-sm">{{ entry.hostname or '&ndash;' | safe }}</td>
<td class="fs-sm" data-sort-value="{{ entry.last_heartbeat_at or '' }}">
{% if entry.last_heartbeat_at %}
<span data-relative-time="{{ entry.last_heartbeat_at }}">{{ entry.last_heartbeat_at | dt }}</span>
{% else %}<span class="faint">nie</span>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if not licenses %}{{ empty_state('Keine Lizenzen', 'Zu diesem Filter gibt es nichts.', 'shield') }}{% endif %}
</section>
{% endblock %}