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:
+42
-2
@@ -5,7 +5,7 @@ import logging
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from . import config, db, notify
|
||||
from . import config, conversation, db, notify
|
||||
from .zammad import ZammadClient, ZammadError
|
||||
|
||||
log = logging.getLogger("episupport.poller")
|
||||
@@ -111,7 +111,12 @@ def _detect_changes(conn, client, new_t: dict, old_row) -> list[dict]:
|
||||
new_count = new_t.get("article_count") or 0
|
||||
if new_count > old_count:
|
||||
detail = None
|
||||
articles = client.ticket_articles(new_t["id"])
|
||||
try:
|
||||
articles = client.ticket_articles(new_t["id"])
|
||||
conversation.save_articles(new_t["id"], articles, conn=conn) # cachen
|
||||
except ZammadError as e:
|
||||
log.warning("Artikel für Ticket %s nicht abrufbar: %s", new_t["id"], e)
|
||||
articles = []
|
||||
if articles:
|
||||
last = articles[-1]
|
||||
sender = last.get("sender") # Agent / Customer / System
|
||||
@@ -133,6 +138,35 @@ def _detect_changes(conn, client, new_t: dict, old_row) -> list[dict]:
|
||||
return found
|
||||
|
||||
|
||||
def sync_ticket(client: ZammadClient, ticket_id: int) -> None:
|
||||
"""Ein einzelnes Ticket (+ Artikel) sofort in die DB übernehmen.
|
||||
|
||||
Wird nach einer EIGENEN Aktion (Antwort/Neuanlage über den Client) gerufen,
|
||||
damit der nächste Poll-Durchlauf keinen Unterschied sieht und die selbst
|
||||
ausgelöste Änderung weder im Feed erscheint noch eine Mail auslöst.
|
||||
"""
|
||||
t = client.get_ticket(ticket_id)
|
||||
with db.get_conn() as conn:
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM tickets WHERE id=?", (ticket_id,)
|
||||
).fetchone()
|
||||
_upsert(conn, _map_ticket(t), is_new=(existing is None))
|
||||
try:
|
||||
conversation.store_articles(client, ticket_id)
|
||||
except ZammadError as e:
|
||||
log.warning("Artikel-Sync nach eigener Aktion (Ticket %s): %s", ticket_id, e)
|
||||
|
||||
|
||||
def _backfill_articles(client: ZammadClient, limit: int = 10) -> None:
|
||||
"""Pro Durchlauf einige Tickets ohne gespeicherte Konversation nachladen,
|
||||
damit die Leseansicht nach und nach komplett aus der DB bedient wird."""
|
||||
for tid in conversation.tickets_without_articles(limit):
|
||||
try:
|
||||
conversation.store_articles(client, tid)
|
||||
except ZammadError as e:
|
||||
log.warning("Backfill für Ticket %s fehlgeschlagen: %s", tid, e)
|
||||
|
||||
|
||||
def poll_once(client: ZammadClient) -> int:
|
||||
"""Ein Abruf-Durchlauf. Gibt Anzahl erkannter Änderungen zurück."""
|
||||
started = _now()
|
||||
@@ -157,9 +191,15 @@ def poll_once(client: ZammadClient) -> int:
|
||||
all_changes.append(_record_change(
|
||||
conn, t, "new_ticket", "Neues Ticket", None,
|
||||
f"#{t.get('number')}", None))
|
||||
try:
|
||||
conversation.store_articles(client, t["id"], conn=conn)
|
||||
except ZammadError:
|
||||
pass
|
||||
else:
|
||||
all_changes.extend(_detect_changes(conn, client, t, existing))
|
||||
_upsert(conn, row, is_new=False)
|
||||
# Backfill NACH der Haupttransaktion (eigene, kurze Verbindungen)
|
||||
_backfill_articles(client)
|
||||
except ZammadError as e:
|
||||
status = "error"
|
||||
error = str(e)
|
||||
|
||||
Reference in New Issue
Block a user