From 9bb22eb17bfe5cb8dc78b6636282d2bddf8619cb Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Tue, 14 Apr 2026 15:19:40 +0200 Subject: [PATCH] feat: Admin-Sicht System-Zeit + TZ-Liste in README/.env.example - /api/settings gibt zusaetzlich timezone, timezone_abbr, server_time, ntp_server zurueck (alle read-only, aus Config/ENV). - AdminView zeigt neuen Abschnitt "System-Zeit" mit Zeitzone, aktueller Server-Zeit und NTP-Server samt Hinweis "wird in der .env festgelegt". - .env.example: Liste gaengiger TZ-Werte mit Link zur IANA-Vollliste. - README.md: neuer Abschnitt "Zeitzone & NTP" mit Werte-Tabelle. Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 8 +++++- README.md | 30 +++++++++++++++++++++ backend/app/api/users.py | 11 ++++++++ frontend/src/views/AdminView.vue | 45 ++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index d9eff7b..cff0d5a 100644 --- a/.env.example +++ b/.env.example @@ -31,8 +31,14 @@ FRONTEND_URL=https://cloud.example.com # Max Upload-Groesse in MB MAX_UPLOAD_SIZE_MB=500 -# Zeitzone (prozessweit) - z.B. Europe/Berlin, Europe/Vienna, UTC +# Zeitzone (prozessweit) - IANA-Format "Region/Stadt". # Wirkt auf datetime.now(), strftime %Z und Kalender/Task-Zeitstempel. +# Haeufige Werte: +# Europe/Berlin, Europe/Vienna, Europe/Zurich, Europe/Amsterdam, +# Europe/Paris, Europe/London, Europe/Madrid, Europe/Rome, +# Europe/Warsaw, Europe/Prague, Europe/Copenhagen, Europe/Stockholm, +# UTC, America/New_York, America/Los_Angeles, Asia/Tokyo, Australia/Sydney +# Vollstaendige Liste: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones TZ=Europe/Berlin # NTP-Server zum Pruefen der Uhrzeit beim Start (nicht-invasiver Offset-Check diff --git a/README.md b/README.md index cf4f736..ed42a90 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,36 @@ docker-compose up --build -d **Ohne OnlyOffice** (`ONLYOFFICE_URL` leer) werden Office-Dateien in einer einfachen Vorschau angezeigt. **Mit OnlyOffice** erhaelt man einen vollwertigen Editor (wie Google Docs). +### Zeitzone & NTP + +In der `.env` stehen zwei Variablen die die Systemzeit betreffen: + +```env +TZ=Europe/Berlin +NTP_SERVER=ptbtime1.ptb.de +``` + +**`TZ`** setzt die prozessweite Zeitzone (wirkt auf Log-Zeitstempel, Kalender/Task-Zeiten, `datetime.now()`). IANA-Format `Region/Stadt`. + +Haeufige Werte: + +| Region | Beispielwerte | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| Deutschland | `Europe/Berlin` | +| DACH/EU | `Europe/Vienna`, `Europe/Zurich`, `Europe/Amsterdam`, `Europe/Paris`, `Europe/London`, `Europe/Madrid`, `Europe/Rome`, `Europe/Warsaw` | +| Nord-EU | `Europe/Copenhagen`, `Europe/Stockholm`, `Europe/Helsinki`, `Europe/Oslo` | +| Sonstige | `UTC`, `America/New_York`, `America/Los_Angeles`, `Asia/Tokyo`, `Australia/Sydney` | + +Vollstaendige Liste: + +**`NTP_SERVER`** wird beim Start abgefragt, um die Abweichung der Systemuhr zu pruefen. Bei Drift > 5 s erscheint eine Warnung im Log. **Hinweis:** Im Container wird die Uhr dadurch nicht gesetzt (benoetigt `CAP_SYS_TIME`) - auf dem Host sollte ein NTP-Daemon laufen. Der Check dient nur zur Sichtbarkeit. + +Default: `ptbtime1.ptb.de` (offizielle deutsche Zeitreferenz der Physikalisch-Technischen Bundesanstalt, Stratum 1, sehr hohe Verfuegbarkeit). + +Alternativen: `ptbtime2.ptb.de`, `ptbtime3.ptb.de`, `de.pool.ntp.org`, `time.cloudflare.com`. Leerlassen um den Check zu deaktivieren. + +Aktuelle Werte sind im Admin-Bereich unter **Einstellungen > System** einsehbar. + ## Verwendung ### Dateien diff --git a/backend/app/api/users.py b/backend/app/api/users.py index 49ec64e..d44856a 100644 --- a/backend/app/api/users.py +++ b/backend/app/api/users.py @@ -145,6 +145,12 @@ def delete_user(user_id): @api_bp.route('/settings', methods=['GET']) @admin_required def get_settings(): + import time as _time + from datetime import datetime as _dt + try: + tzname = _time.strftime('%Z') + except Exception: + tzname = '' return jsonify({ 'public_registration': AppSettings.get_bool('public_registration', default=True), 'system_smtp_host': AppSettings.get('system_smtp_host', ''), @@ -155,6 +161,11 @@ def get_settings(): 'system_email_from': AppSettings.get('system_email_from', ''), 'onlyoffice_url': os.environ.get('ONLYOFFICE_URL', ''), 'onlyoffice_configured': bool(os.environ.get('ONLYOFFICE_URL', '')), + # Read-only system info aus der .env + 'timezone': os.environ.get('TZ', 'Europe/Berlin'), + 'timezone_abbr': tzname, + 'server_time': _dt.now().isoformat(timespec='seconds'), + 'ntp_server': os.environ.get('NTP_SERVER', ''), }), 200 diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index 5436882..cadf653 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -37,6 +37,28 @@ + +
+

