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:
@@ -0,0 +1,78 @@
|
||||
// Anhänge: Dateiauswahl, Einfügen aus Zwischenablage (Strg+V) und Drag & Drop.
|
||||
(function () {
|
||||
function initAttach(form) {
|
||||
var input = form.querySelector(".attach-input");
|
||||
var list = form.querySelector(".attach-list");
|
||||
if (!input) return;
|
||||
var dt = new DataTransfer();
|
||||
var pasteCount = 0;
|
||||
|
||||
function refresh() {
|
||||
input.files = dt.files;
|
||||
list.innerHTML = "";
|
||||
Array.prototype.forEach.call(dt.files, function (f, i) {
|
||||
var chip = document.createElement("span");
|
||||
chip.className = "attach-chip";
|
||||
chip.textContent = (f.name || "Datei") + " ";
|
||||
var x = document.createElement("button");
|
||||
x.type = "button";
|
||||
x.textContent = "×";
|
||||
x.title = "Entfernen";
|
||||
x.addEventListener("click", function () {
|
||||
var keep = new DataTransfer();
|
||||
Array.prototype.forEach.call(dt.files, function (ff, j) {
|
||||
if (j !== i) keep.items.add(ff);
|
||||
});
|
||||
dt = keep;
|
||||
refresh();
|
||||
});
|
||||
chip.appendChild(x);
|
||||
list.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
||||
function addFiles(files) {
|
||||
var added = false;
|
||||
Array.prototype.forEach.call(files, function (f) {
|
||||
if (f) { dt.items.add(f); added = true; }
|
||||
});
|
||||
if (added) refresh();
|
||||
}
|
||||
|
||||
input.addEventListener("change", function () {
|
||||
addFiles(input.files);
|
||||
});
|
||||
|
||||
form.addEventListener("paste", function (e) {
|
||||
var items = e.clipboardData && e.clipboardData.items;
|
||||
if (!items) return;
|
||||
var imgs = [];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].kind === "file") {
|
||||
var f = items[i].getAsFile();
|
||||
if (f) {
|
||||
if (!f.name || f.name === "image.png") {
|
||||
pasteCount++;
|
||||
var ext = (f.type && f.type.split("/")[1]) || "png";
|
||||
f = new File([f], "einfuegung-" + pasteCount + "." + ext, { type: f.type });
|
||||
}
|
||||
imgs.push(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (imgs.length) { e.preventDefault(); addFiles(imgs); }
|
||||
});
|
||||
|
||||
["dragover", "dragenter"].forEach(function (ev) {
|
||||
form.addEventListener(ev, function (e) { e.preventDefault(); form.classList.add("dragging"); });
|
||||
});
|
||||
["dragleave", "drop"].forEach(function (ev) {
|
||||
form.addEventListener(ev, function (e) { e.preventDefault(); form.classList.remove("dragging"); });
|
||||
});
|
||||
form.addEventListener("drop", function (e) {
|
||||
if (e.dataTransfer && e.dataTransfer.files) addFiles(e.dataTransfer.files);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll("form.attach-form").forEach(initAttach);
|
||||
})();
|
||||
Reference in New Issue
Block a user