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:
RobbStarkAustria
2025-11-29 15:35:13 +00:00
parent 6dcf93f0dd
commit df9f29bc6a
13 changed files with 399 additions and 42 deletions

View File

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

View File

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

View File

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

View File

@@ -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"])

View File

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