Admin: Mitglieder inkl. Fotos auf Gitea hochladen

- buildMembersJson() bettet Offline-Fotos (Base64) aus extras ein
- uploadMembersToGitea() lädt members.json via API hoch
- Admin-Panel: Token-Eingabe + Upload-Button + Status
- fetchChapterMembersFromGitea speichert photoData in extras

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Heubaum
2026-04-17 14:46:24 +02:00
parent 0d7b9b2d6a
commit e1c878e6ae

View File

@@ -1336,6 +1336,26 @@
</div>
</div>
<!-- ============================================================
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>
<!-- ============================================================
MODALS
============================================================ -->
@@ -1647,7 +1667,9 @@
// 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 GITEA_RAW = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Sonne/raw';
const GITEA_API = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Sonne';
function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; }
const CHAPTER_CONFIG_KEY = 'bni-active-chapter';
function loadChapterConfig() {
@@ -4009,7 +4031,7 @@ async function activateChapter() {
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' });
const res = await fetch(giteaUrl(`chapters/${code}/config.json`), { cache: 'no-store' });
if (!res.ok) {
status.textContent = 'Ungültiger oder abgelaufener Lizenzcode.';
status.style.color = '#f87171';
@@ -4050,7 +4072,7 @@ function applyChapterUI(config) {
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}`;
: giteaUrl(`chapters/${activeChapter.code}/${config.logoUrl}`);
['display-logo','display-logo-empty','ctrl-logo'].forEach(id => {
const img = document.getElementById(id);
if (img) img.src = src;
@@ -4066,7 +4088,7 @@ function applyChapterUI(config) {
async function checkLicenseAndUpdates() {
if (!activeChapter) return;
try {
const res = await fetch(`${GITEA_RAW}/chapters/${activeChapter.code}/config.json`, { cache: 'no-store' });
const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/config.json`), { cache: 'no-store' });
if (!res.ok) {
clearChapterData();
state = defaultState();
@@ -4089,7 +4111,7 @@ async function checkLicenseAndUpdates() {
async function checkForUpdates() {
try {
const res = await fetch(`${GITEA_RAW}/version.json`, { cache: 'no-store' });
const res = await fetch(giteaUrl('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);
@@ -4110,7 +4132,7 @@ function showUpdateBanner(version, notes) {
async function downloadUpdate(version) {
try {
const licRes = await fetch(`${GITEA_RAW}/chapters/${activeChapter.code}/config.json`, { cache: 'no-store' });
const licRes = await fetch(giteaUrl(`chapters/${activeChapter.code}/config.json`), { cache: 'no-store' });
if (!licRes.ok) {
clearChapterData();
state = defaultState();
@@ -4120,7 +4142,7 @@ async function downloadUpdate(version) {
}
} catch(e) { alert('Keine Serververbindung möglich.'); return; }
try {
const res = await fetch(`${GITEA_RAW}/index.html`, { cache: 'no-store' });
const res = await fetch(giteaUrl('index.html'), { cache: 'no-store' });
const html = await res.text();
const blob = new Blob([html], { type: 'text/html' });
const a = document.createElement('a');
@@ -4134,15 +4156,23 @@ async function downloadUpdate(version) {
async function fetchChapterMembersFromGitea() {
if (!activeChapter) return;
try {
const res = await fetch(`${GITEA_RAW}/chapters/${activeChapter.code}/members.json`, { cache: 'no-store' });
const res = await fetch(giteaUrl(`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 extras = loadExtras();
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);
// Embed photoData into extras so existing photo display picks it up
if (m.photoData) {
const key = extrasKey(m.name);
if (!extras[key]) extras[key] = {};
extras[key].offlinePhotoB64 = m.photoData;
}
});
saveExtras(extras);
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); });
@@ -4154,6 +4184,65 @@ async function fetchChapterMembersFromGitea() {
} catch(e) { /* ignore */ }
}
function buildMembersJson() {
const extras = loadExtras();
const members = state.members.map(m => {
const ex = extras[extrasKey(m.name)] || {};
const entry = { id: m.id, name: m.name, company: m.company, specialty: m.specialty, photoUrl: m.photoUrl || '', profileUrl: m.profileUrl || '' };
if (ex.offlinePhotoB64) entry.photoData = ex.offlinePhotoB64;
return entry;
});
return { lastUpdated: new Date().toISOString().slice(0, 10), members };
}
function saveAdminToken() {
const val = (document.getElementById('admin-token-input').value || '').trim();
if (val) localStorage.setItem('bni-gitea-admin-token', val);
else localStorage.removeItem('bni-gitea-admin-token');
renderAdminPanel();
}
async function uploadMembersToGitea() {
const token = localStorage.getItem('bni-gitea-admin-token');
if (!token) { alert('Bitte zuerst den Admin-Token speichern.'); return; }
const statusEl = document.getElementById('upload-status');
statusEl.textContent = 'Lädt hoch…';
statusEl.style.color = 'var(--muted)';
const data = buildMembersJson();
const jsonStr = JSON.stringify(data, null, 2);
const content = btoa(unescape(encodeURIComponent(jsonStr)));
const path = `chapters/${activeChapter.code}/members.json`;
try {
// Get current SHA
let sha = null;
const shaRes = await fetch(`${GITEA_API}/contents/${path}`, { headers: { 'Authorization': `token ${token}` } });
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: `Mitglieder aktualisiert ${new Date().toLocaleDateString('de')}`, content };
if (sha) body.sha = sha;
const res = await fetch(`${GITEA_API}/contents/${path}`, {
method: sha ? 'PUT' : 'POST',
headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
activeChapter.lastMemberUpload = new Date().toISOString();
saveChapterConfig(activeChapter);
statusEl.textContent = `✓ ${data.members.length} Mitglieder hochgeladen`;
statusEl.style.color = 'var(--success)';
} else {
statusEl.textContent = 'Fehler beim Upload. Token prüfen.';
statusEl.style.color = '#dc2626';
}
} catch(e) {
statusEl.textContent = 'Verbindungsfehler.';
statusEl.style.color = '#dc2626';
}
}
function renderAdminPanel() {
const panel = document.getElementById('admin-panel');
if (!panel) return;
@@ -4161,7 +4250,8 @@ function renderAdminPanel() {
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) : '—';
const lastUpload = activeChapter && activeChapter.lastMemberUpload ? new Date(activeChapter.lastMemberUpload).toLocaleString('de') : '—';
const hasToken = !!localStorage.getItem('bni-gitea-admin-token');
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>
@@ -4169,13 +4259,26 @@ function renderAdminPanel() {
<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 style="font-size:0.82rem;color:var(--muted);">Version: <strong>${APP_VERSION}</strong> · 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="font-size:0.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Mitglieder laden</div>
<button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:4px;" onclick="fetchChapterMembersFromGitea()">🔄 Vom Server laden</button>
<div style="font-size:0.78rem;color:var(--muted);margin-bottom:14px;">Letzter Download: ${lastSync}</div>
<div style="font-size:0.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Mitglieder hochladen (Admin)</div>
<div style="display:flex;gap:6px;margin-bottom:6px;">
<input id="admin-token-input" type="password" placeholder="Gitea API Token"
value="${hasToken ? '••••••••' : ''}"
style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:6px;font-size:0.82rem;"
onfocus="if(this.value==='••••••••')this.value=''">
<button class="btn btn-secondary btn-sm" onclick="saveAdminToken()">Speichern</button>
</div>
<button class="btn btn-primary btn-sm" style="width:100%;margin-bottom:4px;" onclick="uploadMembersToGitea()">📤 Auf Gitea hochladen (inkl. Fotos)</button>
<div id="upload-status" style="font-size:0.78rem;min-height:18px;margin-bottom:4px;"></div>
<div style="font-size:0.78rem;color:var(--muted);margin-bottom:14px;">Letzter Upload: ${lastUpload}</div>
<button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:16px;" onclick="checkForUpdates()">🔍 Auf Programm-Updates prüfen</button>
` : `<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>
@@ -4204,25 +4307,6 @@ if (getView() === 'control') {
}
</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>