System-Zeit

+

Wird in der .env festgelegt (Keys TZ und NTP_SERVER). + Aenderungen erfordern einen Neustart des Backends.

+
+
+ Zeitzone: + {{ settings.timezone || '—' }} + ({{ settings.timezone_abbr }}) +
+
+ Aktuelle Server-Zeit: + {{ formatServerTime(settings.server_time) }} +
+
+ NTP-Server: + {{ settings.ntp_server || '(deaktiviert)' }} +
+
+
+

System-E-Mail (SMTP)

@@ -551,6 +573,17 @@ const smtpForm = ref({ const smtpPasswordSet = ref(false) const onlyofficeConfigured = ref(false) const onlyofficeUrl = ref('') +const settings = ref({ timezone: '', timezone_abbr: '', server_time: '', ntp_server: '' }) + +function formatServerTime(iso) { + if (!iso) return '—' + try { + return new Date(iso).toLocaleString('de-DE', { + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', minute: '2-digit', second: '2-digit', + }) + } catch { return iso } +} const smtpTesting = ref(false) // Backup & Restore @@ -660,6 +693,12 @@ async function loadSettings() { smtpPasswordSet.value = res.data.system_smtp_password_set onlyofficeConfigured.value = res.data.onlyoffice_configured onlyofficeUrl.value = res.data.onlyoffice_url || '' + settings.value = { + timezone: res.data.timezone || '', + timezone_abbr: res.data.timezone_abbr || '', + server_time: res.data.server_time || '', + ntp_server: res.data.ntp_server || '', + } } catch { /* first load, defaults */ } } @@ -1216,6 +1255,12 @@ onMounted(() => { .field-row { display: flex; gap: 0.75rem; align-items: flex-end; } .flex-grow { flex: 1; } .hint { font-size: 0.85rem; color: var(--p-text-muted-color); margin: 0 0 0.75rem; } +.hint code { background: var(--p-surface-100); padding: 0.05rem 0.35rem; border-radius: 3px; font-size: 0.8rem; } +.sysinfo { display: flex; flex-direction: column; gap: 0.4rem; font-size: 0.875rem; } +.sysinfo-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; } +.sysinfo-label { min-width: 180px; color: var(--p-text-muted-color); } +.sysinfo code { background: var(--p-surface-100); padding: 0.15rem 0.5rem; border-radius: 4px; } +.sysinfo-extra { color: var(--p-text-muted-color); font-size: 0.8rem; } .invite-section { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--p-surface-200); } .invite-section h4 { margin: 0 0 0.25rem; font-size: 0.95rem; } .invite-row { display: flex; gap: 0.5rem; align-items: flex-start; }