docs/dev: sync backend rework, MQTT, and devcontainer hygiene
README: add Versioning (unified SemVer, pre-releases, build metadata); emphasize UTC handling and streaming endpoint; add Dev Container notes (UI-only Remote Containers, npm ci, idempotent aliases) TECH-CHANGELOG: backend rework notes (serialization camelCase, UTC normalization, streaming metadata); add component build metadata template (image tags/SHAs) Copilot instructions: integrate maintenance guardrails; reinforce UTC and camelCase conventions; document MQTT topics and scheduler retained payload behavior Devcontainer: map Remote Containers to UI; remove in-container install; switch to npm ci; make aliases idempotent
This commit is contained in:
@@ -9,7 +9,7 @@ FROM python:3.13-slim
|
||||
# verbindet (gemäß devcontainer.json). Sie schaden aber nicht.
|
||||
ARG USER_ID=1000
|
||||
ARG GROUP_ID=1000
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends locales curl git \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends locales curl git docker.io \
|
||||
&& groupadd -g ${GROUP_ID} infoscreen_taa \
|
||||
&& useradd -u ${USER_ID} -g ${GROUP_ID} --shell /bin/bash --create-home infoscreen_taa \
|
||||
&& sed -i 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen \
|
||||
|
||||
@@ -14,7 +14,9 @@ if not DB_URL:
|
||||
# Dev: DB-URL aus Einzelwerten bauen
|
||||
DB_USER = os.getenv("DB_USER", "infoscreen_admin")
|
||||
DB_PASSWORD = os.getenv("DB_PASSWORD", "KqtpM7wmNd&mFKs")
|
||||
DB_HOST = os.getenv("DB_HOST", "db") # IMMER 'db' als Host im Container!
|
||||
# Dev container: use host.docker.internal or localhost if db container isn't on same network
|
||||
# Docker Compose: use 'db' service name
|
||||
DB_HOST = os.getenv("DB_HOST", "db") # Default to db for Docker Compose
|
||||
DB_NAME = os.getenv("DB_NAME", "infoscreen_by_taa")
|
||||
DB_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}"
|
||||
|
||||
|
||||
@@ -3,10 +3,22 @@ import os
|
||||
from dotenv import load_dotenv
|
||||
import bcrypt
|
||||
|
||||
# .env laden
|
||||
load_dotenv()
|
||||
# .env laden (nur in Dev)
|
||||
if os.getenv("ENV", "development") == "development":
|
||||
load_dotenv()
|
||||
|
||||
DB_URL = f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:3306/{os.getenv('DB_NAME')}"
|
||||
# Use same logic as database.py: prefer DB_CONN, fallback to individual vars
|
||||
DB_URL = os.getenv("DB_CONN")
|
||||
if not DB_URL:
|
||||
DB_USER = os.getenv("DB_USER", "infoscreen_admin")
|
||||
DB_PASSWORD = os.getenv("DB_PASSWORD")
|
||||
# In Docker Compose: DB_HOST will be 'db' from env
|
||||
# In dev container: will be 'localhost' from .env
|
||||
DB_HOST = os.getenv("DB_HOST", "db") # Default to 'db' for Docker Compose
|
||||
DB_NAME = os.getenv("DB_NAME", "infoscreen_by_taa")
|
||||
DB_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:3306/{DB_NAME}"
|
||||
|
||||
print(f"init_defaults.py connecting to: {DB_URL.split('@')[1] if '@' in DB_URL else DB_URL}")
|
||||
engine = create_engine(DB_URL, isolation_level="AUTOCOMMIT")
|
||||
|
||||
with engine.connect() as conn:
|
||||
@@ -53,6 +65,7 @@ with engine.connect() as conn:
|
||||
('video_autoplay', 'true', 'Autoplay (automatisches Abspielen) für Videos'),
|
||||
('video_loop', 'true', 'Loop (Wiederholung) für Videos'),
|
||||
('video_volume', '0.8', 'Standard Lautstärke für Videos (0.0 - 1.0)'),
|
||||
('holiday_banner_enabled', 'true', 'Ferienstatus-Banner auf Dashboard anzeigen'),
|
||||
]
|
||||
|
||||
for key, value, description in default_settings:
|
||||
|
||||
@@ -8,7 +8,7 @@ from server.permissions import admin_or_higher, require_role
|
||||
from sqlalchemy import func
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
sys.path.append('/workspace')
|
||||
|
||||
@@ -27,6 +27,14 @@ def get_grace_period():
|
||||
return int(os.environ.get("HEARTBEAT_GRACE_PERIOD_PROD", "170"))
|
||||
|
||||
|
||||
def _to_utc(dt: datetime) -> datetime:
|
||||
if dt is None:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def is_client_alive(last_alive, is_active):
|
||||
"""Berechnet, ob ein Client als alive gilt."""
|
||||
if not last_alive or not is_active:
|
||||
@@ -42,7 +50,10 @@ def is_client_alive(last_alive, is_active):
|
||||
return False
|
||||
else:
|
||||
last_alive_dt = last_alive
|
||||
return datetime.utcnow() - last_alive_dt <= timedelta(seconds=grace_period)
|
||||
# Vergleiche immer in UTC und mit tz-aware Datetimes
|
||||
last_alive_utc = _to_utc(last_alive_dt)
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
return (now_utc - last_alive_utc) <= timedelta(seconds=grace_period)
|
||||
|
||||
|
||||
@groups_bp.route("", methods=["POST"])
|
||||
|
||||
@@ -202,3 +202,66 @@ def update_supplement_table_settings():
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@system_settings_bp.route('/holiday-banner', methods=['GET'])
|
||||
def get_holiday_banner_setting():
|
||||
"""
|
||||
Get holiday banner enabled status.
|
||||
Public endpoint - dashboard needs this.
|
||||
"""
|
||||
session = Session()
|
||||
try:
|
||||
setting = session.query(SystemSetting).filter_by(key='holiday_banner_enabled').first()
|
||||
enabled = setting.value == 'true' if setting else True
|
||||
|
||||
return jsonify({'enabled': enabled}), 200
|
||||
except SQLAlchemyError as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@system_settings_bp.route('/holiday-banner', methods=['POST'])
|
||||
@admin_or_higher
|
||||
def update_holiday_banner_setting():
|
||||
"""
|
||||
Update holiday banner enabled status.
|
||||
Admin+ only.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"enabled": true/false
|
||||
}
|
||||
"""
|
||||
session = Session()
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': 'No data provided'}), 400
|
||||
|
||||
enabled = data.get('enabled', True)
|
||||
|
||||
# Update or create setting
|
||||
setting = session.query(SystemSetting).filter_by(key='holiday_banner_enabled').first()
|
||||
if setting:
|
||||
setting.value = 'true' if enabled else 'false'
|
||||
else:
|
||||
setting = SystemSetting(
|
||||
key='holiday_banner_enabled',
|
||||
value='true' if enabled else 'false',
|
||||
description='Ferienstatus-Banner auf Dashboard anzeigen'
|
||||
)
|
||||
session.add(setting)
|
||||
|
||||
session.commit()
|
||||
|
||||
return jsonify({
|
||||
'enabled': enabled,
|
||||
'message': 'Holiday banner setting updated successfully'
|
||||
}), 200
|
||||
except SQLAlchemyError as e:
|
||||
session.rollback()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user