Preparation for first deployment-test

This commit is contained in:
2025-09-03 19:47:16 +00:00
parent 4e74f72c9f
commit e30723da0a
14 changed files with 3612 additions and 245 deletions

1
.gitignore vendored
View File

@@ -75,3 +75,4 @@ dashboard/sidebar_test.py
dashboard/assets/responsive-sidebar.css dashboard/assets/responsive-sidebar.css
certs/ certs/
sync.ffs_db sync.ffs_db
.pnpm-store/

3105
dashboard/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,56 @@
import React from 'react'; import React from 'react';
interface CustomMediaInfoPanelProps { interface CustomMediaInfoPanelProps {
mediaId: string; name: string;
title: string; size: number;
description: string; type: string;
eventId?: string; dateModified: number;
onSave: (data: { title: string; description: string; eventId?: string }) => void; description?: string | null;
} }
const CustomMediaInfoPanel: React.FC<CustomMediaInfoPanelProps> = ({ const CustomMediaInfoPanel: React.FC<CustomMediaInfoPanelProps> = ({
mediaId, name,
title, size,
type,
dateModified,
description, description,
eventId,
onSave,
}) => { }) => {
// Hier kannst du Formularfelder und Logik für die Bearbeitung einbauen function formatLocalDate(timestamp: number | undefined | null) {
if (!timestamp || isNaN(timestamp)) return '-';
const date = new Date(timestamp * 1000);
return date.toLocaleString('de-DE');
}
return ( return (
<div
style={{
padding: 16,
border: '1px solid #eee',
borderRadius: 8,
background: '#fafafa',
maxWidth: 400,
}}
>
<h3 style={{ marginBottom: 12 }}>Datei-Eigenschaften</h3>
<div> <div>
<h3>Medien-Informationen bearbeiten</h3> <b>Name:</b> {name || '-'}
{/* Formularfelder für Titel, Beschreibung, Event-Zuordnung */} </div>
<div>
<b>Typ:</b> {type || '-'}
</div>
<div>
<b>Größe:</b> {typeof size === 'number' && !isNaN(size) ? size + ' Bytes' : '-'}
</div>
<div>
<b>Geändert:</b> {formatLocalDate(dateModified)}
</div>
<div>
<b>Beschreibung:</b>{' '}
{description && description !== 'null' ? (
description
) : (
<span style={{ color: '#888' }}>Keine Beschreibung</span>
)}
</div>
</div> </div>
); );
}; };

View File

