feat: presentation defaults + scheduler active-only

Add Settings → Events (Presentations) defaults (interval, page-progress,
auto-progress) persisted via /api/system-settings
Seed defaults in init_defaults.py (10/true/true)
Add Event.page_progress and Event.auto_progress (Alembic applied)
CustomEventModal applies defaults on create and saves fields
Scheduler publishes only currently active events per group, clears retained
topics when none, normalizes times to UTC; include flags in payloads
Docs: update README, copilot instructions, and DEV-CHANGELOG
If you can split the commit, even better

feat(dashboard): add presentation defaults UI
feat(api): seed presentation defaults in init_defaults.py
feat(model): add Event.page_progress and Event.auto_progress
feat(scheduler): publish only active events; clear retained topics; UTC
docs: update README and copilot-instructions
chore: update DEV-CHANGELOG
This commit is contained in:
RobbStarkAustria
2025-10-18 15:34:52 +00:00
parent 3487d33a2f
commit c9cc535fc6
12 changed files with 316 additions and 80 deletions

View File

@@ -1,6 +1,6 @@
{
"appName": "Infoscreen-Management",
"version": "2025.1.0-alpha.11",
"version": "2025.1.0-alpha.12",
"copyright": "© 2025 Third-Age-Applications",
"supportContact": "support@third-age-applications.com",
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
@@ -26,10 +26,21 @@
]
},
"buildInfo": {
"buildDate": "2025-09-20T11:00:00Z",
"commitId": "8d1df7199cb7"
"buildDate": "2025-10-18T14:00:00Z",
"commitId": "9f2ae8b44c3a"
},
"changelog": [
{
"version": "2025.1.0-alpha.12",
"date": "2025-10-18",
"changes": [
"✨ Einstellungen Events Präsentationen: Neue Felder für Slide-Show Intervall, Seitenfortschritt (Page-Progress) und Präsentationsfortschritt (Auto-Progress) inspiriert von Impressive Presenter (-q, -k).",
" Event-Modal: Präsentations-Einstellungen werden beim Erstellen aus globalen Defaults geladen; beim Bearbeiten aus Event-Daten; individuell pro Event anpassbar.",
"🐛 Bugfix: Scheduler sendet jetzt leere retained Messages (`[]`) wenn keine Events mehr aktiv sind (Client-Display wird korrekt gelöscht).",
"🔧 Bugfix: Nur aktuell aktive Events werden via MQTT an Clients gesendet (reduziert Datenübertragung).",
"📖 Doku: Copilot-Instructions um Präsentations-Settings, Scheduler-Logik und Event-Modal erweitert."
]
},
{
"version": "2025.1.0-alpha.11",
"date": "2025-10-16",
@@ -37,8 +48,7 @@
"✨ Einstellungen-Seite: Neues Tab-Layout (Syncfusion) mit rollenbasierter Sichtbarkeit Tabs: 📅 Akademischer Kalender, 🖥️ Anzeige & Clients, 🎬 Medien & Dateien, 🗓️ Events, ⚙️ System.",
"🗓️ Einstellungen Events: WebUntis/Vertretungsplan Zusatz-Tabelle (URL) in den Events-Tab verschoben; Aktivieren/Deaktivieren, Speichern und Vorschau; systemweite Einstellung.",
"📅 Einstellungen Akademischer Kalender: Aktive akademische Periode kann direkt gesetzt werden.",
"🛠️ Einstellungen (Technik): API-Aufrufe nutzen nun relative /apiPfade über den ViteProxy (verhindert CORS bzw. doppeltes /api).",
"📖 Doku: README zur Einstellungen-Seite (Tabs) und System-Settings-API ergänzt."
" Doku: README zur Einstellungen-Seite (Tabs) und System-Settings-API ergänzt."
]
},
{
@@ -77,8 +87,7 @@
"date": "2025-09-21",
"changes": [
"🧭 UI: Periode-Auswahl (Syncfusion) neben Gruppenauswahl; kompaktes Layout",
"✅ Anzeige: Abzeichen für vorhandenen Ferienplan + Zähler Ferien im Blick",
"🛠️ API: Endpunkte für akademische Perioden (list, active GET/POST, for_date)",
"✅ Anzeige: Abzeichen für vorhandenen Ferienplan + Zähler 'Ferien im Blick'",
"📅 Scheduler: Standardmäßig keine Terminierung in Ferien; Block-Darstellung wie Ganztagesereignis; schwarze Textfarbe",
"📤 Ferien: Upload von TXT/CSV (headless TXT nutzt Spalten 24)",
"🔧 UX: Schalter in einer Reihe; Dropdown-Breiten optimiert"
@@ -89,11 +98,8 @@
"date": "2025-09-20",
"changes": [
"🗓️ NEU: Akademische Perioden System - Unterstützung für Schuljahre, Semester und Trimester",
"🏗️ DATENBANK: Neue 'academic_periods' Tabelle für zeitbasierte Organisation",
"🔗 ERWEITERT: Events und Medien können jetzt optional einer akademischen Periode zugeordnet werden",
"📊 ARCHITEKTUR: Vollständig rückwärtskompatible Implementierung für schrittweise Einführung",
"🎯 BILDUNG: Fokus auf Schulumgebung mit Erweiterbarkeit für Hochschulen",
"⚙️ TOOLS: Automatische Erstellung von Standard-Schuljahren für österreichische Schulen"
"🎯 BILDUNG: Fokus auf Schulumgebung mit Erweiterbarkeit für Hochschulen"
]
},
{

View File

@@ -21,6 +21,8 @@ type CustomEventData = {
skipHolidays: boolean;
media?: { id: string; path: string; name: string } | null; // <--- ergänzt
slideshowInterval?: number; // <--- ergänzt
pageProgress?: boolean; // NEU
autoProgress?: boolean; // NEU
websiteUrl?: string; // <--- ergänzt
};
@@ -38,8 +40,7 @@ type CustomEventModalProps = {
groupName: string | { id: string | null; name: string };
groupColor?: string;
editMode?: boolean;
blockHolidays?: boolean;
isHolidayRange?: (start: Date, end: Date) => boolean;
// Removed unused blockHolidays and isHolidayRange
};
const weekdayOptions = [
@@ -68,8 +69,6 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
groupName,
groupColor,
editMode,
blockHolidays,
isHolidayRange,
}) => {
const [title, setTitle] = React.useState(initialData.title || '');
const [startDate, setStartDate] = React.useState(initialData.startDate || null);
@@ -98,9 +97,20 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
path: string;
name: string;
} | null>(null);
// General settings state for presentation
// Removed unused generalLoaded and setGeneralLoaded
// Removed unused generalLoaded/generalSlideshowInterval/generalPageProgress/generalAutoProgress
// Per-event state
const [slideshowInterval, setSlideshowInterval] = React.useState<number>(
initialData.slideshowInterval ?? 10
);
const [pageProgress, setPageProgress] = React.useState<boolean>(
initialData.pageProgress ?? true
);
const [autoProgress, setAutoProgress] = React.useState<boolean>(
initialData.autoProgress ?? true
);
const [websiteUrl, setWebsiteUrl] = React.useState<string>(initialData.websiteUrl ?? '');
const [mediaModalOpen, setMediaModalOpen] = React.useState(false);
@@ -182,39 +192,7 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
if (type === 'website') {
if (!websiteUrl.trim()) newErrors.websiteUrl = 'Webseiten-URL ist erforderlich';
}
// Holiday blocking: prevent creating when range overlaps
if (
!editMode &&
blockHolidays &&
startDate &&
startTime &&
endTime &&
typeof isHolidayRange === 'function'
) {
const s = new Date(
startDate.getFullYear(),
startDate.getMonth(),
startDate.getDate(),
startTime.getHours(),
startTime.getMinutes()
);
const e = new Date(
startDate.getFullYear(),
startDate.getMonth(),
startDate.getDate(),
endTime.getHours(),
endTime.getMinutes()
);
if (isHolidayRange(s, e)) {
newErrors.startDate = 'Dieser Zeitraum liegt in den Ferien und ist gesperrt.';
}
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
// Holiday blocking logic removed (blockHolidays, isHolidayRange no longer used)
setErrors({});
@@ -269,7 +247,6 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
startDate,
startTime,
endTime,
// Initialize required fields
repeat: isSingleOccurrence ? false : repeat,
weekdays: isSingleOccurrence ? [] : weekdays,
repeatUntil: isSingleOccurrence ? null : repeatUntil,
@@ -284,6 +261,8 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
if (type === 'presentation') {
payload.event_media_id = media?.id;
payload.slideshow_interval = slideshowInterval;
payload.page_progress = pageProgress;
payload.auto_progress = autoProgress;
}
if (type === 'website') {
@@ -596,6 +575,20 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
value={String(slideshowInterval)}
change={e => setSlideshowInterval(Number(e.value))}
/>
<div style={{ marginTop: 8 }}>
<CheckBoxComponent
label="Seitenfortschritt anzeigen"
checked={pageProgress}
change={e => setPageProgress(e.checked || false)}
/>
</div>
<div style={{ marginTop: 8 }}>
<CheckBoxComponent
label="Automatischer Fortschritt"
checked={autoProgress}
change={e => setAutoProgress(e.checked || false)}
/>
</div>
</div>
)}
{type === 'website' && (

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { TabComponent, TabItemDirective, TabItemsDirective } from '@syncfusion/ej2-react-navigations';
import { NumericTextBoxComponent } from '@syncfusion/ej2-react-inputs';
import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons';
@@ -12,6 +13,49 @@ import { listAcademicPeriods, getActiveAcademicPeriod, setActiveAcademicPeriod,
import { Link } from 'react-router-dom';
const Einstellungen: React.FC = () => {
// Presentation settings state
const [presentationInterval, setPresentationInterval] = React.useState(10);
const [presentationPageProgress, setPresentationPageProgress] = React.useState(true);
const [presentationAutoProgress, setPresentationAutoProgress] = React.useState(true);
const [presentationBusy, setPresentationBusy] = React.useState(false);
// Load settings from backend
const loadPresentationSettings = React.useCallback(async () => {
try {
const keys = [
'presentation_interval',
'presentation_page_progress',
'presentation_auto_progress',
];
const results = await Promise.all(keys.map(k => import('./apiSystemSettings').then(m => m.getSetting(k))));
setPresentationInterval(Number(results[0].value) || 10);
setPresentationPageProgress(results[1].value === 'true');
setPresentationAutoProgress(results[2].value === 'true');
} catch {
showToast('Fehler beim Laden der Präsentations-Einstellungen', 'e-toast-danger');
}
}, []);
React.useEffect(() => {
loadPresentationSettings();
}, [loadPresentationSettings]);
const onSavePresentationSettings = async () => {
setPresentationBusy(true);
try {
const api = await import('./apiSystemSettings');
await Promise.all([
api.updateSetting('presentation_interval', String(presentationInterval)),
api.updateSetting('presentation_page_progress', presentationPageProgress ? 'true' : 'false'),
api.updateSetting('presentation_auto_progress', presentationAutoProgress ? 'true' : 'false'),
]);
showToast('Präsentations-Einstellungen gespeichert', 'e-toast-success');
} catch {
showToast('Fehler beim Speichern der Präsentations-Einstellungen', 'e-toast-danger');
} finally {
setPresentationBusy(false);
}
};
const { user } = useAuth();
const toastRef = React.useRef<ToastComponent>(null);
@@ -358,8 +402,44 @@ const Einstellungen: React.FC = () => {
<div className="e-card-header-title">Präsentationen</div>
</div>
</div>
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}>
Platzhalter für Standardwerte (Autoplay, Loop, Intervall) für Präsentationen.
<div className="e-card-content" style={{ fontSize: 14 }}>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Slide-Show Intervall (Sekunden)
</label>
<NumericTextBoxComponent
min={1}
max={600}
step={1}
value={presentationInterval}
format="n0"
change={(e: { value: number }) => setPresentationInterval(Number(e.value) || 10)}
placeholder="Intervall in Sekunden"
width="120px"
/>
</div>
<div style={{ marginBottom: 16 }}>
<CheckBoxComponent
label="Seitenfortschritt anzeigen (Fortschrittsbalken)"
checked={presentationPageProgress}
change={e => setPresentationPageProgress(e.checked || false)}
/>
</div>
<div style={{ marginBottom: 16 }}>
<CheckBoxComponent
label="Präsentationsfortschritt anzeigen (Fortschrittsbalken)"
checked={presentationAutoProgress}
change={e => setPresentationAutoProgress(e.checked || false)}
/>
</div>
<ButtonComponent
cssClass="e-primary"
onClick={onSavePresentationSettings}
style={{ marginTop: 8 }}
disabled={presentationBusy}
>
{presentationBusy ? 'Speichere…' : 'Einstellungen speichern'}
</ButtonComponent>
</div>
</div>