Files
EpiSupport-Zammad-Scraper/templates/ticket.html
T
leopold.stroblandClaude Opus 4.8 1041f283a4 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>
2026-07-01 09:19:53 +02:00

111 lines
4.8 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}#{{ ticket.number }} · EPI-Support{% endblock %}
{% block content %}
<p><a class="back" href="{{ url_for('index') }}">← zurück zur Übersicht</a></p>
<section class="panel">
<div class="panel-head">
<h2>#{{ ticket.number }} {{ ticket.title }}</h2>
<a class="btn-sm" href="{{ zammad_url }}/#ticket/zoom/{{ ticket.id }}"
target="_blank" rel="noopener">in Zammad öffnen ↗</a>
</div>
<dl class="meta">
<div><dt>Status</dt><dd><span class="state state-{{ ticket.state }}">{{ ticket.state }}</span></dd></div>
<div><dt>Priorität</dt><dd>{{ ticket.priority }}</dd></div>
<div><dt>Bearbeiter</dt><dd>{{ ticket.owner if ticket.owner and ticket.owner != '-' else '—' }}</dd></div>
<div><dt>Gruppe</dt><dd>{{ ticket.group }}</dd></div>
<div><dt>Artikel</dt><dd>{{ ticket.article_count }}</dd></div>
<div><dt>erstellt</dt><dd>{{ ticket.zammad_created_at|dt }}</dd></div>
<div><dt>zuletzt geändert</dt><dd>{{ ticket.zammad_updated_at|dt }}</dd></div>
<div><dt>geschlossen</dt><dd>{{ ticket.last_close_at|dt }}</dd></div>
</dl>
</section>
<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>
{% elif conversation %}
<div class="thread">
{% for a in conversation %}
<article class="msg msg-{{ a.sender|lower if a.sender else 'system' }}">
<header class="msg-head">
<span class="who">
<span class="role role-{{ a.sender|lower if a.sender else 'system' }}">{{ a.sender or 'System' }}</span>
{{ a.frm }}
</span>
<time>{{ a.created_at|dt }}</time>
</header>
{% if a.subject %}<div class="msg-subj">{{ a.subject }}</div>{% endif %}
<div class="msg-body">{{ a.body }}</div>
{% if a.attachments %}
{% 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 %}
</div>
{% else %}
<p class="muted">Keine Nachrichten vorhanden.</p>
{% endif %}
{% if ticket.state != 'closed' %}
<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… (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>
</div>
</form>
{% else %}
<p class="muted">Ticket ist geschlossen zum Antworten ggf. ein neues Ticket anlegen.</p>
{% endif %}
</section>
<section class="panel">
<div class="panel-head"><h2>Verlauf (von uns erfasst)</h2></div>
{% if timeline %}
<ul class="timeline">
{% for c in timeline %}
<li>
<span class="tag tag-{{ c.field }}">{{ c.label }}</span>
<div class="chg">{{ c.old_value or '' }} → <b>{{ c.new_value or '' }}</b>
{% if c.detail %}<div class="detail">{{ c.detail }}</div>{% endif %}
</div>
<time>{{ c.detected_at|dt }}</time>
</li>
{% endfor %}
</ul>
{% else %}
<p class="muted">Seit Beobachtungsbeginn keine Änderung erfasst.</p>
{% endif %}
</section>
{% endblock %}
{% block scripts %}<script src="{{ url_for('static', filename='upload.js') }}"></script>{% endblock %}