From dca064427e8b0e00ff6b19d0bf8d70c02b070791 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Tue, 14 Apr 2026 15:15:57 +0200 Subject: [PATCH] feat(config): TZ + NTP_SERVER in .env mit sinnvollen Defaults - .env / .env.example: TZ=Europe/Berlin und NTP_SERVER=ptbtime1.ptb.de (offizielle deutsche Zeitreferenz, hohe Verfuegbarkeit) - app/__init__.py setzt prozessweite Zeitzone frueh via os.environ+tzset - Leichtgewichtiger SNTP-Client (pure socket, keine deps) prueft den Uhr-Offset beim Start im Hintergrund-Thread und warnt bei Abweichung >5s - Dockerfile installiert tzdata und ENV TZ=Europe/Berlin als Fallback Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 12 +++++++ Dockerfile | 2 ++ backend/app/__init__.py | 29 ++++++++++++++++ backend/app/config.py | 5 +++ backend/app/services/ntp_check.py | 56 +++++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 backend/app/services/ntp_check.py diff --git a/.env.example b/.env.example index 0e1a5b9..d9eff7b 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,18 @@ 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 +# Wirkt auf datetime.now(), strftime %Z und Kalender/Task-Zeitstempel. +TZ=Europe/Berlin + +# NTP-Server zum Pruefen der Uhrzeit beim Start (nicht-invasiver Offset-Check +# - im Container kann die Systemuhr nicht gesetzt werden; bei Abweichung >5s +# erscheint eine Warnung im Log, dann bitte die Host-Uhr synchronisieren). +# Leerlassen um den Check zu deaktivieren. +# Default: Physikalisch-Technische Bundesanstalt (offizielle deutsche Zeit). +# Alternativen: ptbtime2.ptb.de, ptbtime3.ptb.de, de.pool.ntp.org, time.cloudflare.com +NTP_SERVER=ptbtime1.ptb.de + # OnlyOffice Document Server (optional) # Eigene Subdomain mit HTTPS, z.B. https://office.example.com # JWT wird automatisch vom JWT_SECRET_KEY oben verwendet diff --git a/Dockerfile b/Dockerfile index dc73438..fded646 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ WORKDIR /app # Install system dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ + tzdata \ && rm -rf /var/lib/apt/lists/* # Install Python dependencies @@ -30,6 +31,7 @@ RUN mkdir -p /app/data/files # Environment ENV FLASK_ENV=production +ENV TZ=Europe/Berlin ENV DATABASE_PATH=/app/data/minicloud.db ENV UPLOAD_PATH=/app/data/files diff --git a/backend/app/__init__.py b/backend/app/__init__.py index b2f2ccf..bdf2f07 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1,4 +1,5 @@ import os +import time from pathlib import Path from flask import Flask, Response, redirect, send_from_directory @@ -8,6 +9,20 @@ from app.config import Config from app.extensions import db, bcrypt, migrate +def _configure_timezone(tz_name: str) -> None: + """Prozess-Zeitzone setzen, sodass datetime.now(), strftime %Z etc. + die konfigurierte TZ verwenden. Sichere no-op wenn tzdata fehlt.""" + if not tz_name: + return + os.environ['TZ'] = tz_name + tzset = getattr(time, 'tzset', None) + if tzset: + try: + tzset() + except Exception: + pass + + def _auto_migrate(db): """Add missing columns to existing tables by comparing model definitions with actual database schema. This handles the case where new columns are @@ -61,6 +76,9 @@ def _auto_migrate(db): def create_app(config_class=Config): + # Zeitzone moeglichst frueh setzen - vor allen datetime.now()-Aufrufen + _configure_timezone(getattr(config_class, 'TIMEZONE', None) or os.environ.get('TZ')) + # Check if static frontend build exists (Docker production mode) static_dir = Path(__file__).resolve().parent.parent / 'static' if static_dir.exists(): @@ -171,4 +189,15 @@ def create_app(config_class=Config): from app.services.backup_scheduler import start_backup_scheduler start_backup_scheduler(app) + # NTP-Offset gegen den konfigurierten Zeitserver pruefen (nicht fatal). + ntp_server = app.config.get('NTP_SERVER') or '' + if ntp_server.strip(): + import threading + from app.services.ntp_check import check_and_log + threading.Thread( + target=check_and_log, + args=(ntp_server.strip(), app.logger), + daemon=True, + ).start() + return app diff --git a/backend/app/config.py b/backend/app/config.py index bb42459..6bdbd5d 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -40,3 +40,8 @@ class Config: # CORS FRONTEND_URL = os.environ.get('FRONTEND_URL', 'http://localhost:3000') + + # Zeitzone (prozessweit, wirkt nach time.tzset()) + TIMEZONE = os.environ.get('TZ', 'Europe/Berlin') + # NTP-Server fuer Offset-Check beim Start. Leerstring deaktiviert den Check. + NTP_SERVER = os.environ.get('NTP_SERVER', 'ptbtime1.ptb.de') diff --git a/backend/app/services/ntp_check.py b/backend/app/services/ntp_check.py new file mode 100644 index 0000000..0263413 --- /dev/null +++ b/backend/app/services/ntp_check.py @@ -0,0 +1,56 @@ +"""Leichtgewichtiger SNTP-Client zum Pruefen des Zeit-Offsets. + +Im Container koennen wir die Systemzeit nicht wirklich setzen (braucht +CAP_SYS_TIME). Aber wir koennen den Offset ermitteln und loggen, damit +der Admin weiss, ob der Host driftet. Fuer einen harten Sync muss auf +dem Host selbst ein NTP-Daemon laufen. +""" +from __future__ import annotations + +import socket +import struct +import time + +_NTP_EPOCH_OFFSET = 2208988800 # Sekunden zwischen 1900 und 1970 + + +def query_ntp(server: str, timeout: float = 3.0, port: int = 123) -> float | None: + """Fragt einen NTP-Server und gibt das Offset (Server - Local) in + Sekunden zurueck, oder None bei Fehler.""" + packet = b'\x1b' + 47 * b'\0' # LI=0, VN=3, Mode=3 (client) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + try: + t0 = time.time() + sock.sendto(packet, (server, port)) + data, _ = sock.recvfrom(1024) + t3 = time.time() + except (socket.gaierror, socket.timeout, OSError): + return None + finally: + sock.close() + + if len(data) < 48: + return None + # Transmit timestamp: Offset 40, 8 bytes, fixed point 32.32 + secs, frac = struct.unpack('!II', data[40:48]) + if secs == 0: + return None + t2 = secs - _NTP_EPOCH_OFFSET + frac / 2**32 + # Einfacher Offset (sans roundtrip): (t2 - ((t0 + t3) / 2)) + return t2 - (t0 + t3) / 2 + + +def check_and_log(server: str, logger=None) -> float | None: + import logging + log = logger or logging.getLogger('ntp') + offset = query_ntp(server) + if offset is None: + log.warning('NTP-Check: Server %s nicht erreichbar', server) + return None + if abs(offset) > 5.0: + log.warning('NTP-Check: Systemzeit weicht um %.2fs von %s ab -> Host-Uhr synchronisieren!', + offset, server) + else: + log.info('NTP-Check: Offset %.3fs gegen %s (ok)', offset, server) + return offset