Watch EPIRent version and archive its download packages

- 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>
This commit is contained in:
2026-07-20 15:12:48 +02:00
co-authored by Claude Opus 4.8
parent 528d66b2b0
commit bdcb17285c
13 changed files with 575 additions and 10 deletions
+25 -3
View File
@@ -9,9 +9,10 @@ from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from flask import (Flask, Response, abort, flash, redirect, render_template,
request, url_for)
request, send_file, url_for)
from . import analytics, charts, config, conversation, db, export, poller, uploads
from . import (analytics, charts, config, conversation, db, downloads, epirent,
export, poller, uploads)
from .zammad import ZammadClient, ZammadError
log = logging.getLogger("episupport.web")
@@ -201,7 +202,8 @@ def create_app() -> Flask:
stats=stats, recent=recent, last_run=last_run,
active_state=state, sort=sort, dir=direction,
unseen_map=unseen_map, show_archived=show_archived,
archived_count=archived_count)
archived_count=archived_count,
epirent_version=epirent.current())
@app.route("/ticket/<int:ticket_id>")
def ticket_detail(ticket_id):
@@ -447,6 +449,26 @@ def create_app() -> Flask:
headers={"Content-Disposition": f'attachment; filename="{fname}"'},
)
@app.route("/downloads")
def downloads_view():
return render_template("downloads.html",
versions=downloads.history(),
human=downloads.human_size,
keep=config.EPIRENT_DOWNLOAD_KEEP,
current=epirent.current())
@app.route("/downloads/<version>/<path:filename>")
def download_file(version, filename):
# nur bekannte, in der DB verzeichnete Dateien ausliefern
entry = next((r for r in downloads.archived(version)
if r["filename"] == filename), None)
if not entry:
abort(404)
path = downloads.version_dir(version) / filename
if not path.exists():
abort(404)
return send_file(path, as_attachment=True, download_name=filename)
@app.route("/health")
def health():
with db.get_conn() as conn: