- Poll the public download page for "Aktuelle Version <nr> vom <date>", report changes through the normal change feed and notification mail, and show the current version as a card on the overview. Runs on its own interval (default 30 min); the first run only records a baseline. - Archive epirentServer.zip / epirentClient.zip per version number so we keep a download cache and history: streamed to disk with SHA-256 and size, fetched in a background thread so ticket polling is unaffected, with a free-space check before downloading (~1.1 GB per version). - Retention is configurable via EPIRENT_DOWNLOAD_KEEP, where 0 means unlimited; a successful archive is reported as a change and by mail. - New Downloads page listing the archived versions with checksums and links to fetch the cached files. - Notification lines now omit the arrow when there is no previous value and drop the "#" for entries without a ticket. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
130 lines
5.5 KiB
HTML
130 lines
5.5 KiB
HTML
{% extends "base.html" %}
|
||
{% from "_macros.html" import change_row %}
|
||
{% block title %}Übersicht · EPI-Support{% endblock %}
|
||
{% block content %}
|
||
|
||
<section class="cards">
|
||
<div class="card"><span class="num">{{ stats.total }}</span><span class="lbl">Tickets gesamt</span></div>
|
||
<div class="card"><span class="num">{{ stats.open }}</span><span class="lbl">offen</span></div>
|
||
<div class="card {{ 'alert' if stats.unseen else '' }}">
|
||
<span class="num">{{ stats.unseen }}</span><span class="lbl">ungelesene Änderungen</span>
|
||
</div>
|
||
{% if epirent_version %}
|
||
<div class="card">
|
||
<span class="num">{{ epirent_version.version }}</span>
|
||
<span class="lbl">EPIRent-Version<br>vom {{ epirent_version.released_at }}</span>
|
||
</div>
|
||
{% endif %}
|
||
<div class="card status">
|
||
{% if last_run %}
|
||
<span class="num {{ 'ok' if last_run.status == 'ok' else 'err' }}">
|
||
{{ '✓' if last_run.status == 'ok' else '!' }}
|
||
</span>
|
||
<span class="lbl">letzter Abruf<br>{{ last_run.finished_at|dt }}</span>
|
||
{% else %}
|
||
<span class="num">…</span><span class="lbl">startet</span>
|
||
{% endif %}
|
||
</div>
|
||
</section>
|
||
|
||
<div class="grid">
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<h2>Ungelesene Änderungen{% if stats.unseen %} ({{ stats.unseen }}){% endif %}</h2>
|
||
{% if stats.unseen %}
|
||
<form method="post" action="{{ url_for('mark_read') }}">
|
||
<button class="btn-sm" type="submit">alle als gelesen</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
{% if recent %}
|
||
<ul class="feed compact">
|
||
{% for c in recent %}{{ change_row(c) }}{% endfor %}
|
||
</ul>
|
||
{% if stats.unseen > recent|length %}
|
||
<a class="more" href="{{ url_for('changes', show='unseen') }}">
|
||
+ {{ stats.unseen - recent|length }} weitere ungelesene anzeigen →</a>
|
||
{% endif %}
|
||
{% else %}
|
||
<p class="muted">Alles gelesen – keine offenen Änderungen. 🎉</p>
|
||
{% endif %}
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<div class="panel-head"><h2>Status-Filter</h2></div>
|
||
{% set arch = '1' if show_archived else none %}
|
||
<div class="chips">
|
||
<a class="chip {{ 'on' if not active_state }}" href="{{ url_for('index', sort=sort, dir=dir, archived=arch) }}">alle</a>
|
||
<a class="chip {{ 'on' if active_state == 'not_closed' }}"
|
||
href="{{ url_for('index', state='not_closed', sort=sort, dir=dir, archived=arch) }}">nicht geschlossen</a>
|
||
{% for s in states %}
|
||
<a class="chip {{ 'on' if active_state == s.state }}"
|
||
href="{{ url_for('index', state=s.state, sort=sort, dir=dir, archived=arch) }}">{{ s.state }} ({{ s.c }})</a>
|
||
{% endfor %}
|
||
</div>
|
||
<div class="chips" style="margin-top:.5rem">
|
||
{% if show_archived %}
|
||
<a class="chip on" href="{{ url_for('index', state=active_state or none, sort=sort, dir=dir) }}">erledigte ausblenden</a>
|
||
{% else %}
|
||
<a class="chip" href="{{ url_for('index', state=active_state or none, sort=sort, dir=dir, archived='1') }}">erledigte einblenden{% if archived_count %} ({{ archived_count }}){% endif %}</a>
|
||
{% endif %}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
{% macro th(col, label) %}
|
||
{% set nextdir = 'asc' if (sort == col and dir == 'desc') else 'desc' %}
|
||
<th class="{{ 'sorted' if sort == col }}">
|
||
<a href="{{ url_for('index', state=active_state or none, sort=col, dir=nextdir, archived=request.args.get('archived')) }}">
|
||
{{ label }}{% if sort == col %} <span class="arrow">{{ '▼' if dir == 'desc' else '▲' }}</span>{% endif %}
|
||
</a>
|
||
</th>
|
||
{% endmacro %}
|
||
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<h2>Tickets{% if active_state == 'not_closed' %} – nicht geschlossen{% elif active_state %} – {{ active_state }}{% endif %}</h2>
|
||
</div>
|
||
<table class="tickets">
|
||
<thead>
|
||
<tr>
|
||
{{ th('number', '#') }}{{ th('title', 'Titel') }}{{ th('state', 'Status') }}
|
||
{{ th('priority', 'Priorität') }}{{ th('owner', 'Bearbeiter') }}
|
||
{{ th('articles', 'Artikel') }}{{ th('updated', 'zuletzt geändert') }}
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for t in tickets %}
|
||
{% set n = unseen_map.get(t.id) %}
|
||
<tr class="{{ 'row-unseen' if n }}{{ ' row-archived' if t.archived }}">
|
||
<td>
|
||
{% if n %}<span class="badge" title="{{ n }} unbestätigte Änderung(en)">{{ n }}</span>{% endif %}
|
||
<a href="{{ url_for('ticket_detail', ticket_id=t.id) }}">{{ t.number }}</a>
|
||
</td>
|
||
<td class="title">{{ t.title }}{% if t.archived %} <span class="tag">erledigt</span>{% endif %}</td>
|
||
<td><span class="state state-{{ t.state }}">{{ t.state }}</span></td>
|
||
<td>{{ t.priority }}</td>
|
||
<td>{{ t.owner if t.owner and t.owner != '-' else '—' }}</td>
|
||
<td>{{ t.article_count }}</td>
|
||
<td><time>{{ t.zammad_updated_at|dt }}</time></td>
|
||
<td class="rowact">
|
||
{% if t.archived %}
|
||
<form method="post" action="{{ url_for('unarchive_ticket', ticket_id=t.id) }}">
|
||
<button type="submit" title="Wieder einblenden">↩</button>
|
||
</form>
|
||
{% else %}
|
||
<form method="post" action="{{ url_for('archive_ticket', ticket_id=t.id) }}">
|
||
<button type="submit" title="Als wirklich erledigt ablegen (ausblenden)">✓</button>
|
||
</form>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="8" class="muted">Keine Tickets.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
{% endblock %}
|