@@ -19,33 +19,43 @@ interface MediaItem {
} }
const Media: React.FC = () => { const Media: React.FC = () => {
const [mediaList, setMediaList] = useState<MediaItem[]>([]); // State für die angezeigten Dateidetails
const [selectedMedia, setSelectedMedia] = useState<MediaItem | null>(null); const [fileDetails, setFileDetails] = useState<null | {
name: string;
size: number;
type: string;
dateModified: number;
description?: string | null;
}>(null);
// Ansicht: 'LargeIcons', 'Details'
const [viewMode, setViewMode] = useState<'LargeIcons' | 'Details'>('LargeIcons');
// Medien vom Server laden // Hilfsfunktion für Datum in Browser-Zeitzone
useEffect(() => { function formatLocalDate(timestamp: number) {
fetch('/api/eventmedia') if (!timestamp) return '';
.then(res => res.json()) const date = new Date(timestamp * 1000);
.then(setMediaList); return date.toLocaleString('de-DE'); // Zeigt lokale Zeit des Browsers
}, []); }
// Speichern von Metadaten/Event-Zuordnung
const handleSave = async (data: { title: string; description: string; eventId?: string }) => {
if (!selectedMedia) return;
await fetch(`/api/eventmedia/${selectedMedia.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
// Nach dem Speichern neu laden
const res = await fetch('/api/eventmedia');
setMediaList(await res.json());
};
return ( return (
<div> <div>
<h2 className="text-xl font-bold mb-4">Medien</h2> <h2 className="text-xl font-bold mb-4">Medien</h2>
{/* Ansicht-Umschalter */}
<div style={{ marginBottom: 12 }}>
<button
className={viewMode === 'LargeIcons' ? 'e-btn e-active' : 'e-btn'}
onClick={() => setViewMode('LargeIcons')}
style={{ marginRight: 8 }}
>
Icons
</button>
<button
className={viewMode === 'Details' ? 'e-btn e-active' : 'e-btn'}
onClick={() => setViewMode('Details')}
>
Details
</button>
</div>
<FileManagerComponent <FileManagerComponent
ajaxSettings={{ ajaxSettings={{
url: hostUrl + 'operations', url: hostUrl + 'operations',
@@ -71,18 +81,29 @@ const Media: React.FC = () => {
layout: ['SortBy', 'Refresh', '|', 'View', 'Details'], layout: ['SortBy', 'Refresh', '|', 'View', 'Details'],
}} }}
allowMultiSelection={false} allowMultiSelection={false}
view={viewMode}
detailsViewSettings={{
columns: [
{ field: 'name', headerText: 'Name', minWidth: '120', width: '200' },
{ field: 'size', headerText: 'Größe', minWidth: '80', width: '100' },
{
field: 'dateModified',
headerText: 'Upload-Datum',
minWidth: '120',
width: '180',
template: (data: { dateModified: number }) => formatLocalDate(data.dateModified),
},
{ field: 'type', headerText: 'Typ', minWidth: '80', width: '100' },
],
}}
menuClick={(args: any) => {
console.log('FileManager popupOpen:', args);
}}
> >
<Inject services={[NavigationPane, DetailsView, Toolbar]} /> <Inject services={[NavigationPane, DetailsView, Toolbar]} />
</FileManagerComponent> </FileManagerComponent>
{selectedMedia && ( {/* Details-Panel anzeigen, wenn Details verfügbar sind */}
<CustomMediaInfoPanel {fileDetails && <CustomMediaInfoPanel {...fileDetails} />}
mediaId={selectedMedia.id}
title={selectedMedia.url}
description={selectedMedia.description}
eventId={selectedMedia.eventId}
onSave={handleSave}
/>
)}
</div> </div>
); );
}; };

131
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,131 @@
version: '3.8'
networks:
infoscreen-net:
driver: bridge
services:
proxy:
image: nginx:1.25
container_name: infoscreen-proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- server
- dashboard
networks:
- infoscreen-net
db:
image: mariadb:11.2
container_name: infoscreen-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/mysql
networks:
- infoscreen-net
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
mqtt:
image: eclipse-mosquitto:2.0.21
container_name: infoscreen-mqtt
restart: unless-stopped
volumes:
- ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
ports:
- "1883:1883"
- "9001:9001"
networks:
- infoscreen-net
healthcheck:
test: ["CMD-SHELL", "mosquitto_pub -h localhost -t test -m 'health' || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
# Verwende fertige Images statt Build
server:
image: ghcr.io/robbstarkaustria/infoscreen-api:latest # Oder wo auch immer Ihre Images liegen
container_name: infoscreen-api
restart: unless-stopped
depends_on:
db:
condition: service_healthy
mqtt:
condition: service_healthy
environment:
DB_CONN: "mysql+pymysql://${DB_USER}:${DB_PASSWORD}@db/${DB_NAME}"
FLASK_ENV: production
MQTT_BROKER_URL: mqtt://mqtt:1883
MQTT_USER: ${MQTT_USER}
MQTT_PASSWORD: ${MQTT_PASSWORD}
networks:
- infoscreen-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 40s
dashboard:
image: ghcr.io/robbstarkaustria/infoscreen-dashboard:latest # Oder wo auch immer Ihre Images liegen
container_name: infoscreen-dashboard
restart: unless-stopped
depends_on:
server:
condition: service_healthy
environment:
NODE_ENV: production
VITE_API_URL: ${API_URL}
networks:
- infoscreen-net
listener:
image: ghcr.io/robbstarkaustria/infoscreen-listener:latest # Oder wo auch immer Ihre Images liegen
container_name: infoscreen-listener
restart: unless-stopped
depends_on:
db:
condition: service_healthy
mqtt:
condition: service_healthy
environment:
DB_URL: mysql+pymysql://${DB_USER}:${DB_PASSWORD}@db/${DB_NAME}
networks:
- infoscreen-net
scheduler:
image: ghcr.io/robbstarkaustria/infoscreen-scheduler:latest # Oder wo auch immer Ihre Images liegen
container_name: infoscreen-scheduler
restart: unless-stopped
depends_on:
db:
condition: service_healthy
mqtt:
condition: service_healthy
environment:
DB_CONN: "mysql+pymysql://${DB_USER}:${DB_PASSWORD}@db/${DB_NAME}"
MQTT_BROKER_URL: mqtt
MQTT_PORT: 1883
networks:
- infoscreen-net
volumes:
db-data:

100
early-validation.sh Normal file
View File

@@ -0,0 +1,100 @@
#!/bin/bash
# Early Hardware Validation für 25% Entwicklungsstand
# Ziel: Architektur-Probleme früh erkennen, nicht Volltest
echo "🧪 Infoscreen Early Hardware Validation"
echo "======================================"
echo "Entwicklungsstand: ~25-30%"
echo "Ziel: Basis-Deployment + Performance-Baseline"
echo ""
# Phase 1: Quick-Setup (30 Min)
echo "📦 Phase 1: Container-Setup-Test"
echo "- Docker-Compose startet alle Services?"
echo "- Health-Checks werden grün?"
echo "- Ports sind erreichbar?"
echo ""
# Phase 2: Connectivity-Test (1 Stunde)
echo "🌐 Phase 2: Service-Kommunikation"
echo "- Database-Connection vom Server?"
echo "- MQTT-Broker empfängt Messages?"
echo "- Nginx routet zu Services?"
echo "- API-Grundendpoints antworten?"
echo ""
# Phase 3: Performance-Baseline (2 Stunden)
echo "📊 Phase 3: Performance-Snapshot"
echo "- Memory-Verbrauch pro Container"
echo "- CPU-Usage im Idle"
echo "- Startup-Zeiten messen"
echo "- Network-Latency zwischen Services"
echo ""
# Phase 4: Basic Load-Test (4 Stunden)
echo "🔥 Phase 4: Basis-Belastungstest"
echo "- 10 parallele API-Requests"
echo "- 1000 MQTT-Messages senden"
echo "- Database-Insert-Performance"
echo "- Memory-Leak-Check (1h Laufzeit)"
echo ""
# Test-Checklist erstellen
cat > early-validation-checklist.md << 'EOF'
# Early Hardware Validation Checklist
## ✅ Container-Setup
- [ ] `docker compose up -d` erfolgreich
- [ ] Alle Services zeigen "healthy" Status
- [ ] Keine Error-Logs in den ersten 5 Minuten
- [ ] Ports 80, 8000, 3306, 1883 erreichbar
## ✅ Service-Kommunikation
- [ ] Server kann zu Database verbinden
- [ ] MQTT-Test-Message wird empfangen
- [ ] Nginx zeigt Service-Status-Page
- [ ] API-Health-Endpoint antwortet (200 OK)
## ✅ Performance-Baseline
- [ ] Total Memory < 4GB bei Idle
- [ ] CPU-Usage < 10% bei Idle
- [ ] Container-Startup < 60s
- [ ] API-Response-Time < 500ms
## ✅ Basic-Load-Test
- [ ] 10 parallele Requests ohne Errors
- [ ] 1000 MQTT-Messages ohne Message-Loss
- [ ] Memory-Usage stabil über 1h
- [ ] Keine Container-Restarts
## 📊 Baseline-Metriken (dokumentieren)
- Memory pro Container: ___MB
- CPU-Usage bei Load: ___%
- API-Response-Time: ___ms
- Database-Query-Time: ___ms
- Container-Startup-Zeit: ___s
## 🚨 Gefundene Probleme
- [ ] Performance-Bottlenecks: ____________
- [ ] Memory-Issues: ____________________
- [ ] Network-Probleme: _________________
- [ ] Container-Probleme: _______________
## ✅ Architektur-Validierung
- [ ] Container-Orchestrierung funktioniert
- [ ] Service-Discovery läuft
- [ ] Volume-Mounting korrekt
- [ ] Environment-Variables werden geladen
- [ ] Health-Checks sind aussagekräftig
EOF
echo "✅ Early Validation Checklist erstellt: early-validation-checklist.md"
echo ""
echo "🎯 Erwartetes Ergebnis:"
echo "- Architektur-Probleme identifiziert"
echo "- Performance-Baseline dokumentiert"
echo "- Deployment-Prozess validiert"
echo "- Basis für spätere Tests gelegt"
echo ""
echo "⏰ Geschätzter Aufwand: 8-12 Stunden über 2-3 Tage"
echo "💰 ROI: Verhindert teure Architektur-Änderungen später"

140
hardware-test-setup.sh Normal file
View File

@@ -0,0 +1,140 @@
#!/bin/bash
# Infoscreen Hardware Test Setup für Quad-Core 16GB System
echo "🖥️ Infoscreen Hardware Test Setup"
echo "=================================="
echo "System: Quad-Core, 16GB RAM, SSD"
echo ""
# System-Info anzeigen
echo "📊 System-Information:"
echo "CPU Cores: $(nproc)"
echo "RAM Total: $(free -h | grep Mem | awk '{print $2}')"
echo "Disk Free: $(df -h / | tail -1 | awk '{print $4}')"
echo ""
# Docker-Setup
echo "🐳 Docker-Setup..."
sudo apt update -y
sudo apt install -y docker.io docker-compose-plugin
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker $USER
# Test-Verzeichnisse erstellen
echo "📁 Test-Umgebung erstellen..."
mkdir -p ~/infoscreen-hardware-test/{prod,dev,monitoring,scripts,backups}
# Performance-Monitoring-Tools
echo "📊 Monitoring-Tools installieren..."
sudo apt install -y htop iotop nethogs ncdu stress-ng
# Test-Script erstellen
cat > ~/infoscreen-hardware-test/scripts/system-monitor.sh << 'EOF'
#!/bin/bash
# System-Monitoring während Tests
echo "=== Infoscreen System Monitor ==="
echo "Zeit: $(date)"
echo ""
echo "🖥️ CPU-Info:"
echo "Load: $(uptime | awk -F'load average:' '{print $2}')"
echo "Cores: $(nproc) | Usage: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)%"
echo ""
echo "💾 Memory-Info:"
free -h
echo ""
echo "💿 Disk-Info:"
df -h /
echo ""
echo "🐳 Docker-Info:"
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo ""
echo "🌡️ System-Temperature (falls verfügbar):"
sensors 2>/dev/null || echo "lm-sensors nicht installiert"
echo ""
echo "🌐 Network-Connections:"
ss -tuln | grep :80\\\|:443\\\|:8000\\\|:3306\\\|:1883
EOF
chmod +x ~/infoscreen-hardware-test/scripts/system-monitor.sh
# Load-Test-Script erstellen
cat > ~/infoscreen-hardware-test/scripts/load-test.sh << 'EOF'
#!/bin/bash
# Load-Test für Infoscreen-System
echo "🔥 Infoscreen Load-Test startet..."
# CPU-Load erzeugen (für Thermal-Tests)
echo "CPU-Stress-Test (30s)..."
stress-ng --cpu $(nproc) --timeout 30s &
# Memory-Test
echo "Memory-Stress-Test..."
stress-ng --vm 2 --vm-bytes 2G --timeout 30s &
# Disk-I/O-Test
echo "Disk-I/O-Test..."
stress-ng --hdd 1 --hdd-bytes 1G --timeout 30s &
# Warten auf Tests
wait
echo "✅ Load-Test abgeschlossen"
EOF
chmod +x ~/infoscreen-hardware-test/scripts/load-test.sh
# Docker-Test-Setup
echo "🧪 Docker-Test-Setup..."
cat > ~/infoscreen-hardware-test/docker-compose.test.yml << 'EOF'
version: '3.8'
services:
test-web:
image: nginx:alpine
ports: ["8080:80"]
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128M
test-db:
image: mariadb:11.2
environment:
MYSQL_ROOT_PASSWORD: test123
MYSQL_DATABASE: testdb
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
test-load:
image: alpine
command: sh -c "while true; do wget -q -O- http://test-web/ > /dev/null; sleep 0.1; done"
depends_on: [test-web]
EOF
echo ""
echo "✅ Setup abgeschlossen!"
echo ""
echo "🚀 Nächste Schritte:"
echo "1. Logout/Login für Docker-Gruppe"
echo "2. Test: docker run hello-world"
echo "3. System-Monitor: ~/infoscreen-hardware-test/scripts/system-monitor.sh"
echo "4. Load-Test: ~/infoscreen-hardware-test/scripts/load-test.sh"
echo "5. Docker-Test: cd ~/infoscreen-hardware-test && docker compose -f docker-compose.test.yml up"
echo ""
echo "📁 Test-Verzeichnis: ~/infoscreen-hardware-test/"
echo "📊 Monitoring: Führen Sie system-monitor.sh parallel zu Tests aus"

View File

@@ -1,8 +1,9 @@
from sqlalchemy import ( from sqlalchemy import (
Column, Integer, String, Enum, TIMESTAMP, func, Boolean, ForeignKey, Float, Text, Index Column, Integer, String, Enum, TIMESTAMP, func, Boolean, ForeignKey, Float, Text, Index, DateTime
) )
from sqlalchemy.orm import declarative_base from sqlalchemy.orm import declarative_base
import enum import enum
from datetime import datetime, timezone
Base = declarative_base() Base = declarative_base()
@@ -129,6 +130,7 @@ class EventMedia(Base):
url = Column(String(255), nullable=False) url = Column(String(255), nullable=False)
file_path = Column(String(255), nullable=True) file_path = Column(String(255), nullable=True)
message_content = Column(Text, nullable=True) message_content = Column(Text, nullable=True)
uploaded_at = Column(TIMESTAMP, nullable=False, default=lambda: datetime.now(timezone.utc))
def to_dict(self): def to_dict(self):
return { return {

View File

@@ -1,7 +1,7 @@
events {} events {}
http { http {
upstream dashboard { upstream dashboard {
server 127.0.0.1:3000; server infoscreen-dashboard:80;
} }
upstream infoscreen_api { upstream infoscreen_api {
server infoscreen-api:8000; server infoscreen-api:8000;

View File

@@ -0,0 +1,38 @@
"""Change uploaded_at to TIMESTAMP in EventMedia
Revision ID: 216402147826
Revises: b22d339ed2af
Create Date: 2025-09-01 10:22:55.285710
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = '216402147826'
down_revision: Union[str, None] = 'b22d339ed2af'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('event_media', 'uploaded_at',
existing_type=mysql.DATETIME(),
type_=sa.TIMESTAMP(),
nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('event_media', 'uploaded_at',
existing_type=sa.TIMESTAMP(),
type_=mysql.DATETIME(),
nullable=True)
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""Add uploaded_at to EventMedia
Revision ID: b22d339ed2af
Revises: e6eaede720aa
Create Date: 2025-09-01 10:07:46.915640
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b22d339ed2af'
down_revision: Union[str, None] = 'e6eaede720aa'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('event_media', sa.Column('uploaded_at', sa.DateTime(timezone=True), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('event_media', 'uploaded_at')
# ### end Alembic commands ###

View File

@@ -36,16 +36,54 @@ def filemanager_operations():
if action == 'read': if action == 'read':
# List files and folders # List files and folders
items = [] items = []
session = Session()
for entry in os.scandir(full_path): for entry in os.scandir(full_path):
items.append({ item = {
'name': entry.name, 'name': entry.name,
'isFile': entry.is_file(), 'isFile': entry.is_file(),
'size': entry.stat().st_size, 'size': entry.stat().st_size,
'dateModified': entry.stat().st_mtime,
'type': os.path.splitext(entry.name)[1][1:] if entry.is_file() else '', 'type': os.path.splitext(entry.name)[1][1:] if entry.is_file() else '',
'hasChild': entry.is_dir() 'hasChild': entry.is_dir()
}) }
# Wenn Datei, versuche Upload-Datum aus DB zu holen
if entry.is_file():
media = session.query(EventMedia).filter_by(
url=entry.name).first()
if media and media.uploaded_at:
# FileManager erwartet UNIX-Timestamp (Sekunden)
item['dateModified'] = int(media.uploaded_at.timestamp())
else:
item['dateModified'] = entry.stat().st_mtime
else:
item['dateModified'] = entry.stat().st_mtime
items.append(item)
session.close()
return jsonify({'files': items, 'cwd': {'name': os.path.basename(full_path), 'path': path}}) return jsonify({'files': items, 'cwd': {'name': os.path.basename(full_path), 'path': path}})
elif action == 'details':
# Details für eine oder mehrere Dateien zurückgeben
names = request.form.getlist('names[]') or (request.json.get(
'names') if request.is_json and request.json else [])
path = get_param('path', '/')
details = []
session = Session()
for name in names:
file_path = os.path.join(MEDIA_ROOT, path.lstrip('/'), name)
media = session.query(EventMedia).filter_by(url=name).first()
if os.path.isfile(file_path):
detail = {
'name': name,
'size': os.path.getsize(file_path),
'dateModified': int(media.uploaded_at.timestamp()) if media and media.uploaded_at else int(os.path.getmtime(file_path)),
'type': os.path.splitext(name)[1][1:],
'hasChild': False,
'isFile': True,
'description': media.message_content if media else '',
# weitere Felder nach Bedarf
}
details.append(detail)
session.close()
return jsonify({'details': details})
elif action == 'delete': elif action == 'delete':
for item in request.form.getlist('names[]'): for item in request.form.getlist('names[]'):
item_path = os.path.join(full_path, item) item_path = os.path.join(full_path, item)
@@ -88,10 +126,12 @@ def filemanager_upload():
media_type = MediaType(ext) media_type = MediaType(ext)
except ValueError: except ValueError:
media_type = MediaType.other media_type = MediaType.other
from datetime import datetime, timezone
media = EventMedia( media = EventMedia(
media_type=media_type, media_type=media_type,
url=file.filename, url=file.filename,
file_path=os.path.relpath(file_path, MEDIA_ROOT) file_path=os.path.relpath(file_path, MEDIA_ROOT),
uploaded_at=datetime.now(timezone.utc)
) )
session.add(media) session.add(media)
session.commit() session.commit()

38
setup-deployment.sh Normal file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Minimaler Setup für Produktions-Deployment
# Dieser Script erstellt nur die nötigen Dateien für Container-Deployment
echo "🚀 Infoscreen Production Deployment Setup"
# 1. Deployment-Ordner erstellen
mkdir -p deployment/{certs,config}
# 2. Produktions docker-compose kopieren
cp docker-compose.prod.yml deployment/
cp .env deployment/
cp nginx.conf deployment/
# 3. Mosquitto-Konfiguration erstellen
cat > deployment/mosquitto.conf << 'EOF'
listener 1883
allow_anonymous true
listener 9001
protocol websockets
EOF
# 4. SSL-Zertifikate kopieren (falls vorhanden)
if [ -f "certs/dev.crt" ] && [ -f "certs/dev.key" ]; then
cp certs/* deployment/certs/
echo "✅ SSL-Zertifikate kopiert"
else
echo "⚠️ SSL-Zertifikate fehlen - werden auf Zielmaschine erstellt"
fi
echo ""
echo "📦 Deployment-Paket erstellt in ./deployment/"
echo ""
echo "Nächste Schritte:"
echo "1. Kopieren Sie den 'deployment'-Ordner auf den Zielserver"
echo "2. Images bereitstellen (Registry oder TAR-Export)"
echo "3. docker compose -f docker-compose.prod.yml up -d"

78
test-vm-setup.sh Normal file
View File

@@ -0,0 +1,78 @@
#!/bin/bash
# Quick VM Setup Script für Infoscreen Deployment Test
echo "🧪 Infoscreen VM Test Setup"
echo "=========================="
# System Update
echo "📦 System aktualisieren..."
sudo apt update -y
sudo apt upgrade -y
# Docker Installation
echo "🐳 Docker installieren..."
sudo apt install -y docker.io docker-compose-plugin curl wget htop
# Docker aktivieren
sudo systemctl enable docker
sudo systemctl start docker
# User zu Docker-Gruppe hinzufügen
sudo usermod -aG docker $USER
# Firewall konfigurieren
echo "🔥 Firewall konfigurieren..."
sudo ufw --force enable
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
# Test-Verzeichnis erstellen
mkdir -p ~/infoscreen-test
cd ~/infoscreen-test
# Basis-Konfiguration erstellen
cat > docker-compose.test.yml << 'EOF'
version: '3.8'
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx-test.conf:/etc/nginx/nginx.conf:ro
test-api:
image: httpd:alpine
environment:
- TEST=true
EOF
cat > nginx-test.conf << 'EOF'
events {}
http {
server {
listen 80;
location / {
return 200 "✅ VM Test erfolgreich!\n";
add_header Content-Type text/plain;
}
}
}
EOF
echo ""
echo "✅ VM Setup abgeschlossen!"
echo ""
echo "Nächste Schritte:"
echo "1. Logout/Login für Docker-Gruppe"
echo "2. Test: docker run hello-world"
echo "3. Test: docker compose -f docker-compose.test.yml up -d"
echo "4. Test: curl http://localhost"
echo "5. Echtes Deployment: Dateien übertragen und starten"
echo ""
echo "🔍 System-Info:"
echo "Docker: $(docker --version)"
echo "Compose: $(docker compose version)"
echo "RAM: $(free -h | grep Mem | awk '{print $2}')"
echo "Disk: $(df -h / | tail -1 | awk '{print $4}') frei"