Version 2.1: Klicker-Fix für Agenda + Sprechernotizen im Steuerpult

Präsentations-Klicker funktioniert jetzt durchgängig über die gesamte
Agenda-Sequenz, auch wenn der Tastatur-Fokus auf dem Display-Fenster
statt dem Steuerpult liegt (ArrowRight/Leertaste), plus ein dezenter
Klick-Button unten rechts im Display für Klicker mit Mausklick-Signal.

Zusätzlich: manuell gepflegte Sprechernotizen pro Agenda-Punkt, die
ausschließlich im Steuerpult angezeigt werden, nie im Display/Beamer.
This commit is contained in:
Christian Heubaum
2026-08-29 20:20:08 +02:00
parent 580725f2f7
commit 706a91b193
4 changed files with 112 additions and 7 deletions

View File

@@ -730,6 +730,27 @@
}
.absent-section-title + .absent-section-title { margin-top: 10px; }
/* Notizen-Leiste im Steuerpult: warmer Hinweis-Kasten für Sprecher-Notizen des aktuell
live geschalteten Agenda-Punkts. Nur im Steuerpult, nie im Display gerendert. */
.agenda-notes-panel {
padding: 10px 24px;
background: #fef3c7;
border-bottom: 1px solid #fde68a;
color: #78350f;
font-size: 0.88rem;
line-height: 1.4;
white-space: pre-wrap;
}
.agenda-notes-panel strong {
display: block;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: #92400e;
margin-bottom: 4px;
}
/* Display: Absent summary overlay */
#display-absent-summary {
position: fixed; inset: 0; z-index: 5;
@@ -1310,6 +1331,17 @@
<iframe id="agenda-item-embed" src="" title="Agenda" allow="fullscreen" style="display:none;width:100%;height:100%;border:0;"></iframe>
<div id="agenda-item-error" style="display:none;color:rgba(255,255,255,0.6);font-size:1.3rem;text-align:center;padding:40px;">⚠ Bild nicht gefunden – bitte im Admin-Panel neu hochladen.</div>
</div>
<!-- Klicker-Ersatz: unauffälliger Klick-Button zum Weiterschalten direkt vom Display aus,
für Klicker, die einen Mausklick statt eines Tastendrucks senden, oder falls der
Tastatur-Fokus in eine eingebettete Folie gerutscht ist. Bewusst nur in der Ecke,
unsichtbar bis Hover/Fokus, damit die Zuschauer-Ansicht nicht gestört wird. -->
<button id="display-advance-btn" onclick="displayAdvanceClick()" title="Weiter"
style="position:fixed;right:0;bottom:0;z-index:30;width:64px;height:64px;border:none;
background:rgba(0,0,0,0.35);color:rgba(255,255,255,0.35);font-size:1.6rem;
line-height:1;cursor:pointer;border-top-left-radius:14px;opacity:0;
transition:opacity 0.15s ease, color 0.15s ease, background 0.15s ease;"
onmouseover="this.style.opacity='1';this.style.background='rgba(0,0,0,0.55)';this.style.color='#fff';"
onmouseout="this.style.opacity='0';this.style.background='rgba(0,0,0,0.35)';this.style.color='rgba(255,255,255,0.35)';">▶</button>
<!-- Startbild: außerhalb der Karte, damit es unabhängig angezeigt werden kann -->
<div id="display-empty" style="position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:10;">
<img id="display-logo-empty" src="logo.jpeg" alt="BNI" style="width:min(520px,72vw);height:auto;border-radius:24px;box-shadow:0 8px 60px rgba(0,0,0,0.5);">
@@ -1397,6 +1429,11 @@
</div>
</div>
<!-- Notizen-Leiste: Sprecher-Notizen des aktuell live geschalteten Agenda-Punkts, unabhängig
vom gerade aktiven Reiter (Agenda/Mitglieder/Besucher) links. Wird von renderControl()
über renderAgendaNotesPanel() befüllt/versteckt — NUR im Steuerpult, nie im Display-View. -->
<div id="agenda-notes-panel" style="display:none;"></div>
<!-- Body -->
<div class="ctrl-body">
@@ -1906,7 +1943,7 @@
// ============================================================
// CONSTANTS & CHAPTER CONFIG
// ============================================================
const APP_VERSION = '2.0';
const APP_VERSION = '2.1';
const GITEA_RAW = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Vorstellungsrunde/raw';
const GITEA_API = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Vorstellungsrunde';
function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; }
@@ -1956,7 +1993,8 @@ function defaultState() {
memberTimers: {}, // { memberId: customSeconds }
showTimer: true, // false = timer completely hidden
coordinatorReportData: null,
agendaCatalog: [], // { id, label, type: 'image'|'embed', imagePath?, embedUrl? } — geladen aus chapters/<code>/agenda.json
agendaCatalog: [], // { id, label, type: 'image'|'embed', imagePath?, embedUrl?, notes? } — geladen aus chapters/<code>/agenda.json
// notes: optionaler Freitext, nur im Steuerpult sichtbar (nie im Display/Beamer-View)
agendaOrder: [], // Reihenfolge von IDs (Katalog-Punkte + eingebaute Vorstellungsrunden), aktiv für dieses Meeting (wie meetingOrder)
agendaKnownIds: [], // IDs, die schon einmal automatisch in agendaOrder eingefügt wurden (verhindert, dass bewusst ausgeblendete Punkte zurückkehren)
currentAgendaItemId: null,
@@ -2945,10 +2983,32 @@ function renderControl() {
renderInvitedBySelect();
renderAdminPanel();
renderStammvertreterSection();
renderAgendaNotesPanel();
updateNextButton();
syncDot = document.getElementById('sync-dot');
}
// Zeigt die Sprecher-Notizen des aktuell live geschalteten Agenda-Punkts im Steuerpult an —
// unabhängig vom gerade gewählten linken Reiter (Agenda/Mitglieder/Besucher), damit die
// Moderatorin die Notizen auch sieht, während sie z.B. die Mitglieder-Queue pflegt. Bewusst aus
// renderControl() heraus aufgerufen (nicht nur aus showAgendaItem/closeAgendaItem), damit der
// Zustand bei jeder State-Synchronisierung korrekt bleibt. renderDisplay() kennt dieses Element
// nicht — Notizen erscheinen dadurch garantiert nie auf dem Display/Beamer.
function renderAgendaNotesPanel() {
const panel = document.getElementById('agenda-notes-panel');
if (!panel) return;
const it = state.mode === 'agenda-item' ? getAgendaItem(state.currentAgendaItemId) : null;
const notes = it && it.notes ? it.notes.trim() : '';
if (!notes) {
panel.style.display = 'none';
panel.innerHTML = '';
return;
}
panel.className = 'agenda-notes-panel';
panel.style.display = 'block';
panel.innerHTML = `<strong>Notiz — ${escHtml(it.label || it.id)}</strong>${escHtml(notes)}`;
}
function renderInvitedBySelect() {
const sel = document.getElementById('v-invited-by');
if (!sel) return;
@@ -3666,6 +3726,25 @@ function goNext() {
renderDisplay();
}
// Klick-Ersatz für die Display-Seite (siehe #display-advance-btn): delegiert 1:1 an dieselbe
// Weiter-Logik/Disabled-Prüfung wie der "Weiter"-Button im Steuerpult, keine zweite Zustandsquelle.
// Ob "Weiter" gerade sinnvoll etwas tut. Bewusst auf `state` statt auf dem DOM-Element
// #btn-next geprüft: #btn-next existiert zwar in beiden Fenstern (Steuerpult + Display sind
// dasselbe Dokument), aber sein disabled-Attribut wird nur von updateNextButton() gepflegt,
// und das läuft laut renderCurrentView() ausschließlich im Steuerpult-Kontext — im
// Display-Fenster bliebe #btn-next.disabled sonst dauerhaft auf dem statischen HTML-Anfangswert
// stehen. Einziger echter "nichts zu tun"-Fall (siehe updateNextButton() ~Zeile 3599): Meeting
// noch nicht gestartet und keine Mitglieder in der Warteschlange.
function canAdvance() {
const hasStarted = state.currentIndex >= 0 || state.mode === 'visitors' || state.mode !== 'members';
if (hasStarted) return true;
return state.meetingOrder.length > 0;
}
function displayAdvanceClick() {
if (canAdvance()) goNext();
}
function resetMeeting() {
if (!confirm('Vorstellungsrunde neu starten? Der Fortschritt wird zurückgesetzt.')) return;
stopTimer();
@@ -4082,13 +4161,20 @@ function uid() {
// KEYBOARD SHORTCUTS
// ============================================================
document.addEventListener('keydown', (e) => {
if (getView() !== 'control') return;
// Bewusst NICHT mehr auf getView()==='control' beschränkt: ein Präsentations-Klicker kann
// ArrowRight/Space senden, während der OS-Fokus auf dem Display-(Beamer)-Fenster liegt statt
// auf dem Steuerpult. #btn-next existiert als reales DOM-Element unabhängig davon, welche
// View gerade sichtbar ist. Funktioniert weiterhin nur, solange der Fokus auf unserer eigenen
// Seite liegt — sobald in eine eingebettete Zoho-Show-Folie geklickt wurde, verschluckt deren
// eigenes (fremdes) Dokument die Tastendrücke; das lässt sich aus Sicherheitsgründen
// (Cross-Origin-iframe) von hier aus nicht abfangen.
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === 'ArrowRight' || e.key === ' ') {
e.preventDefault();
const btn = document.getElementById('btn-next');
if (btn && !btn.disabled) goNext();
// canAdvance() statt #btn-next.disabled: dessen disabled-Attribut wird nur im
// Steuerpult-Kontext von updateNextButton() gepflegt (siehe canAdvance()-Kommentar).
if (canAdvance()) goNext();
}
});