Multi-Chapter-Support: Lizenzcode, Gitea-Sync, Auto-Update
- Aktivierungsscreen mit Lizenzcode-Eingabe - Hintergrund-Lizenzprüfung bei jedem Start - Admin-Tab mit Mitglieder-Sync und Update-Check - version.json + chapters/sonne-7x4m9k/ Struktur Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
270
index.html
270
index.html
@@ -1120,8 +1120,8 @@
|
||||
<div id="view-display" class="view">
|
||||
<button id="fullscreen-btn" onclick="toggleFullscreen()" title="Vollbild">⛶</button>
|
||||
<div class="display-header">
|
||||
<img class="bni-badge" src="logo.jpeg" alt="BNI Sonne Ulm">
|
||||
<div class="chapter-name">Vorstellungsrunde</div>
|
||||
<img class="bni-badge" id="display-logo" src="logo.jpeg" alt="BNI">
|
||||
<div class="chapter-name" id="display-chapter-name">Vorstellungsrunde</div>
|
||||
<a href="#" onclick="window.location.hash=''" style="margin-left:16px;color:rgba(255,255,255,0.25);font-size:0.78rem;text-decoration:none;letter-spacing:1px;" title="Zur Steuerung">⚙ Steuerung</a>
|
||||
</div>
|
||||
|
||||
@@ -1149,7 +1149,7 @@
|
||||
|
||||
<!-- 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 src="logo.jpeg" alt="BNI Sonne Ulm" 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);">
|
||||
</div>
|
||||
|
||||
<div class="main-card" id="main-card">
|
||||
@@ -1213,8 +1213,8 @@
|
||||
|
||||
<!-- Header -->
|
||||
<div class="ctrl-header">
|
||||
<img class="bni-badge" src="logo.jpeg" alt="BNI Sonne Ulm">
|
||||
<div class="ctrl-title">Vorstellungsrunde steuern</div>
|
||||
<img class="bni-badge" id="ctrl-logo" src="logo.jpeg" alt="BNI">
|
||||
<div class="ctrl-title" id="ctrl-chapter-name">Vorstellungsrunde steuern</div>
|
||||
<div class="flex-gap-8">
|
||||
<div class="status-dot dot-idle" id="sync-dot" title="Display-Verbindung"></div>
|
||||
<button class="btn btn-secondary btn-sm" onclick="resetMeeting()">↺ Neu starten</button>
|
||||
@@ -1259,6 +1259,7 @@
|
||||
<div class="panel-right-tab active" data-panel="visitors-panel" onclick="switchRightPanel('visitors-panel')">Besucher</div>
|
||||
<div class="panel-right-tab" data-panel="members-panel" onclick="switchRightPanel('members-panel')">Mitglieder</div>
|
||||
<div class="panel-right-tab" data-panel="substitutes-panel" onclick="switchRightPanel('substitutes-panel')">Vertretungen</div>
|
||||
<div class="panel-right-tab" data-panel="admin-panel" onclick="switchRightPanel('admin-panel')">Admin</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-right-content">
|
||||
@@ -1303,6 +1304,10 @@
|
||||
</div>
|
||||
|
||||
<!-- SUBSTITUTES -->
|
||||
<div id="admin-panel" class="right-panel-section">
|
||||
<!-- rendered by renderAdminPanel() -->
|
||||
</div>
|
||||
|
||||
<div id="substitutes-panel" class="right-panel-section">
|
||||
<div style="padding: 14px 16px 8px;">
|
||||
<div style="font-size:0.85rem; color:var(--muted); margin-bottom:12px;">Mitglied als vertreten markieren:</div>
|
||||
@@ -1610,7 +1615,7 @@
|
||||
<div style="font-weight:700;font-size:0.9rem;margin-bottom:6px;">Schritt 2 – Daten exportieren</div>
|
||||
<ol style="font-size:0.85rem;color:var(--muted);padding-left:18px;line-height:1.8;">
|
||||
<li>BNI-Mitgliederliste öffnen:
|
||||
<a href="https://bni-ulm.com/sonne/de/memberlist?chapterName=6035®ionIds=43671&isChapterwebsite=true" target="_blank" style="color:var(--red);font-weight:600;">bni-ulm.com → Chapter Sonne</a>
|
||||
<a id="bni-sync-link" href="https://bni-ulm.com/sonne/de/memberlist?chapterName=6035®ionIds=43671&isChapterwebsite=true" target="_blank" style="color:var(--red);font-weight:600;">bni-ulm.com → Chapter Sonne</a>
|
||||
</li>
|
||||
<li>Warten bis alle Mitglieder geladen sind</li>
|
||||
<li>Bookmarklet <strong>"BNI Mitglieder exportieren"</strong> klicken</li>
|
||||
@@ -1638,10 +1643,32 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ============================================================
|
||||
// CONSTANTS & CHAPTER CONFIG
|
||||
// ============================================================
|
||||
const APP_VERSION = '1.1.0';
|
||||
const GITEA_RAW = 'http://72.60.134.73:3000/cheubaum/BNI-Sonne/raw/branch/main';
|
||||
const CHAPTER_CONFIG_KEY = 'bni-active-chapter';
|
||||
|
||||
function loadChapterConfig() {
|
||||
try { const s = localStorage.getItem(CHAPTER_CONFIG_KEY); return s ? JSON.parse(s) : null; } catch(e) { return null; }
|
||||
}
|
||||
function saveChapterConfig(cfg) {
|
||||
localStorage.setItem(CHAPTER_CONFIG_KEY, JSON.stringify(cfg));
|
||||
}
|
||||
function clearChapterData() {
|
||||
if (activeChapter) localStorage.removeItem(`bni-chapter-${activeChapter.code}-v2`);
|
||||
if (activeChapter) localStorage.removeItem(`bni-extras-${activeChapter.code}`);
|
||||
localStorage.removeItem(CHAPTER_CONFIG_KEY);
|
||||
activeChapter = null;
|
||||
}
|
||||
|
||||
let activeChapter = loadChapterConfig();
|
||||
let STORAGE_KEY = activeChapter ? `bni-chapter-${activeChapter.code}-v2` : 'bni-chapter-default-v2';
|
||||
|
||||
// ============================================================
|
||||
// STATE
|
||||
// ============================================================
|
||||
const STORAGE_KEY = 'bni-sonne-v2';
|
||||
|
||||
let state = loadState();
|
||||
let sortableInstance = null;
|
||||
@@ -1685,7 +1712,7 @@ function saveState(broadcast = true) {
|
||||
// ============================================================
|
||||
// PROFILE EXTRAS (Kurzlinks + Offline-Fotos, persistent)
|
||||
// ============================================================
|
||||
const EXTRAS_KEY = 'bni-profile-extras';
|
||||
let EXTRAS_KEY = activeChapter ? `bni-extras-${activeChapter.code}` : 'bni-profile-extras';
|
||||
|
||||
function loadExtras() {
|
||||
try {
|
||||
@@ -1845,7 +1872,7 @@ let syncDot;
|
||||
|
||||
function initChannel() {
|
||||
try {
|
||||
channel = new BroadcastChannel('bni-sonne-channel');
|
||||
channel = new BroadcastChannel('bni-channel-' + (activeChapter ? activeChapter.code : 'default'));
|
||||
channel.onmessage = (ev) => {
|
||||
if (ev.data.type === 'STATE_UPDATE') {
|
||||
state = ev.data.state;
|
||||
@@ -1889,9 +1916,12 @@ function getView() {
|
||||
}
|
||||
|
||||
function renderCurrentView() {
|
||||
const hasChapter = !!activeChapter;
|
||||
const view = getView();
|
||||
document.getElementById('view-display').classList.toggle('active', view === 'display');
|
||||
document.getElementById('view-control').classList.toggle('active', view === 'control');
|
||||
document.getElementById('view-activation').classList.toggle('active', !hasChapter);
|
||||
document.getElementById('view-display').classList.toggle('active', hasChapter && view === 'display');
|
||||
document.getElementById('view-control').classList.toggle('active', hasChapter && view === 'control');
|
||||
if (!hasChapter) return;
|
||||
|
||||
if (view === 'display') {
|
||||
renderDisplay();
|
||||
@@ -2365,6 +2395,7 @@ function renderControl() {
|
||||
renderSubstitutesList();
|
||||
renderSubstituteMemberSelect();
|
||||
renderInvitedBySelect();
|
||||
renderAdminPanel();
|
||||
updateNextButton();
|
||||
syncDot = document.getElementById('sync-dot');
|
||||
}
|
||||
@@ -3965,16 +3996,233 @@ function applyQueueRatio(ratio) {
|
||||
});
|
||||
})();
|
||||
|
||||
// ============================================================
|
||||
// CHAPTER ACTIVATION & LICENSE
|
||||
// ============================================================
|
||||
async function activateChapter() {
|
||||
const input = document.getElementById('activation-code-input');
|
||||
const code = (input ? input.value : '').trim().toLowerCase();
|
||||
if (!code) return;
|
||||
const btn = document.getElementById('activation-btn');
|
||||
const status = document.getElementById('activation-status');
|
||||
btn.disabled = true;
|
||||
status.textContent = 'Überprüfe Lizenzcode…';
|
||||
status.style.color = 'rgba(255,255,255,0.6)';
|
||||
try {
|
||||
const res = await fetch(`${GITEA_RAW}/chapters/${code}/config.json`, { cache: 'no-store' });
|
||||
if (!res.ok) {
|
||||
status.textContent = 'Ungültiger oder abgelaufener Lizenzcode.';
|
||||
status.style.color = '#f87171';
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
const config = await res.json();
|
||||
activeChapter = { code, config, activatedAt: new Date().toISOString(), lastChecked: new Date().toISOString() };
|
||||
saveChapterConfig(activeChapter);
|
||||
STORAGE_KEY = `bni-chapter-${code}-v2`;
|
||||
EXTRAS_KEY = `bni-extras-${code}`;
|
||||
state = loadState();
|
||||
if (config.defaultTimer && !state.meetingOrder.length) {
|
||||
state.defaultTimer = config.defaultTimer;
|
||||
saveState(false);
|
||||
}
|
||||
applyChapterUI(config);
|
||||
status.textContent = `✓ Chapter "${config.chapterName}" aktiviert!`;
|
||||
status.style.color = '#4ade80';
|
||||
setTimeout(() => {
|
||||
renderCurrentView();
|
||||
fetchChapterMembersFromGitea();
|
||||
}, 800);
|
||||
} catch(e) {
|
||||
status.textContent = 'Verbindungsfehler. Bitte Internetverbindung prüfen.';
|
||||
status.style.color = '#f87171';
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyChapterUI(config) {
|
||||
if (!config) return;
|
||||
const name = config.chapterName || 'BNI Chapter';
|
||||
document.title = `${name} – Vorstellungsrunde`;
|
||||
const el = document.getElementById('display-chapter-name');
|
||||
if (el) el.textContent = name;
|
||||
const ctrlName = document.getElementById('ctrl-chapter-name');
|
||||
if (ctrlName) ctrlName.textContent = `${name} – Steuerung`;
|
||||
if (config.logoUrl && activeChapter) {
|
||||
const src = config.logoUrl.startsWith('http') ? config.logoUrl
|
||||
: `${GITEA_RAW}/chapters/${activeChapter.code}/${config.logoUrl}`;
|
||||
['display-logo','display-logo-empty','ctrl-logo'].forEach(id => {
|
||||
const img = document.getElementById(id);
|
||||
if (img) img.src = src;
|
||||
});
|
||||
}
|
||||
const syncLink = document.getElementById('bni-sync-link');
|
||||
if (syncLink && config.syncUrl) {
|
||||
syncLink.href = config.syncUrl;
|
||||
syncLink.textContent = name + ' – Mitgliederliste';
|
||||
}
|
||||
}
|
||||
|
||||
async function checkLicenseAndUpdates() {
|
||||
if (!activeChapter) return;
|
||||
try {
|
||||
const res = await fetch(`${GITEA_RAW}/chapters/${activeChapter.code}/config.json`, { cache: 'no-store' });
|
||||
if (!res.ok) {
|
||||
clearChapterData();
|
||||
state = defaultState();
|
||||
alert('Deine Lizenz wurde widerrufen. Bitte gib einen neuen Code ein.');
|
||||
renderCurrentView();
|
||||
return;
|
||||
}
|
||||
const config = await res.json();
|
||||
activeChapter.config = config;
|
||||
activeChapter.lastChecked = new Date().toISOString();
|
||||
saveChapterConfig(activeChapter);
|
||||
applyChapterUI(config);
|
||||
await checkForUpdates();
|
||||
} catch(e) {
|
||||
// Offline → continue with cached data
|
||||
const dot = document.getElementById('sync-dot');
|
||||
if (dot) dot.title = 'Offline – letzte Prüfung: ' + (activeChapter.lastChecked ? new Date(activeChapter.lastChecked).toLocaleDateString('de') : 'unbekannt');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdates() {
|
||||
try {
|
||||
const res = await fetch(`${GITEA_RAW}/version.json`, { cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (data.version && data.version > APP_VERSION) showUpdateBanner(data.version, data.notes);
|
||||
} catch(e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function showUpdateBanner(version, notes) {
|
||||
let banner = document.getElementById('update-banner');
|
||||
if (!banner) {
|
||||
banner = document.createElement('div');
|
||||
banner.id = 'update-banner';
|
||||
banner.style.cssText = 'background:#1a2744;color:white;padding:8px 16px;display:flex;align-items:center;gap:12px;font-size:0.85rem;flex-shrink:0;z-index:100;';
|
||||
const ctrlView = document.getElementById('view-control');
|
||||
ctrlView.insertBefore(banner, ctrlView.firstChild);
|
||||
}
|
||||
banner.innerHTML = `🆕 Update ${version} verfügbar${notes ? ' – ' + notes : ''} <button onclick="downloadUpdate('${version}')" style="background:#cc0000;color:white;border:none;border-radius:6px;padding:4px 12px;cursor:pointer;font-size:0.82rem;font-weight:600;">Jetzt laden</button> <button onclick="this.parentElement.style.display='none'" style="background:transparent;border:none;color:rgba(255,255,255,0.5);cursor:pointer;margin-left:auto;font-size:1rem;">✕</button>`;
|
||||
}
|
||||
|
||||
async function downloadUpdate(version) {
|
||||
try {
|
||||
const licRes = await fetch(`${GITEA_RAW}/chapters/${activeChapter.code}/config.json`, { cache: 'no-store' });
|
||||
if (!licRes.ok) {
|
||||
clearChapterData();
|
||||
state = defaultState();
|
||||
alert('Lizenz nicht mehr gültig. Bitte neuen Code eingeben.');
|
||||
renderCurrentView();
|
||||
return;
|
||||
}
|
||||
} catch(e) { alert('Keine Serververbindung möglich.'); return; }
|
||||
try {
|
||||
const res = await fetch(`${GITEA_RAW}/index.html`, { cache: 'no-store' });
|
||||
const html = await res.text();
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'index.html';
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
} catch(e) { alert('Download fehlgeschlagen: ' + e.message); }
|
||||
}
|
||||
|
||||
async function fetchChapterMembersFromGitea() {
|
||||
if (!activeChapter) return;
|
||||
try {
|
||||
const res = await fetch(`${GITEA_RAW}/chapters/${activeChapter.code}/members.json`, { cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (!data.members || !data.members.length) return;
|
||||
const mergedMap = new Map(state.members.map(m => [m.id, m]));
|
||||
data.members.forEach(m => {
|
||||
const ex = mergedMap.get(m.id);
|
||||
mergedMap.set(m.id, ex ? Object.assign({}, ex, m) : m);
|
||||
});
|
||||
state.members = Array.from(mergedMap.values());
|
||||
const knownIds = new Set(state.meetingOrder);
|
||||
state.members.forEach(m => { if (!knownIds.has(m.id)) state.meetingOrder.push(m.id); });
|
||||
activeChapter.lastMemberSync = new Date().toISOString();
|
||||
saveChapterConfig(activeChapter);
|
||||
saveState(false);
|
||||
renderCurrentView();
|
||||
renderAdminPanel();
|
||||
} catch(e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function renderAdminPanel() {
|
||||
const panel = document.getElementById('admin-panel');
|
||||
if (!panel) return;
|
||||
const cfg = activeChapter && activeChapter.config;
|
||||
const code = activeChapter && activeChapter.code;
|
||||
const lastChecked = activeChapter && activeChapter.lastChecked ? new Date(activeChapter.lastChecked).toLocaleString('de') : '—';
|
||||
const lastSync = activeChapter && activeChapter.lastMemberSync ? new Date(activeChapter.lastMemberSync).toLocaleString('de') : '—';
|
||||
const maskedCode = code ? code.slice(0, -3).replace(/./g, '·') + code.slice(-3) : '—';
|
||||
|
||||
panel.innerHTML = `<div style="padding:16px;overflow-y:auto;height:100%;">
|
||||
<div style="font-weight:700;font-size:0.9rem;margin-bottom:14px;">Chapter-Administration</div>
|
||||
${cfg ? `
|
||||
<div style="background:#f8f9fa;border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:14px;">
|
||||
<div style="font-size:0.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Aktives Chapter</div>
|
||||
<div style="font-weight:600;margin-bottom:2px;">${cfg.chapterName || '—'}</div>
|
||||
<div style="font-size:0.82rem;color:var(--muted);">Version: <strong>${APP_VERSION}</strong></div>
|
||||
<div style="font-size:0.82rem;color:var(--muted);">Letzte Lizenzprüfung: ${lastChecked}</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:6px;" onclick="fetchChapterMembersFromGitea()">🔄 Mitglieder vom Server laden</button>
|
||||
<div style="font-size:0.78rem;color:var(--muted);margin-bottom:14px;">Letzte Synchronisation: ${lastSync}</div>
|
||||
<button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:6px;" onclick="checkForUpdates()">🔍 Auf Updates prüfen</button>
|
||||
<div style="font-size:0.78rem;color:var(--muted);margin-bottom:20px;"></div>
|
||||
` : `<div style="color:var(--muted);font-size:0.88rem;margin-bottom:20px;">Kein Chapter aktiviert.</div>`}
|
||||
<div style="border-top:1px solid var(--border);padding-top:14px;">
|
||||
<button class="btn btn-ghost btn-sm" style="width:100%;" onclick="confirmSwitchChapter()">↔ Chapter wechseln</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function confirmSwitchChapter() {
|
||||
if (!confirm('Chapter wechseln?\n\nAlle lokalen Daten dieses Chapters werden gelöscht. Fortfahren?')) return;
|
||||
clearChapterData();
|
||||
state = defaultState();
|
||||
renderCurrentView();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INIT
|
||||
// ============================================================
|
||||
initChannel();
|
||||
if (activeChapter) applyChapterUI(activeChapter.config);
|
||||
renderCurrentView();
|
||||
checkLicenseAndUpdates();
|
||||
|
||||
// Keyboard shortcut hint
|
||||
if (getView() === 'control') {
|
||||
console.log('Tastaturkürzel: Leertaste oder → = Weiter');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ============================================================
|
||||
ACTIVATION VIEW
|
||||
============================================================ -->
|
||||
<div id="view-activation" class="view" style="background:linear-gradient(160deg,#0f1f45 0%,#1a2744 40%,#0d1b35 100%);align-items:center;justify-content:center;flex-direction:column;">
|
||||
<img id="activation-logo" src="logo.jpeg" alt="BNI" style="height:80px;margin-bottom:32px;border-radius:8px;object-fit:contain;">
|
||||
<div style="background:rgba(255,255,255,0.08);backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.15);border-radius:20px;padding:40px;width:min(420px,90vw);color:white;">
|
||||
<div style="font-size:1.4rem;font-weight:700;margin-bottom:6px;">BNI Vorstellungsrunde</div>
|
||||
<div style="font-size:0.88rem;color:rgba(255,255,255,0.6);margin-bottom:24px;">Bitte gib deinen Lizenzcode ein, um dein Chapter freizuschalten.</div>
|
||||
<input id="activation-code-input" type="text" placeholder="chapter-lizenzcode"
|
||||
style="width:100%;padding:12px 14px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.25);border-radius:10px;color:white;font-size:1rem;outline:none;margin-bottom:12px;letter-spacing:1px;"
|
||||
onkeydown="if(event.key==='Enter')activateChapter()"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
<div id="activation-status" style="font-size:0.85rem;min-height:20px;margin-bottom:12px;"></div>
|
||||
<button id="activation-btn" onclick="activateChapter()"
|
||||
style="width:100%;padding:12px;background:#cc0000;color:white;border:none;border-radius:10px;font-size:1rem;font-weight:700;cursor:pointer;">
|
||||
Aktivieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user