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

@@ -341,6 +341,8 @@
<input type="text" id="ai-embed-url" placeholder="https://show.zoho.eu/publish/..."> <input type="text" id="ai-embed-url" placeholder="https://show.zoho.eu/publish/...">
<p style="font-size:0.78rem;color:var(--muted);margin-top:4px;">In Zoho Show: Datei → Freigeben/Veröffentlichen → Einbetten, Link hier einfügen.</p> <p style="font-size:0.78rem;color:var(--muted);margin-top:4px;">In Zoho Show: Datei → Freigeben/Veröffentlichen → Einbetten, Link hier einfügen.</p>
</div> </div>
<label>Notizen — nur im Steuerpult sichtbar, nie auf dem Display</label>
<textarea id="ai-notes" rows="4" placeholder="z.B. Stichpunkte, was hier gesagt werden soll"></textarea>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-danger btn-sm" id="ai-delete-btn" onclick="deleteAgendaItem()" style="display:none;margin-right:auto;">Löschen</button> <button class="btn btn-danger btn-sm" id="ai-delete-btn" onclick="deleteAgendaItem()" style="display:none;margin-right:auto;">Löschen</button>
<button class="btn btn-ghost" onclick="closeAdminModal('modal-agenda-item')">Abbrechen</button> <button class="btn btn-ghost" onclick="closeAdminModal('modal-agenda-item')">Abbrechen</button>
@@ -887,6 +889,7 @@ function openAddAgendaItem() {
document.getElementById('ai-label').value = ''; document.getElementById('ai-label').value = '';
document.getElementById('ai-type').value = 'image'; document.getElementById('ai-type').value = 'image';
document.getElementById('ai-embed-url').value = ''; document.getElementById('ai-embed-url').value = '';
document.getElementById('ai-notes').value = '';
document.getElementById('ai-delete-btn').style.display = 'none'; document.getElementById('ai-delete-btn').style.display = 'none';
_agendaImageDataUrl = null; _agendaImageChanged = false; _agendaImageDataUrl = null; _agendaImageChanged = false;
document.getElementById('ai-image-thumb').style.display = 'none'; document.getElementById('ai-image-thumb').style.display = 'none';
@@ -904,6 +907,7 @@ function editAgendaItem(id) {
document.getElementById('ai-label').value = it.label || ''; document.getElementById('ai-label').value = it.label || '';
document.getElementById('ai-type').value = it.type || 'image'; document.getElementById('ai-type').value = it.type || 'image';
document.getElementById('ai-embed-url').value = it.embedUrl || ''; document.getElementById('ai-embed-url').value = it.embedUrl || '';
document.getElementById('ai-notes').value = it.notes || '';
document.getElementById('ai-delete-btn').style.display = 'inline-flex'; document.getElementById('ai-delete-btn').style.display = 'inline-flex';
_agendaImageDataUrl = null; _agendaImageChanged = false; _agendaImageDataUrl = null; _agendaImageChanged = false;
const thumb = document.getElementById('ai-image-thumb'); const thumb = document.getElementById('ai-image-thumb');
@@ -992,7 +996,9 @@ async function saveAgendaItem() {
id = candidate; id = candidate;
} }
const existing = state.agendaCatalog.find(a => a.id === id) || {}; const existing = state.agendaCatalog.find(a => a.id === id) || {};
const notes = document.getElementById('ai-notes').value.trim();
const item = { id, label, type }; const item = { id, label, type };
if (notes) item.notes = notes;
if (type === 'embed') { if (type === 'embed') {
const url = document.getElementById('ai-embed-url').value.trim(); const url = document.getElementById('ai-embed-url').value.trim();

View File

@@ -832,6 +832,19 @@
<p style="font-size:0.88rem;color:var(--muted);">Der rote „Weiter"-Button oben führt automatisch zum jeweils nächsten Punkt in der festgelegten Reihenfolge — egal ob als Nächstes eine Folie, die Mitglieder-Vorstellungsrunde oder die Besucherrunde kommt. Bei einer Live-Einbettung kann „Weiter" nicht innerhalb der eingebetteten Folie selbst blättern (das bleibt Sache von Zoho Show), sondern springt direkt zum nächsten Agenda-Punkt.</p> <p style="font-size:0.88rem;color:var(--muted);">Der rote „Weiter"-Button oben führt automatisch zum jeweils nächsten Punkt in der festgelegten Reihenfolge — egal ob als Nächstes eine Folie, die Mitglieder-Vorstellungsrunde oder die Besucherrunde kommt. Bei einer Live-Einbettung kann „Weiter" nicht innerhalb der eingebetteten Folie selbst blättern (das bleibt Sache von Zoho Show), sondern springt direkt zum nächsten Agenda-Punkt.</p>
</div> </div>
<div class="info">
<strong>Mit Präsentations-Klicker durchklicken:</strong> Pfeiltaste rechts (→) oder Leertaste lösen „Weiter" aus — das funktioniert jetzt sowohl im Steuerpult- als auch im Display-Fenster, je nachdem, welches Fenster gerade den Tastatur-Fokus hat. Zusätzlich gibt es unten rechts im Display-Fenster einen unsichtbaren Klick-Button (erscheint bei Hover), falls der Klicker statt einer Taste einen Mausklick sendet.
</div>
<div class="tip">
<strong>Grenze bei mehrseitigen Einbettungen:</strong> Sobald mit dem Klicker in eine eingebettete Folie mit mehreren internen Seiten hineingeklickt wird, um dort zu blättern, übernimmt Zoho Show den Tastatur-Fokus vollständig — von dort aus kann der Klicker nicht mehr automatisch zum nächsten Agenda-Punkt springen. Am Ende einer solchen Datei einmal bewusst „Weiter" im Steuerpult klicken oder den Klick-Button im Display nutzen. Am zuverlässigsten bleibt weiterhin: ein Zoho-Show-File pro Agenda-Punkt.
</div>
<div class="card">
<p style="font-weight:700;font-size:0.9rem;margin-bottom:10px;">Sprechernotizen (nur im Steuerpult sichtbar)</p>
<p style="font-size:0.88rem;color:var(--muted);">Wie bei PowerPoint lässt sich pro Agenda-Punkt ein Notiz-Text hinterlegen (Admin-Panel → Agenda-Punkt bearbeiten → Feld „Notizen"). Ist ein Punkt mit Notizen live geschaltet, erscheint direkt unter der Kopfzeile im Steuerpult ein gelber Hinweiskasten mit dem Text — unabhängig davon, welcher Reiter (Agenda/Mitglieder/Besucher) gerade offen ist. Im Display-/Beamer-Fenster erscheinen diese Notizen zu keinem Zeitpunkt, das sieht nur die Person am Steuerpult.</p>
</div>
<div class="mockup"> <div class="mockup">
<div class="mockup-bar"> <div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div> <div class="mockup-dot" style="background:#ff5f57;"></div>

View File

@@ -730,6 +730,27 @@
} }
.absent-section-title + .absent-section-title { margin-top: 10px; } .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 overlay */
#display-absent-summary { #display-absent-summary {
position: fixed; inset: 0; z-index: 5; 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> <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 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> </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 --> <!-- 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;"> <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);"> <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>
</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 --> <!-- Body -->
<div class="ctrl-body"> <div class="ctrl-body">
@@ -1906,7 +1943,7 @@
// ============================================================ // ============================================================
// CONSTANTS & CHAPTER CONFIG // 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_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'; const GITEA_API = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Vorstellungsrunde';
function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; } function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; }
@@ -1956,7 +1993,8 @@ function defaultState() {
memberTimers: {}, // { memberId: customSeconds } memberTimers: {}, // { memberId: customSeconds }
showTimer: true, // false = timer completely hidden showTimer: true, // false = timer completely hidden
coordinatorReportData: null, 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) 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) agendaKnownIds: [], // IDs, die schon einmal automatisch in agendaOrder eingefügt wurden (verhindert, dass bewusst ausgeblendete Punkte zurückkehren)
currentAgendaItemId: null, currentAgendaItemId: null,
@@ -2945,10 +2983,32 @@ function renderControl() {
renderInvitedBySelect(); renderInvitedBySelect();
renderAdminPanel(); renderAdminPanel();
renderStammvertreterSection(); renderStammvertreterSection();
renderAgendaNotesPanel();
updateNextButton(); updateNextButton();
syncDot = document.getElementById('sync-dot'); 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() { function renderInvitedBySelect() {
const sel = document.getElementById('v-invited-by'); const sel = document.getElementById('v-invited-by');
if (!sel) return; if (!sel) return;
@@ -3666,6 +3726,25 @@ function goNext() {
renderDisplay(); 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() { function resetMeeting() {
if (!confirm('Vorstellungsrunde neu starten? Der Fortschritt wird zurückgesetzt.')) return; if (!confirm('Vorstellungsrunde neu starten? Der Fortschritt wird zurückgesetzt.')) return;
stopTimer(); stopTimer();
@@ -4082,13 +4161,20 @@ function uid() {
// KEYBOARD SHORTCUTS // KEYBOARD SHORTCUTS
// ============================================================ // ============================================================
document.addEventListener('keydown', (e) => { 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.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === 'ArrowRight' || e.key === ' ') { if (e.key === 'ArrowRight' || e.key === ' ') {
e.preventDefault(); e.preventDefault();
const btn = document.getElementById('btn-next'); // canAdvance() statt #btn-next.disabled: dessen disabled-Attribut wird nur im
if (btn && !btn.disabled) goNext(); // Steuerpult-Kontext von updateNextButton() gepflegt (siehe canAdvance()-Kommentar).
if (canAdvance()) goNext();
} }
}); });

View File

@@ -1,4 +1,4 @@
{ {
"version": "2.0", "version": "2.1",
"notes": "Agenda-Folien für Online-Meetings (Hybrid-Chapter): Bild- oder Live-Einbettungs-Folien frei in die Reihenfolge einsortieren, inkl. Mitglieder-/Besucher-Vorstellungsrunde und neuer Marketing-/Ergebnisrunde (ohne Timer). \"Weiter\" folgt jetzt der Agenda." "notes": "Agenda-Folien: Präsentations-Klicker funktioniert jetzt auch dann weiter, wenn der Fokus auf dem Display-(Beamer)-Fenster liegt statt im Steuerpult (Pfeiltaste rechts/Leertaste), zusätzlich ein dezenter Klick-Button unten rechts im Display. Außerdem: Sprechernotizen pro Agenda-Punkt, die nur im Steuerpult sichtbar sind, nie auf dem Display/Beamer."
} }