Add EPI-Support monitoring & analytics platform

Internal Flask app that monitors the epi-helpdesk.zammad.com tickets via
the Zammad API, deployable as a systemd service behind Apache under
/episupport.

Features:
- Poller: session-login API sync, snapshot diff to detect agent changes
  (status, owner, priority, closures, new replies); email notifications
- Dashboard: ticket overview with sorting, "not closed" filter and
  highlighting of tickets with unconfirmed changes; per-item and bulk
  "mark as read"
- Analytics: KPIs, response/resolution times, monthly SVG trend chart,
  distributions (status/priority/group/agent), Excel export
- Conversation view: full per-ticket thread cached in the DB (avoids
  proxy timeouts), inline image attachments, safe HTML-to-text rendering
- Write actions: reply to tickets and create new tickets with a
  confirmation step and attachment upload (incl. clipboard paste);
  self-triggered actions are synced back so they aren't flagged as changes
- Local timezone display (Europe/Berlin) for all timestamps

Includes deploy tooling: setup.sh, systemd units and Apache config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 09:19:53 +02:00
co-authored by Claude Opus 4.8
parent da2f7c3c86
commit 1041f283a4
17 changed files with 600 additions and 57 deletions
+27 -3
View File
@@ -25,6 +25,9 @@
<section class="panel">
<div class="panel-head">
<h2>Konversation{% if conversation %} ({{ conversation|length }}){% endif %}</h2>
<form method="post" action="{{ url_for('refresh_conversation', ticket_id=ticket.id) }}">
<button class="btn-sm" type="submit">↻ Aktualisieren</button>
</form>
</div>
{% if conv_error %}
<p class="muted">Konversation konnte nicht geladen werden: {{ conv_error }}</p>
@@ -42,7 +45,25 @@
{% if a.subject %}<div class="msg-subj">{{ a.subject }}</div>{% endif %}
<div class="msg-body">{{ a.body }}</div>
{% if a.attachments %}
<div class="msg-att">📎 {{ a.attachments|join(', ') }}</div>
{% set imgs = a.attachments|selectattr('is_image')|list %}
{% set files = a.attachments|rejectattr('is_image')|list %}
{% if imgs %}
<div class="msg-imgs">
{% for att in imgs %}
<a href="{{ url_for('attachment', ticket_id=ticket.id, article_id=a.id, att_id=att.id) }}" target="_blank" rel="noopener">
<img class="msg-img" loading="lazy" alt="{{ att.filename }}"
src="{{ url_for('attachment', ticket_id=ticket.id, article_id=a.id, att_id=att.id) }}">
</a>
{% endfor %}
</div>
{% endif %}
{% if files %}
<div class="msg-att">
{% for att in files %}
📎 {% if att.id %}<a href="{{ url_for('attachment', ticket_id=ticket.id, article_id=a.id, att_id=att.id) }}" target="_blank" rel="noopener">{{ att.filename }}</a>{% else %}{{ att.filename }}{% endif %}
{% endfor %}
</div>
{% endif %}
{% endif %}
</article>
{% endfor %}
@@ -52,9 +73,11 @@
{% endif %}
{% if ticket.state != 'closed' %}
<form method="post" action="{{ url_for('reply_preview', ticket_id=ticket.id) }}" class="reply-form">
<form method="post" action="{{ url_for('reply_preview', ticket_id=ticket.id) }}"
class="reply-form attach-form" enctype="multipart/form-data">
<textarea name="body" rows="4" required
placeholder="Antwort an den EPI-Support verfassen…"></textarea>
placeholder="Antwort an den EPI-Support verfassen… (Screenshot mit Strg+V einfügen)"></textarea>
{% include "_attach.html" %}
<div class="form-actions">
<span class="hint-inline">Geht nach Bestätigung live an EPI.</span>
<button class="btn-sm" type="submit">Antworten →</button>
@@ -84,3 +107,4 @@
{% endif %}
</section>
{% endblock %}
{% block scripts %}<script src="{{ url_for('static', filename='upload.js') }}"></script>{% endblock %}