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) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-14 15:19:40 +02:00
parent dca064427e
commit 9bb22eb17b
4 changed files with 93 additions and 1 deletions

View File

@ -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

View File

@ -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: <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>
**`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

View File

@ -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

View File

@ -37,6 +37,28 @@
</div>
</div>
<!-- System-Info: Zeitzone & NTP (read-only) -->
<div class="admin-section">
<h3>System-Zeit</h3>
<p class="hint">Wird in der <code>.env</code> festgelegt (Keys <code>TZ</code> und <code>NTP_SERVER</code>).
Aenderungen erfordern einen Neustart des Backends.</p>
<div class="sysinfo">
<div class="sysinfo-row">
<span class="sysinfo-label">Zeitzone:</span>
<code>{{ settings.timezone || '—' }}</code>
<span v-if="settings.timezone_abbr" class="sysinfo-extra">({{ settings.timezone_abbr }})</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Aktuelle Server-Zeit:</span>
<code>{{ formatServerTime(settings.server_time) }}</code>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">NTP-Server:</span>
<code>{{ settings.ntp_server || '(deaktiviert)' }}</code>
</div>
</div>
</div>
<!-- System Email -->
<div class="admin-section">
<h3>System-E-Mail (SMTP)</h3>
@ -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; }