diff --git a/.gitignore b/.gitignore index 4ce29a1..69ffbd5 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,5 @@ desktop.ini received_screenshots/ mosquitto/ -alte/ \ No newline at end of file +alte/ +screenshots/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index de61e9b..0000000 --- a/Dockerfile +++ /dev/null @@ -1,56 +0,0 @@ -```dockerfile -# Use a stable Python base image -FROM python:3.11-slim - -# Build arguments for host user mapping -ARG USER_ID=1000 -ARG GROUP_ID=1000 - -# Create non-root user -RUN groupadd -g ${GROUP_ID} infoscreen_taa \ - && useradd -u ${USER_ID} -g ${GROUP_ID} --shell /bin/bash --create-home infoscreen_taa - -# Ensure user exists -RUN getent passwd infoscreen_taa - -# Install locale dependencies and generate UTF-8 locale -RUN apt-get update && apt-get install -y locales \ - && sed -i 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen \ - && locale-gen - -# Set environment variables for locale -ENV LANG=de_DE.UTF-8 \ - LANGUAGE=de_DE:de \ - LC_ALL=de_DE.UTF-8 - -# Enable Dash debug during development -ENV DASH_DEBUG_MODE=True - -# Working directory inside container -WORKDIR /app # entspricht mount in devcontainer.json - -# Copy only requirements first for efficient caching -COPY server/requirements-dev.txt ./ - -# Install dev dependencies under the non-root user -USER infoscreen_taa -RUN pip install --upgrade pip \ - && pip install --user -r requirements-dev.txt - -# Switch back to root to copy source files and fix permissions -USER root - -# Copy the server application code into /app -COPY server/ /app -RUN chown -R infoscreen_taa:infoscreen_taa /app - -# Create config directory under the non-root user's home -RUN mkdir -p /home/infoscreen_taa/.config/Infoscreen-Server \ - && chown -R infoscreen_taa:infoscreen_taa /home/infoscreen_taa/.config/Infoscreen-Server - -# Expose development ports -EXPOSE 8000 8050 - -# Use a long-running process so the container stays alive -CMD ["tail", "-f", "/dev/null"] -``` diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile index f2335b4..0ebca70 100644 --- a/dashboard/Dockerfile +++ b/dashboard/Dockerfile @@ -10,7 +10,7 @@ WORKDIR /app # --- Systemabhängigkeiten installieren (falls benötigt) --- RUN apt-get update \ && apt-get install -y --no-install-recommends \ - build-essential \ + build-essential git \ && rm -rf /var/lib/apt/lists/* # --- Python-Abhängigkeiten kopieren und installieren --- diff --git a/dashboard/Dockerfile.dev b/dashboard/Dockerfile.dev index 22aa010..4c0e404 100644 --- a/dashboard/Dockerfile.dev +++ b/dashboard/Dockerfile.dev @@ -28,10 +28,12 @@ WORKDIR /app # Kopiere nur Requirements für schnellen Rebuild COPY requirements.txt ./ +COPY requirements-dev.txt ./ # Installiere Abhängigkeiten RUN pip install --upgrade pip \ - && pip install --no-cache-dir -r requirements.txt + && pip install --no-cache-dir -r requirements.txt \ + && pip install --no-cache-dir -r requirements-dev.txt # Setze Entwicklungs-Modus ENV DASH_DEBUG_MODE=True @@ -39,9 +41,10 @@ ENV API_URL=http://server:8000/api # Ports für Dash und optional Live-Reload EXPOSE 8050 +EXPOSE 5678 # Wechsle zum non-root User USER infoscreen_taa # Dev-Start: Dash mit Hot-Reload -CMD ["tail", "-f", "/dev/null"] +CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5637", "app.py"] diff --git a/dashboard/app.py b/dashboard/app.py index 10bb02b..d75a3dd 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -1,4 +1,6 @@ # dashboard/app.py +import sys +sys.path.append('/workspace') from dash import Dash, html, dcc, page_container from flask import Flask diff --git a/dashboard/callbacks/overview_callbacks.py b/dashboard/callbacks/overview_callbacks.py index ee35494..513d183 100644 --- a/dashboard/callbacks/overview_callbacks.py +++ b/dashboard/callbacks/overview_callbacks.py @@ -1,8 +1,9 @@ # dashboard/callbacks/overview_callbacks.py import sys -print(sys.path) +sys.path.append('/workspace') import threading import dash +import requests from dash import Input, Output, State, MATCH, html, dcc from flask import session from utils.db import get_session # Diese Funktion muss eine SQLAlchemy-Session liefern! @@ -10,7 +11,8 @@ from utils.mqtt_client import publish, start_loop from config import ENV import dash_bootstrap_components as dbc import os -from server.models import Client + +API_BASE_URL = os.getenv("API_BASE_URL", "http://infoscreen-api:8000") mqtt_thread_started = False SCREENSHOT_DIR = "received-screenshots" @@ -23,46 +25,48 @@ def ensure_mqtt_running(): mqtt_thread_started = True def get_latest_screenshot(client_uuid): - prefix = f"{client_uuid}_" + # TODO: Hier genau im Produkitv-Modus die IPs testen! + # Wenn API_BASE_URL auf "http" beginnt, absolute URL verwenden (z.B. im lokalen Dev) + if API_BASE_URL.startswith("http"): + return f"{API_BASE_URL}/screenshots/{client_uuid}" + # Sonst relative URL (nginx-Proxy übernimmt das Routing) + return f"/screenshots/{client_uuid}" + +def fetch_clients(): try: - files = [f for f in os.listdir('..', SCREENSHOT_DIR) if f.startswith(prefix)] - if not files: - return "/assets/placeholder.png" - latest = max(files, key=lambda x: os.path.getmtime(os.path.join('.', SCREENSHOT_DIR, x))) - return f"/received-screenshots/{latest}" - except Exception: - return "/assets/placeholder.png" + resp = requests.get(f"{API_BASE_URL}/api/clients") + resp.raise_for_status() + return resp.json() + except Exception as e: + print("Fehler beim Abrufen der Clients:", e) + return [] @dash.callback( Output("clients-cards-container", "children"), Input("interval-update", "n_intervals") ) def update_clients(n): - # Auto-Login im Development-Modus - if "role" not in session: - if ENV == "development": - session["role"] = "admin" - else: - return dcc.Location(id="redirect-login", href="/login") - + # ... Session-Handling wie gehabt ... ensure_mqtt_running() - session_db = get_session() - clients = session_db.query(Client).all() - session_db.close() + clients = fetch_clients() cards = [] for client in clients: - uuid = client.uuid + uuid = client["uuid"] + # screenshot = get_latest_screenshot(uuid) screenshot = get_latest_screenshot(uuid) + # if screenshot[-3] != "jpg": + # screenshot += ".jpg" + print(f"UUID: {uuid}, Screenshot: {screenshot}") card = dbc.Card( [ - dbc.CardHeader(client.location or client.hardware_hash), + dbc.CardHeader(client["location"]), dbc.CardBody([ html.Img( src=screenshot, style={"width": "160px", "height": "90px", "object-fit": "cover"}, ), - html.P(f"IP: {client.ip_address or '-'}", className="card-text"), - html.P(f"Letzte Aktivität: {client.last_alive or '-'}", className="card-text"), + html.P(f"IP: {client["ip_address"] or '-'}", className="card-text"), + html.P(f"Letzte Aktivität: {client["last_alive"] or '-'}", className="card-text"), dbc.ButtonGroup([ dbc.Button("Reload Page", color="primary", id={"type": "btn-reload", "index": uuid}, n_clicks=0), dbc.Button("Restart Client", color="danger", id={"type": "btn-restart", "index": uuid}, n_clicks=0), diff --git a/dashboard/requirements-dev.txt b/dashboard/requirements-dev.txt new file mode 100644 index 0000000..8ccada8 --- /dev/null +++ b/dashboard/requirements-dev.txt @@ -0,0 +1 @@ +debugpy \ No newline at end of file diff --git a/dashboard/requirements.txt b/dashboard/requirements.txt index 97f2c9c..7447fd4 100644 --- a/dashboard/requirements.txt +++ b/dashboard/requirements.txt @@ -8,3 +8,5 @@ full-calendar-component>=0.0.4 pandas>=2.2.3 paho-mqtt>=2.1.0 python-dotenv>=1.1.0 +PyMySQL>=1.1.1 +SQLAlchemy>=2.0.41 diff --git a/dashboard/utils/mqtt_client.py b/dashboard/utils/mqtt_client.py index e8e1e27..1ab151a 100644 --- a/dashboard/utils/mqtt_client.py +++ b/dashboard/utils/mqtt_client.py @@ -5,6 +5,7 @@ import threading import time from dotenv import load_dotenv import paho.mqtt.client as mqtt +import random # 1. Laden der Umgebungsvariablen aus .env load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), "..", ".env")) @@ -15,7 +16,9 @@ MQTT_BROKER_PORT = int(os.getenv("MQTT_BROKER_PORT", "1883")) MQTT_USERNAME = os.getenv("MQTT_USERNAME", None) MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", None) MQTT_KEEPALIVE = int(os.getenv("MQTT_KEEPALIVE", "60")) -MQTT_CLIENT_ID = os.getenv("MQTT_CLIENT_ID", f"dash-{int(time.time())}") +base_id = os.getenv("MQTT_CLIENT_ID", "dash") +unique_part = f"{os.getpid()}_{random.randint(1000,9999)}" +MQTT_CLIENT_ID = f"{base_id}-{unique_part}" # 3. Erstelle eine globale Client‐Instanz client = mqtt.Client(client_id=MQTT_CLIENT_ID) diff --git a/docker-compose.yml b/docker-compose.yml index e37525d..ada2fca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,19 @@ networks: driver: bridge services: + proxy: + image: nginx:1.25 + container_name: infoscreen-proxy + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - server + - dashboard + networks: + - infoscreen-net db: image: mariadb:11.4.7 container_name: infoscreen-db diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..457383e --- /dev/null +++ b/nginx.conf @@ -0,0 +1,24 @@ +events {} +http { + upstream dashboard { + server infoscreen-dashboard:8050; + } + server { + listen 80; + server_name _; + + location /api/ { + proxy_pass http://infoscreen-api:8000/api/; + } + location /screenshots/ { + proxy_pass http://infoscreen-api:8000/screenshots/; + } + location / { + proxy_pass http://dashboard; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile index a62ef94..d649576 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -10,7 +10,7 @@ WORKDIR /app # --- Systemabhängigkeiten für MariaDB-Client & Locale --- RUN apt-get update \ && apt-get install -y --no-install-recommends \ - libmariadb-dev-compat libmariadb-dev locales \ + libmariadb-dev-compat libmariadb-dev locales git\ && rm -rf /var/lib/apt/lists/* # --- Locale konfigurieren --- diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev index b13c4d2..f9553e8 100644 --- a/server/Dockerfile.dev +++ b/server/Dockerfile.dev @@ -34,6 +34,7 @@ RUN pip install --upgrade pip \ # Expose Ports für Flask API EXPOSE 8000 +EXPOSE 5678 # Setze Env für Dev ENV FLASK_ENV=development @@ -43,4 +44,4 @@ ENV ENV_FILE=.env USER infoscreen_taa # Default Command: Flask Development Server -CMD ["flask", "run", "--host=0.0.0.0", "--port=8000"] +CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "wsgi.py"] diff --git a/server/mqtt_multitopic_receiver.py b/server/mqtt_multitopic_receiver.py index f3088d7..24d54d5 100644 --- a/server/mqtt_multitopic_receiver.py +++ b/server/mqtt_multitopic_receiver.py @@ -1,15 +1,19 @@ +import sys +sys.path.append('/workspace') import os import json import base64 import glob -from datetime import datetime, timezone -# import paho.mqtt.client as mqtt +from datetime import datetime from paho.mqtt import client as mqtt_client -import pytz from sqlalchemy import create_engine, func from sqlalchemy.orm import sessionmaker from models import Client, Base from helpers.check_folder import ensure_folder_exists +import shutil + +# Basisverzeichnis relativ zum aktuellen Skript +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # Konfiguration MQTT_BROKER = os.getenv("MQTT_BROKER_HOST", "localhost") @@ -27,24 +31,26 @@ topics = [ ("infoscreen/heartbeat", 0), # ... weitere Topics hier ] -SAVE_DIR = "received_screenshots" + +# Verzeichnisse für Screenshots +RECEIVED_DIR = os.path.join(BASE_DIR, "received_screenshots") +LATEST_DIR = os.path.join(BASE_DIR, "screenshots") MAX_PER_CLIENT = 20 -# Ordner für empfangene Screenshots anlegen -ensure_folder_exists(SAVE_DIR) +# Ordner für empfangene Screenshots und den neuesten Screenshot anlegen +ensure_folder_exists(RECEIVED_DIR) +ensure_folder_exists(LATEST_DIR) # Datenbank konfigurieren (MariaDB) -# Ersetze user, password, host und datenbankname entsprechend. DB_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}" engine = create_engine(DB_URL, echo=False) Session = sessionmaker(bind=engine) -# Falls Tabellen noch nicht existieren Base.metadata.create_all(engine) def prune_old_screenshots(client_id: str): """Löscht alte Screenshots, wenn mehr als MAX_PER_CLIENT vorhanden sind.""" - pattern = os.path.join(SAVE_DIR, f"{client_id}_*.jpg") + pattern = os.path.join(RECEIVED_DIR, f"{client_id}_*.jpg") files = sorted(glob.glob(pattern), key=os.path.getmtime) while len(files) > MAX_PER_CLIENT: oldest = files.pop(0) @@ -68,12 +74,17 @@ def handle_screenshot(msg): # Dateiname mit Client-ID und Zeitstempel filename = ts.strftime(f"{client_id}_%Y%m%d_%H%M%S.jpg") - filepath = os.path.join(SAVE_DIR, filename) + received_path = os.path.join(RECEIVED_DIR, filename) - # Bild speichern - with open(filepath, "wb") as f: + # Bild im Verzeichnis "received_screenshots" speichern + with open(received_path, "wb") as f: f.write(img_data) - print(f"Bild gespeichert: {filepath}") + print(f"Bild gespeichert: {received_path}") + + # Kopiere den neuesten Screenshot in das Verzeichnis "screenshots" + latest_path = os.path.join(LATEST_DIR, f"{client_id}.jpg") + shutil.copy(received_path, latest_path) + print(f"Neuester Screenshot aktualisiert: {latest_path}") # Alte Screenshots beschneiden prune_old_screenshots(client_id) @@ -81,6 +92,7 @@ def handle_screenshot(msg): except Exception as e: print("Fehler beim Verarbeiten der Screenshot-Nachricht:", e) + def handle_heartbeat(msg): """Verarbeitet Heartbeat und aktualisiert oder legt Clients an.""" session = Session() @@ -100,7 +112,6 @@ def handle_heartbeat(msg): else: # Neuer Client: Location per input abfragen location = input(f"Neuer Client {uuid} gefunden. Bitte Standort eingeben: ") - # ip_address = msg._sock.getpeername()[0] new_client = Client( uuid=uuid, hardware_hash=hardware_hash, diff --git a/server/requirements-dev.txt b/server/requirements-dev.txt index a1cd17c..63446bd 100644 --- a/server/requirements-dev.txt +++ b/server/requirements-dev.txt @@ -1 +1,2 @@ -python-dotenv>=1.1.0 \ No newline at end of file +python-dotenv>=1.1.0 +debugpy diff --git a/server/wsgi.py b/server/wsgi.py index 9cc1e40..6662a93 100644 --- a/server/wsgi.py +++ b/server/wsgi.py @@ -1,6 +1,20 @@ # server/wsgi.py +import glob +import os +from flask import Flask, jsonify, send_from_directory +from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine +from models import Client, Base -from flask import Flask, jsonify +DB_USER = os.getenv("DB_USER") +DB_PASSWORD = os.getenv("DB_PASSWORD") +DB_HOST = os.getenv("DB_HOST") +DB_NAME = os.getenv("DB_NAME") + +# Datenbank-Engine und Session anlegen (passe ggf. die DB-URL an) +DB_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}" +engine = create_engine(DB_URL, echo=False) +Session = sessionmaker(bind=engine) app = Flask(__name__) @@ -14,3 +28,40 @@ def index(): return "Hello from Infoscreen‐API!" # (Weitere Endpunkte, Blueprints, Datenbank-Initialisierung usw. kommen hierher) +@app.route("/screenshots/") +def get_screenshot(uuid): + """Liefert den aktuellen Screenshot für die angegebene UUID zurück.""" + print(f"Anfrage für Screenshot mit UUID: {uuid}") + pattern = os.path.join("screenshots", f"{uuid}*.jpg") + files = glob.glob(pattern) + if not files: + return jsonify({"error": "Screenshot not found"}), 404 + # Es gibt nur eine Datei pro UUID + filename = os.path.basename(files[0]) + print(filename) + print("Arbeitsverzeichnis:", os.getcwd()) + print("Suchmuster:", pattern) + print("Gefundene Dateien:", files) + return send_from_directory("screenshots", filename) + +@app.route("/api/clients") +def get_clients(): + # from models import Client # Import lokal, da im selben Container + print("Abrufen der Clients aus der Datenbank...") + session = Session() + clients = session.query(Client).all() + result = [ + { + "uuid": c.uuid, + "location": c.location, + "hardware_hash": c.hardware_hash, + "ip_address": c.ip_address, + "last_alive": c.last_alive.isoformat() if c.last_alive else None + } + for c in clients + ] + session.close() + return jsonify(result) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000, debug=True) \ No newline at end of file