Compare commits

..

166 Commits

Author SHA1 Message Date
baf236ea40 release: Version 2.2 – Gitea über https://git.digiseed.cloud 2026-10-25 13:27:01 +01:00
e9a98e7423 fix: Gitea-API über https://git.digiseed.cloud statt IP-Adresse 2026-10-25 13:20:24 +01:00
b6fb0f519b Agenda aktualisiert 12.10.2026 2026-10-12 17:27:18 +00:00
7893110125 Agenda aktualisiert 12.10.2026 2026-10-12 16:58:37 +00:00
7b024ae919 Agenda aktualisiert 12.10.2026 2026-10-12 16:53:38 +00:00
d6e6809dd7 Agenda aktualisiert 12.10.2026 2026-10-12 16:45:56 +00:00
b0c83b7b0c Agenda aktualisiert 11.10.2026 2026-10-11 16:58:53 +00:00
Christian Heubaum
7c3523959e Favicon (Sonne) auf allen Seiten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:06:54 +02:00
Christian Heubaum
2bcf6095ab Stammvertreter-Auswahl als A–Z sortiertes Dropdown
Im Vertretungs-Dialog ersetzt ein Dropdown die Kachel-Auswahl; Sidebar- und Admin-Liste ebenfalls nach Name sortiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:01:27 +02:00
bb9186d759 Agenda aktualisiert 6.10.2026 2026-10-06 04:48:24 +00:00
29f4e91e38 Agenda aktualisiert 5.10.2026 2026-10-05 16:30:55 +00:00
ae68d34829 Agenda aktualisiert 29.9.2026 2026-09-29 04:32:53 +00:00
22561af0a4 Agenda aktualisiert 28.9.2026 2026-09-28 18:49:48 +00:00
719bb05610 Agenda aktualisiert 28.9.2026 2026-09-28 18:44:42 +00:00
3ef838a58b Agenda aktualisiert 28.9.2026 2026-09-28 18:41:53 +00:00
475aa65bc0 Agenda aktualisiert 28.9.2026 2026-09-28 18:27:13 +00:00
24eee60cf1 Agenda aktualisiert 21.9.2026 2026-09-21 18:22:02 +00:00
bd1575500e Agenda aktualisiert 15.9.2026 2026-09-15 04:31:50 +00:00
4e2dacc50f Agenda aktualisiert 14.9.2026 2026-09-14 17:39:52 +00:00
9d48b427ba Agenda aktualisiert 14.9.2026 2026-09-14 17:16:37 +00:00
294a1be11c Agenda aktualisiert 7.9.2026 2026-09-07 18:08:06 +00:00
958f8e13c7 Agenda aktualisiert 7.9.2026 2026-09-07 17:40:06 +00:00
8aee368200 Stammvertreter aktualisiert 2026-09-07 17:34:12 +00:00
8c26f26e47 Agenda aktualisiert 7.9.2026 2026-09-07 17:31:18 +00:00
b1cd3b1c5f Agenda aktualisiert 7.9.2026 2026-09-07 17:01:14 +00:00
64fb49f4bc Agenda aktualisiert 7.9.2026 2026-09-07 17:00:17 +00:00
6fa3d62d36 Agenda aktualisiert 7.9.2026 2026-09-07 16:25:59 +00:00
f993ac91b2 Agenda aktualisiert 3.9.2026 2026-09-03 16:42:00 +00:00
be377caf51 Agenda aktualisiert 3.9.2026 2026-09-03 16:40:29 +00:00
9df667e7ee Agenda aktualisiert 3.9.2026 2026-09-03 16:40:17 +00:00
b3f4f41bfa Agenda aktualisiert 31.8.2026 2026-08-31 17:25:09 +00:00
Christian Heubaum
5d9427c7a6 chapters/ vorübergehend wieder auf main (Übergangsphase)
Noch nicht aktualisierte Installationen lesen Chapter-Daten über den
alten Pfad (ref=main) und interpretieren das Fehlen als widerrufene
Lizenz — das hätte laufende Chapter sofort ausgesperrt. chapters/
bleibt deshalb vorerst auf main UND data parallel vorhanden, bis alle
Installationen auf die neue, data-Branch-basierte Version aktualisiert
sind. main wird erst danach final bereinigt.
2026-08-31 17:17:35 +02:00
Christian Heubaum
08340b5aec Chapter-Daten auf separaten data-Branch ausgelagert
chapters/ lag bisher im main-Branch, aus dem sich jede lokale
Installation bedient (z.B. per ZIP-Download) — dadurch bekam jedes
Chapter beim Einrichten die Mitgliederdaten aller anderen Chapter mit,
obwohl die App diese Daten ohnehin nie lokal liest, sondern immer live
über die Gitea-API holt.

Alle Lese-/Schreibzugriffe auf chapters/* laufen jetzt gegen den neuen
data-Branch (giteaDataUrl / giteaAuthUrl(..., 'data') inkl. branch:
'data' in den Schreib-Bodies). App-Code (index.html, admin.html,
version.json) bleibt weiterhin auf main. chapters/ ist aus main
entfernt (lokal weiterhin vorhanden, per .gitignore ausgeschlossen)
und liegt vollständig auf dem neuen data-Branch.
2026-08-31 17:16:29 +02:00
Christian Heubaum
8127f87e03 Per-Chapter Start-Dateien aus dem Repo genommen
start-<chapter>.html enthält den Aktivierungscode des jeweiligen
Chapters im Klartext. Da jede Chapter-Installation aus demselben
Repo/Download besteht, würde sonst jedes Chapter die Codes aller
anderen Chapter mitbekommen. Solche Dateien bleiben ab jetzt lokal
(per .gitignore ausgeschlossen) und werden individuell verschickt.
2026-08-31 17:00:42 +02:00
Christian Heubaum
95df518900 Auto-Aktivierung per URL-Parameter + Start-Datei pro Chapter
index.html liest jetzt ?chapter=<code> aus der URL und aktiviert das
Chapter automatisch, ohne dass der Lizenzcode manuell eingegeben
werden muss. start-helios.html ist ein Beispiel: eine kleine
Redirect-Datei, die man statt index.html verschickt/öffnet und die
direkt zum aktivierten Steuerpult des jeweiligen Chapters führt.
2026-08-31 16:57:36 +02:00
Christian Heubaum
f031e94fd8 BNI-Vorstellungsrunde-Anleitung.pdf aus dem Haupt-Repo entfernt (bleibt lokal, Doku läuft über Zoho Desk) 2026-08-31 16:51:58 +02:00
Christian Heubaum
22997c1add Merge remote-tracking branch 'origin/main' 2026-08-31 16:50:28 +02:00
Christian Heubaum
f15dba0786 chapters/.DS_Store aus dem Repo entfernt (Finder-Metadatei, bereits per .gitignore ausgeschlossen) 2026-08-31 16:50:23 +02:00
Christian Heubaum
7841c4854b Screenshots und anleitung.html aus dem Haupt-Repo entfernt
Beide werden von Chapter-Installationen nicht benötigt und bleiben
lokal erhalten. Die Anleitung wird stattdessen in Zoho Desk gepflegt.
2026-08-31 16:49:58 +02:00
3b4c16e85d chapters/.DS_Store gelöscht 2026-08-31 14:46:31 +00:00
9906eb03fe Mitglieder aktualisiert 31.8.2026 2026-08-31 14:40:42 +00:00
Christian Heubaum
760a6aadfa Neues Chapter Helios (Bad Waldsee) angelegt
Basis-Config mit Sync-URL zur BNI-Konstanz-Chapterseite; Mitglieder
werden separat ergänzt.
2026-08-31 16:02:44 +02:00
Christian Heubaum
66134424e2 Merge remote-tracking branch 'origin/main' 2026-08-29 20:24:36 +02:00
Christian Heubaum
706a91b193 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.
2026-08-29 20:20:08 +02:00
ea876fb29d Agenda aktualisiert 29.8.2026 2026-08-29 18:05:43 +00:00
370a3f6cd8 Agenda aktualisiert 29.8.2026 2026-08-29 18:04:50 +00:00
7d96a3b5ad Agenda aktualisiert 29.8.2026 2026-08-29 18:02:07 +00:00
ca1902c7bb Stammvertreter aktualisiert 2026-08-28 03:53:38 +00:00
64a2c5b279 Stammvertreter aktualisiert 2026-08-28 03:52:22 +00:00
de5f560ee7 Agenda aktualisiert 25.8.2026 2026-08-25 06:50:50 +00:00
d526b97873 Agenda-Bild aktualisiert 25.8.2026 2026-08-25 06:50:47 +00:00
9e00549391 Agenda aktualisiert 25.8.2026 2026-08-25 06:43:32 +00:00
9366fa71af Agenda aktualisiert 21.8.2026 2026-08-21 13:01:32 +00:00
45400cd71e Agenda aktualisiert 21.8.2026 2026-08-21 12:58:15 +00:00
c5e5895303 Agenda aktualisiert 21.8.2026 2026-08-21 04:12:11 +00:00
36e001ae97 Agenda-Bild aktualisiert 21.8.2026 2026-08-21 04:12:08 +00:00
c1a5eaf653 Agenda aktualisiert 21.8.2026 2026-08-21 04:08:07 +00:00
11a97b62f0 Agenda aktualisiert 21.8.2026 2026-08-21 04:05:09 +00:00
8a629399ce Agenda aktualisiert 21.8.2026 2026-08-21 04:04:18 +00:00
e2a9423a6a Agenda-Bild aktualisiert 21.8.2026 2026-08-21 04:04:16 +00:00
a309d32d52 Agenda aktualisiert 21.8.2026 2026-08-21 04:03:27 +00:00
6f1800d9a6 Agenda aktualisiert 21.8.2026 2026-08-21 03:50:06 +00:00
5229c614f4 Agenda-Bild aktualisiert 21.8.2026 2026-08-21 03:48:48 +00:00
Christian Heubaum
580725f2f7 Version 2.0: Agenda-Folien für Online-Meetings (Hybrid-Chapter)
Neue Agenda-Punkte im Steuerpult (frei per Drag & Drop sortierbar, wie
die Mitgliederliste), pro Punkt entweder als hochgeladenes Bild oder als
Live-Einbettung aus Zoho Show. Mitglieder-/Besucher-Vorstellungsrunde und
eine neue Marketing-/Ergebnisrunde (gleiche Reihenfolge, ohne Timer) sind
eingebaute Agenda-Punkte und lassen sich genauso einsortieren. Der
"Weiter"-Button folgt jetzt der kompletten Agenda-Reihenfolge statt nur
Mitglieder- und Besucherrunde.

Unterwegs behobene Bugs: unsichtbarer "Anzeigen"-Button (falsche
Button-Klasse), fehlendes chapters/<code>/-Präfix beim Bild-Upload,
Race-Condition beim Speichern eines gerade erst ausgewählten Bildes,
Agenda-Reihenfolge-Duplikate bei bestehenden Chaptern.

Anleitung um Agenda-Abschnitt ergänzt, plus Korrekturen zu Ordnername,
Aktivierungs-Kontakt, Admin-Panel-URL und Offline-Hinweis für
Live-Einbettungen. Screenshots-Ordner (inkl. bisher nie gepushter
Bestandsbilder) erstmals vollständig committet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 18:59:22 +02:00
b40f384123 Agenda aktualisiert 19.8.2026 2026-08-19 10:09:11 +00:00
2b23bf8abc Agenda aktualisiert 19.8.2026 2026-08-19 09:54:06 +00:00
8164e2c7bc Agenda aktualisiert 19.8.2026 2026-08-19 09:49:21 +00:00
443eb138ec Agenda-Bild aktualisiert 19.8.2026 2026-08-19 08:23:11 +00:00
7b4b6a3f92 chapters/lumora-567294212/stammvertreter.json aktualisiert 2026-08-19 08:22:48 +00:00
7f2ebd6bb4 Falsch abgelegtes Bild entfernen (Pfad-Bug beim Agenda-Upload) 2026-08-19 08:20:02 +00:00
e66612de93 Stammvertreter aktualisiert 2026-08-19 08:19:58 +00:00
6f70a6bf71 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 08:10:09 +00:00
1d53f7b41a Agenda aktualisiert 19.8.2026 2026-08-19 08:09:01 +00:00
2865895a67 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 08:08:56 +00:00
d05f1396a2 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:57:29 +00:00
30f0290363 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:56:02 +00:00
a03740c2b4 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:54:11 +00:00
0debe09f64 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:50:16 +00:00
1a89a62149 Agenda aktualisiert 19.8.2026 2026-08-19 07:50:08 +00:00
347b682576 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:50:05 +00:00
9a5a8d1f2b Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:48:47 +00:00
b5d62ce51e Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:48:30 +00:00
2cdb378883 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:46:26 +00:00
e719ccbee0 Agenda aktualisiert 19.8.2026 2026-08-19 07:43:25 +00:00
28b0338a15 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:43:23 +00:00
0651fb4752 Stammvertreter aktualisiert 2026-08-19 07:43:20 +00:00
b11e9afa6a Agenda aktualisiert 19.8.2026 2026-08-19 07:42:34 +00:00
7ad0ab8114 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:42:32 +00:00
c0b9a6cbf8 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:41:26 +00:00
cf94ffd320 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:38:56 +00:00
8df0d03f8f Agenda aktualisiert 19.8.2026 2026-08-19 07:34:35 +00:00
26882ae897 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:34:30 +00:00
1796145bb3 Agenda aktualisiert 19.8.2026 2026-08-19 07:34:18 +00:00
7e19ede38d Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:32:20 +00:00
f20f67ebc0 Agenda aktualisiert 19.8.2026 2026-08-19 07:29:22 +00:00
ad52836ca4 Agenda aktualisiert 19.8.2026 2026-08-19 07:20:30 +00:00
d810ca68a0 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:18:19 +00:00
d4aea00b4d Agenda aktualisiert 19.8.2026 2026-08-19 07:16:28 +00:00
677ba40104 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:16:24 +00:00
f9048c5826 Agenda aktualisiert 19.8.2026 2026-08-19 07:06:54 +00:00
e208bd2cf1 Agenda aktualisiert 19.8.2026 2026-08-19 07:05:22 +00:00
3548dea926 Agenda aktualisiert 19.8.2026 2026-08-19 06:34:20 +00:00
f4dfa39b7d Agenda aktualisiert 19.8.2026 2026-08-19 06:29:47 +00:00
d52e28c3d5 Stammvertreter aktualisiert 2026-08-18 10:27:57 +00:00
c9991a4192 Stammvertreter aktualisiert 2026-08-18 10:24:28 +00:00
88d9fac5da Mitglieder aktualisiert 18.8.2026 2026-08-18 10:23:53 +00:00
66de72edc0 Mitglieder aktualisiert 18.8.2026 2026-08-18 09:34:02 +00:00
69670dd58f Stammvertreter aktualisiert 2026-08-18 09:22:15 +00:00
0d16aecf3a Stammvertreter aktualisiert 2026-08-14 04:07:55 +00:00
2d8931d140 Koordinatoren-Bericht deaktiviert für sonne-7x4m9k 2026-08-06 13:21:39 +00:00
5a6778de40 chapters/lumora-567294212/config.json aktualisiert 2026-08-06 11:50:31 +00:00
4506dbf8f6 chapters/lumora-567294212/config.json aktualisiert 2026-08-06 11:50:16 +00:00
c21f5605a2 Dateien nach "chapters/lumora-567294212" hochladen 2026-08-06 11:49:57 +00:00
a336e55759 Stammvertreter aktualisiert 2026-08-06 10:59:29 +00:00
9ce7c00b69 Stammvertreter aktualisiert 2026-08-06 10:54:10 +00:00
462d97b6cf Mitglieder aktualisiert 6.8.2026 2026-08-06 10:48:40 +00:00
c6a682fc62 Stammvertreter aktualisiert 2026-08-06 10:48:28 +00:00
611373b8d2 Mitglieder aktualisiert 6.8.2026 2026-08-06 10:39:20 +00:00
d4303dce98 Stammvertreter aktualisiert 2026-08-06 09:39:07 +00:00
6399f714fb Stammvertreter aktualisiert 2026-08-04 04:21:04 +00:00
f802da4a46 Mitglieder aktualisiert 3.8.2026 2026-08-03 06:14:44 +00:00
2170f3b8a4 Stammvertreter aktualisiert 2026-08-03 06:14:35 +00:00
62b5fcbfd6 Chapter Epsilon anlegen 2026-08-01 17:30:11 +00:00
6429b45fff Chapter Lumora Hybrid anlegen 2026-08-01 17:30:09 +00:00
7e7e1bdff0 Mitglieder aktualisiert 16.7.2026 2026-07-16 19:52:35 +00:00
Christian Heubaum
24808e35f6 Vertretungs-Foto: extras (photoB64 + offlinePhotoB64) berücksichtigen
Foto-Auflösung für Vertretungen prüft nun auch manuelle Uploads
und synchronisierte Server-Fotos aus extras, nicht nur sub.photoUrl
und Mitglieder-Match. Ines-Benedict-Fall (Foto nur als Base64 in
extras) wird jetzt korrekt aufgelöst.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 05:42:14 +02:00
Christian Heubaum
68adca35e5 Version 1.5: Farbwarnung + Glockenton + große Header-Zeitanzeige
- Main-Card wird gelb ab 15s Restzeit, BNI-Rot ab 5s
- Timer-Text kontrastabhängig (schwarz auf gelb, weiß auf rot)
- Fachgebiets-Pille immer in weißer Schrift lesbar
- Glockenton bei Ablauf (bell.mp3 als data-URI in HTML eingebettet)
- Große Sekundenanzeige rechts oben im Display-Header

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 05:42:14 +02:00
491adbaef8 Version 1.5 2026-07-02 22:29:48 +00:00
7fde6df4c1 Mitglieder aktualisiert 2.7.2026 2026-07-02 21:31:09 +00:00
3396ed062e Version 1.3. 2026-05-24 11:06:48 +00:00
50c603e251 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-05-24 11:05:39 +00:00
66f45fb80d Stammvertreter aktualisiert 2026-05-24 10:59:47 +00:00
Christian Heubaum
0797cc8984 Mitgliederkoordinator-Bericht + Anzeige-Verbesserungen
Anzeige (index.html):
- Mitgliederkoordinator-Bericht-Modus: 4x2 Kacheln mit
  Jahreszielen, Sparkline-Trend und Monatswerte-Reihe pro
  Kennzahl; jüngste Jahres-JSON-Datei wird automatisch gewählt
- QR-Code wird auf Abwesenheits- und Bericht-Anzeige
  ausgeblendet (war zuvor vom letzten Mitglied übrig)
- Abwesenheits-Namen skalieren dynamisch (2.2rem bei wenigen
  Einträgen bis 0.75rem bei vollen Listen)
- Fotos für Besucher und Vertreter in Runde 2: getDisplayData
  fällt für Besucher zusätzlich auf loadExtras() zurück, damit
  Stammvertreter-Fotos (photoB64) erscheinen; Besucher-Queue
  nutzt dieselbe Fallback-Kette

Admin (admin.html):
- Karte "Mitgliederkoordinator-Bericht": Jahresziele und
  Monatswerte je Chapter und BNI-Jahr erfassen
- _coordStatus()-Helfer für einheitliches Speicher-Feedback;
  explizite Fehlermeldungen bei fehlendem Chapter/Monat,
  HTTP-Detail bei Server-Fehlern, fehlendes Admin-Token auch
  im Statusfeld
- Sort-Crash bei alten entries-Datensätzen ohne month-Feld
  behoben (null-sicher) + Bereinigung beim Laden

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 18:07:38 +02:00
8aed9c646c Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-05-21 21:46:55 +00:00
bd3182aae5 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-05-21 21:38:27 +00:00
dba4ba35ea Update Koordinatoren-Bericht 2025-2026 für sonne-7x4m9k 2026-05-21 20:33:12 +00:00
6717eea965 Update Koordinatoren-Bericht 2025-2026 für sonne-7x4m9k 2026-05-21 20:31:51 +00:00
e7e68417d4 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 13:29:23 +00:00
a01f0ef1ad Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 13:27:04 +00:00
859d7d94fa Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 13:25:57 +00:00
ffa951ea53 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 11:56:07 +00:00
2fe2373665 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 11:55:17 +00:00
3fd457d413 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 11:54:34 +00:00
3c5959bdf7 Update Koordinatoren-Bericht für sonne-7x4m9k 2026-04-27 11:34:45 +00:00
cd51dfa9fd Koordinatoren-Bericht aktiviert für sonne-7x4m9k 2026-04-27 11:32:03 +00:00
2f8caf80bc Stammvertreter aktualisiert 2026-04-27 10:24:07 +00:00
9a97b6d7a4 Mitglieder aktualisiert 27.4.2026 2026-04-27 10:22:21 +00:00
a632e9b057 Mitglieder aktualisiert 27.4.2026 2026-04-27 10:20:30 +00:00
Christian Heubaum
eff28fcc79 Aufräumen: .DS_Store aus Repo entfernt und ignoriert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:39:57 +02:00
Christian Heubaum
f7b819eed4 Aufräumen: Push-Skript aus Repo entfernt, .gitignore ergänzt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:39:19 +02:00
Christian Heubaum
331cb08609 Aufräumen: Letztes ungenutztes Profilbild entfernt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:38:41 +02:00
Christian Heubaum
a33a68dacb Aufräumen: Kallisto-Kopie-Ordner entfernt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:36:58 +02:00
Christian Heubaum
bf4c099b62 Aufräumen: Ungenutzte Profilbilder entfernt, Anleitung aktualisiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:36:19 +02:00
Christian Heubaum
c2946a07f2 Fix: admin.html als Auto-Download, index.html per Speichern-Dialog (v1.2.4)
Zweiter showSaveFilePicker wurde vom Browser blockiert (kein User-Gesture mehr).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:12:34 +02:00
Christian Heubaum
875ef0d42f Fix: admin.html wird beim Update automatisch mitgespeichert (v1.2.3)
Blob-URL-Ansatz rückgängig gemacht (localStorage-Problem), stattdessen
beide Dateien gemeinsam per showSaveFilePicker aktualisieren.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:10:12 +02:00
Christian Heubaum
6dcfcfa4a6 Feature: Admin-Panel wird immer live vom Server geladen (v1.2.2)
Kein manuelles Update von admin.html mehr nötig – immer die aktuelle Version.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:03:51 +02:00
Christian Heubaum
f8c405f3c4 Fix: Chapter-Namen in Admin immer frisch vom Server laden
cache: no-store verhindert dass veraltete Chapter-Namen angezeigt werden.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:57:42 +02:00
Christian Heubaum
1cf92503ed Fix: Schreibfehler Callisto → Kallisto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:46:36 +02:00
Christian Heubaum
fbb2d471de Fix: Update-Dialog mit showSaveFilePicker (v1.2.1)
Direktes Speichern in den richtigen Ordner statt Download-Ordner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:42:07 +02:00
Christian Heubaum
f97ab2d69f Release v1.2.0: Timer-Schalter im Admin-Menü
Chapter können die Zeitanzeige deaktivieren. Update-Funktion ersetzt Datei direkt im richtigen Ordner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:39:15 +02:00
Christian Heubaum
69824b610c Feature: Timer-Schalter im Admin-Menü (v1.1.2)
Timer kann chapter-weit deaktiviert werden – Display-Ring, Countdown und Zeitbadges werden ausgeblendet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:35:58 +02:00
Christian Heubaum
3c37c4ad61 Fix: Abstand unter Member-Card erhöht damit DigiSeed-Logo sichtbar bleibt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 19:57:59 +02:00
Christian Heubaum
ca2e970389 Fix: DigiSeed-Logo aktualisiert und Sichtbarkeit verbessert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 19:35:18 +02:00
72c7c94ce2 Mitglieder aktualisiert 20.4.2026 2026-04-20 14:42:02 +00:00
21f5e28ca3 Mitglieder aktualisiert 20.4.2026 2026-04-20 14:34:23 +00:00
2254185cfa Mitglieder aktualisiert 20.4.2026 2026-04-20 14:34:04 +00:00
48 changed files with 3582 additions and 736 deletions

BIN
.DS_Store vendored

Binary file not shown.

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
Push
.DS_Store
screenshots/
anleitung.html
BNI-Vorstellungsrunde-Anleitung.pdf
start-*.html

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

3
Push
View File

@@ -1,3 +0,0 @@
neues Push, nach Änderungen
cd "/Users/christianheubaum/Documents/Claude/BNI Sonne"
git add -u && git commit -m "Update" && git push

View File

@@ -4,6 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BNI Admin-Panel</title> <title>BNI Admin-Panel</title>
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="icon" href="favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="apple-touch-icon.png">
<style> <style>
:root { :root {
--red: #cc0000; --red-dark: #990000; --red: #cc0000; --red-dark: #990000;
@@ -82,6 +85,10 @@
.import-member-row { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--border); cursor:pointer; } .import-member-row { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--border); cursor:pointer; }
.import-member-row:hover { background:#f8f9fa; } .import-member-row:hover { background:#f8f9fa; }
.import-member-row:last-child { border-bottom:none; } .import-member-row:last-child { border-bottom:none; }
.coord-metric-row { display:grid; grid-template-columns:1fr 90px; gap:8px; align-items:center; padding:5px 0; border-bottom:1px solid var(--border); }
.coord-metric-row:last-child { border-bottom:none; }
.coord-metric-row input { padding:5px 8px; font-size:0.85rem; text-align:right; }
.coord-section-header { font-size:0.72rem; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:1px; margin:12px 0 6px; border-top:1px solid var(--border); padding-top:10px; }
</style> </style>
</head> </head>
<body> <body>
@@ -98,6 +105,7 @@
<div class="tabs"> <div class="tabs">
<div class="tab active" onclick="switchAdminTab('stammvertreter')">Stammvertreter</div> <div class="tab active" onclick="switchAdminTab('stammvertreter')">Stammvertreter</div>
<div class="tab" onclick="switchAdminTab('mitglieder')">Mitglieder</div> <div class="tab" onclick="switchAdminTab('mitglieder')">Mitglieder</div>
<div class="tab" onclick="switchAdminTab('agenda')">Agenda</div>
<div class="tab" id="superadmin-tab" onclick="switchAdminTab('superadmin')" style="display:none;">Superadmin</div> <div class="tab" id="superadmin-tab" onclick="switchAdminTab('superadmin')" style="display:none;">Superadmin</div>
<div class="tab" onclick="switchAdminTab('einstellungen')">Einstellungen</div> <div class="tab" onclick="switchAdminTab('einstellungen')">Einstellungen</div>
</div> </div>
@@ -142,12 +150,83 @@
</div> </div>
</div> </div>
<!-- ============================================================ AGENDA -->
<div id="tab-agenda" class="tab-content">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<div class="card-title" style="margin:0;">Agenda-Folien (Online-Meeting)</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-secondary btn-sm" onclick="fetchAgendaAdmin()">🔄 Vom Server laden</button>
<button class="btn btn-primary btn-sm" onclick="uploadAgendaAdmin()">📤 Auf Server hochladen</button>
</div>
</div>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">
Punkte für den Online-Meeting-Ablauf (z.B. Begrüßung, DSGVO, BNI-Zahlen, Events) – als Bild oder als Live-Einbettung
aus Zoho Show. Die Reihenfolge hier ist nur der Katalog; welche Punkte in welcher Reihenfolge live gezeigt werden,
stellt die Moderatorin frei im Steuerpult (index.html) ein.
</p>
<div class="status-msg" id="agenda-status"></div>
<div id="admin-agenda-list" style="margin-top:10px;"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div>
<button class="btn btn-secondary btn-sm w-full" style="margin-top:12px;" onclick="openAddAgendaItem()">+ Agenda-Punkt hinzufügen</button>
</div>
</div>
<!-- ============================================================ SUPERADMIN --> <!-- ============================================================ SUPERADMIN -->
<div id="tab-superadmin" class="tab-content"> <div id="tab-superadmin" class="tab-content">
<div class="card"> <div class="card">
<div class="card-title">Alle Chapter</div> <div class="card-title">Alle Chapter</div>
<div id="all-chapters-list"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div> <div id="all-chapters-list"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div>
</div> </div>
<div class="card">
<div class="card-title">Mitgliederkoordinator-Bericht</div>
<div style="display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;">
<div style="flex:1;min-width:140px;">
<label>Chapter</label>
<select id="coord-chapter-select" onchange="loadCoordinatorReportData()">
<option value="">— Chapter auswählen —</option>
</select>
</div>
<div style="width:130px;">
<label>BNI-Jahr</label>
<select id="coord-year-select" onchange="loadCoordinatorReportData()">
<option value="2024-2025">2024 / 2025</option>
<option value="2025-2026" selected>2025 / 2026</option>
<option value="2026-2027">2026 / 2027</option>
</select>
</div>
</div>
<div id="coord-metrics-form" style="display:none;">
<!-- Jahresziele -->
<div class="coord-section-header">Jahresziele</div>
<div style="display:grid;grid-template-columns:1fr 90px;gap:8px;padding-bottom:3px;border-bottom:2px solid var(--border);margin-bottom:2px;">
<div style="font-size:0.72rem;font-weight:700;color:var(--muted);text-transform:uppercase;">Kennzahl</div>
<div style="font-size:0.72rem;font-weight:700;color:var(--muted);text-transform:uppercase;text-align:right;">Jahresziel</div>
</div>
<div id="coord-targets-rows"></div>
<button class="btn btn-secondary btn-sm" onclick="saveCoordinatorTargets()" style="margin-top:8px;">Ziele speichern</button>
<!-- Monat erfassen -->
<div class="coord-section-header">Monatswerte erfassen</div>
<div style="margin-bottom:8px;">
<label>Monat</label>
<select id="coord-month-select" onchange="loadMonthlyEntryForMonth()">
<option value="">— Monat wählen —</option>
</select>
</div>
<div style="display:grid;grid-template-columns:1fr 90px;gap:8px;padding-bottom:3px;border-bottom:2px solid var(--border);margin-bottom:2px;">
<div style="font-size:0.72rem;font-weight:700;color:var(--muted);text-transform:uppercase;">Kennzahl</div>
<div style="font-size:0.72rem;font-weight:700;color:var(--muted);text-transform:uppercase;text-align:right;">Ist-Wert</div>
</div>
<div id="coord-monthly-rows"></div>
<div style="display:flex;gap:8px;margin-top:8px;">
<button class="btn btn-primary btn-sm" onclick="saveMonthlyEntry()">Monat speichern</button>
<button class="btn btn-danger btn-sm" id="coord-delete-month-btn" onclick="deleteMonthlyEntry()" style="display:none;">Löschen</button>
</div>
<div id="coord-entries-summary" style="margin-top:8px;font-size:0.78rem;color:var(--muted);"></div>
</div>
<div class="status-msg" id="coord-status"></div>
</div>
<div class="card"> <div class="card">
<div class="card-title">Update-Rollout</div> <div class="card-title">Update-Rollout</div>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">Neue Versionsnummer in version.json pushen → alle Nutzer aller Chapter sehen das Update-Banner beim nächsten Start.</p> <p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">Neue Versionsnummer in version.json pushen → alle Nutzer aller Chapter sehen das Update-Banner beim nächsten Start.</p>
@@ -237,6 +316,44 @@
</div> </div>
</div> </div>
<!-- Agenda Item Modal -->
<div class="modal-overlay" id="modal-agenda-item">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="agenda-item-modal-title">Agenda-Punkt</div>
<button class="modal-close" onclick="closeAdminModal('modal-agenda-item')">✕</button>
</div>
<input type="hidden" id="ai-id">
<label>Bezeichnung</label>
<input type="text" id="ai-label" placeholder="z.B. Begrüßung">
<label>Anzeigeart</label>
<select id="ai-type" onchange="updateAgendaItemTypeUI()">
<option value="image">Bild (Export, wird hochgeladen)</option>
<option value="embed">Live-Einbettung (Zoho Show o.ä.)</option>
</select>
<div id="ai-image-row">
<label>Bild</label>
<div class="photo-actions">
<img id="ai-image-thumb" class="photo-thumb" style="width:80px;height:80px;border-radius:8px;display:none;" src="" alt="">
<label class="btn-file-label">📁 Bild wählen<input type="file" id="ai-image-file" accept="image/*" style="display:none" onchange="handleAgendaImageFile(this)"></label>
</div>
<div id="ai-image-thumb-hint" style="display:none;font-size:0.78rem;color:#d97706;margin-top:6px;"></div>
</div>
<div id="ai-embed-row" style="display:none;">
<label>Einbettungs-URL</label>
<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>
</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">
<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-primary" onclick="saveAgendaItem()">Speichern</button>
</div>
</div>
</div>
<!-- Import from Chapter Modal --> <!-- Import from Chapter Modal -->
<div class="modal-overlay" id="modal-import-chapter"> <div class="modal-overlay" id="modal-import-chapter">
<div class="modal"> <div class="modal">
@@ -262,11 +379,12 @@
// SHARED CONSTANTS (copy from index.html) // SHARED CONSTANTS (copy from index.html)
// ============================================================ // ============================================================
const APP_VERSION = '1.1.0'; const APP_VERSION = '1.1.0';
const GITEA_RAW = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Vorstellungsrunde/raw'; const GITEA_RAW = 'https://git.digiseed.cloud/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 = 'https://git.digiseed.cloud/api/v1/repos/cheubaum/BNI-Vorstellungsrunde';
const CHAPTER_CONFIG_KEY = 'bni-active-chapter'; const CHAPTER_CONFIG_KEY = 'bni-active-chapter';
function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; } function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; }
function giteaAuthUrl(path, token) { return `${GITEA_API}/contents/${path}?token=${encodeURIComponent(token)}`; } function giteaDataUrl(path) { return `${GITEA_RAW}/${path}?ref=data`; }
function giteaAuthUrl(path, token, ref) { return `${GITEA_API}/contents/${path}?token=${encodeURIComponent(token)}${ref ? '&ref=' + ref : ''}`; }
// ============================================================ // ============================================================
// CHAPTER CONFIG // CHAPTER CONFIG
@@ -345,6 +463,7 @@ function switchAdminTab(tab) {
document.getElementById(`tab-${tab}`).classList.add('active'); document.getElementById(`tab-${tab}`).classList.add('active');
if (tab === 'stammvertreter') renderSvList(); if (tab === 'stammvertreter') renderSvList();
if (tab === 'mitglieder') renderMembersList(); if (tab === 'mitglieder') renderMembersList();
if (tab === 'agenda') renderAgendaAdminList();
if (tab === 'superadmin') { loadAllChapters(); loadCurrentVersion(); } if (tab === 'superadmin') { loadAllChapters(); loadCurrentVersion(); }
if (tab === 'einstellungen') renderSettings(); if (tab === 'einstellungen') renderSettings();
} }
@@ -355,7 +474,7 @@ function switchAdminTab(tab) {
let _svAdminPhotoB64 = null; let _svAdminPhotoB64 = null;
function renderSvList() { function renderSvList() {
const list = loadStammvertreter(); const list = loadStammvertreter().sort((a, b) => (a.name || '').localeCompare(b.name || '', 'de', { sensitivity: 'base' }));
const container = document.getElementById('admin-sv-list'); const container = document.getElementById('admin-sv-list');
if (!list.length) { if (!list.length) {
container.innerHTML = `<div style="color:var(--muted);font-size:0.88rem;padding:8px 0;">Noch keine Stammvertreter gespeichert.</div>`; container.innerHTML = `<div style="color:var(--muted);font-size:0.88rem;padding:8px 0;">Noch keine Stammvertreter gespeichert.</div>`;
@@ -462,7 +581,7 @@ async function openImportFromChapter() {
const listEl = document.getElementById('import-chapter-select-list'); const listEl = document.getElementById('import-chapter-select-list');
listEl.innerHTML = '<div style="color:var(--muted);font-size:0.85rem;">Lade Chapter…</div>'; listEl.innerHTML = '<div style="color:var(--muted);font-size:0.85rem;">Lade Chapter…</div>';
try { try {
const res = await fetch(`${GITEA_API}/contents/chapters?ref=main`); const res = await fetch(`${GITEA_API}/contents/chapters?ref=data`);
const items = await res.json(); const items = await res.json();
const chapters = Array.isArray(items) ? items.filter(i => i.type === 'dir') : []; const chapters = Array.isArray(items) ? items.filter(i => i.type === 'dir') : [];
if (!chapters.length) { listEl.innerHTML = '<div style="color:var(--muted);">Keine anderen Chapter gefunden.</div>'; return; } if (!chapters.length) { listEl.innerHTML = '<div style="color:var(--muted);">Keine anderen Chapter gefunden.</div>'; return; }
@@ -470,7 +589,7 @@ async function openImportFromChapter() {
for (const ch of chapters) { for (const ch of chapters) {
if (activeChapter && ch.name === activeChapter.code) continue; if (activeChapter && ch.name === activeChapter.code) continue;
try { try {
const cfgRes = await fetch(giteaUrl(`chapters/${ch.name}/config.json`)); const cfgRes = await fetch(giteaDataUrl(`chapters/${ch.name}/config.json`), { cache: 'no-store' });
const cfg = cfgRes.ok ? await cfgRes.json() : {}; const cfg = cfgRes.ok ? await cfgRes.json() : {};
const btn = document.createElement('div'); const btn = document.createElement('div');
btn.className = 'chapter-item'; btn.className = 'chapter-item';
@@ -496,7 +615,7 @@ async function loadChapterMembersForImport(code, chapterName) {
const listEl = document.getElementById('import-member-list'); const listEl = document.getElementById('import-member-list');
listEl.innerHTML = '<div style="padding:12px;color:var(--muted);">Lade Mitglieder…</div>'; listEl.innerHTML = '<div style="padding:12px;color:var(--muted);">Lade Mitglieder…</div>';
try { try {
const res = await fetch(giteaUrl(`chapters/${code}/members.json`)); const res = await fetch(giteaDataUrl(`chapters/${code}/members.json`));
if (!res.ok) { listEl.innerHTML = '<div style="padding:12px;color:#dc2626;">Keine Mitglieder gefunden.</div>'; return; } if (!res.ok) { listEl.innerHTML = '<div style="padding:12px;color:#dc2626;">Keine Mitglieder gefunden.</div>'; return; }
const data = await res.json(); const data = await res.json();
const members = data.members || []; const members = data.members || [];
@@ -554,11 +673,11 @@ async function uploadSvToGitea() {
const path = `chapters/${activeChapter.code}/stammvertreter.json`; const path = `chapters/${activeChapter.code}/stammvertreter.json`;
try { try {
let sha = null; let sha = null;
const shaRes = await fetch(giteaAuthUrl(path, token)); const shaRes = await fetch(giteaAuthUrl(path, token, 'data'));
if (shaRes.ok) sha = (await shaRes.json()).sha; if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: 'Stammvertreter aktualisiert', content }; const body = { message: 'Stammvertreter aktualisiert', content, branch: 'data' };
if (sha) body.sha = sha; if (sha) body.sha = sha;
const res = await fetch(giteaAuthUrl(path, token), { method: sha?'PUT':'POST', headers: { 'Content-Type':'application/json' }, body: JSON.stringify(body) }); const res = await fetch(giteaAuthUrl(path, token, 'data'), { method: sha?'PUT':'POST', headers: { 'Content-Type':'application/json' }, body: JSON.stringify(body) });
if (res.ok) { statusEl.textContent = `✓ ${list.length} Stammvertreter gesichert`; statusEl.style.color = 'var(--success)'; } if (res.ok) { statusEl.textContent = `✓ ${list.length} Stammvertreter gesichert`; statusEl.style.color = 'var(--success)'; }
else { statusEl.textContent = 'Fehler beim Upload.'; statusEl.style.color = '#dc2626'; } else { statusEl.textContent = 'Fehler beim Upload.'; statusEl.style.color = '#dc2626'; }
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; } } catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
@@ -568,7 +687,7 @@ async function downloadSvFromGitea() {
const statusEl = document.getElementById('sv-sync-status'); const statusEl = document.getElementById('sv-sync-status');
statusEl.textContent = 'Lade…'; statusEl.style.color = 'var(--muted)'; statusEl.textContent = 'Lade…'; statusEl.style.color = 'var(--muted)';
try { try {
const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/stammvertreter.json`), { cache: 'no-store' }); const res = await fetch(giteaDataUrl(`chapters/${activeChapter.code}/stammvertreter.json`), { cache: 'no-store' });
if (!res.ok) { statusEl.textContent = 'Keine Daten auf Server.'; statusEl.style.color = 'var(--muted)'; return; } if (!res.ok) { statusEl.textContent = 'Keine Daten auf Server.'; statusEl.style.color = 'var(--muted)'; return; }
const data = await res.json(); const data = await res.json();
if (data.substitutes) { saveStammvertreter(data.substitutes); renderSvList(); statusEl.textContent = `✓ ${data.substitutes.length} Stammvertreter geladen`; statusEl.style.color = 'var(--success)'; } if (data.substitutes) { saveStammvertreter(data.substitutes); renderSvList(); statusEl.textContent = `✓ ${data.substitutes.length} Stammvertreter geladen`; statusEl.style.color = 'var(--success)'; }
@@ -665,7 +784,7 @@ async function fetchMembersAdmin() {
const statusEl = document.getElementById('members-status'); const statusEl = document.getElementById('members-status');
statusEl.textContent = 'Lade vom Server…'; statusEl.style.color = 'var(--muted)'; statusEl.textContent = 'Lade vom Server…'; statusEl.style.color = 'var(--muted)';
try { try {
const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/members.json`), { cache: 'no-store' }); const res = await fetch(giteaDataUrl(`chapters/${activeChapter.code}/members.json`), { cache: 'no-store' });
if (!res.ok) { statusEl.textContent = 'Keine Mitglieder auf Server.'; return; } if (!res.ok) { statusEl.textContent = 'Keine Mitglieder auf Server.'; return; }
const data = await res.json(); const data = await res.json();
if (!data.members || !data.members.length) { statusEl.textContent = 'Liste ist leer.'; return; } if (!data.members || !data.members.length) { statusEl.textContent = 'Liste ist leer.'; return; }
@@ -702,14 +821,14 @@ async function uploadMembersAdmin() {
const data = { lastUpdated: new Date().toISOString().slice(0,10), members }; const data = { lastUpdated: new Date().toISOString().slice(0,10), members };
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2)))); const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const path = `chapters/${activeChapter.code}/members.json`; const path = `chapters/${activeChapter.code}/members.json`;
const url = giteaAuthUrl(path, token); const url = giteaAuthUrl(path, token, 'data');
console.log('[Upload] URL:', url); console.log('[Upload] URL:', url);
statusEl.textContent = 'Prüfe SHA…'; statusEl.textContent = 'Prüfe SHA…';
let sha = null; let sha = null;
const shaRes = await fetch(url); const shaRes = await fetch(url);
console.log('[Upload] SHA-Status:', shaRes.status); console.log('[Upload] SHA-Status:', shaRes.status);
if (shaRes.ok) sha = (await shaRes.json()).sha; if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message:`Mitglieder aktualisiert ${new Date().toLocaleDateString('de')}`, content }; const body = { message:`Mitglieder aktualisiert ${new Date().toLocaleDateString('de')}`, content, branch: 'data' };
if (sha) body.sha = sha; if (sha) body.sha = sha;
statusEl.textContent = sha ? 'Aktualisiere…' : 'Erstelle…'; statusEl.textContent = sha ? 'Aktualisiere…' : 'Erstelle…';
const res = await fetch(url, { method:sha?'PUT':'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) }); const res = await fetch(url, { method:sha?'PUT':'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
@@ -726,33 +845,280 @@ async function uploadMembersAdmin() {
} }
} }
// ============================================================
// AGENDA-FOLIEN (Online-Meeting-Ablauf)
// ============================================================
let _agendaImageDataUrl = null;
let _agendaImageChanged = false;
let _agendaImageProcessing = null; // Promise, solange ein gewähltes Bild noch verkleinert wird
function slugify(s) {
const map = { ä:'ae', ö:'oe', ü:'ue', ß:'ss' };
return (s || '').toLowerCase().trim()
.replace(/[äöüß]/g, c => map[c])
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '') || 'punkt';
}
function renderAgendaAdminList() {
const state = loadState();
const container = document.getElementById('admin-agenda-list');
const items = state.agendaCatalog || [];
if (!items.length) {
container.innerHTML = `<div style="color:var(--muted);font-size:0.88rem;">Noch keine Agenda-Punkte. Mit "+ Agenda-Punkt hinzufügen" starten.</div>`;
return;
}
container.innerHTML = '';
items.forEach(it => {
const div = document.createElement('div');
div.className = 'member-item';
div.style.cursor = 'pointer';
div.title = 'Bearbeiten';
const typeLabel = it.type === 'embed' ? 'Live-Einbettung' : 'Bild';
div.innerHTML = `
<div style="flex:1;min-width:0;">
<div style="font-weight:600;font-size:0.88rem;">${escHtml(it.label || it.id)}</div>
<div style="font-size:0.75rem;color:var(--muted);">${typeLabel}</div>
</div>
<button class="btn btn-ghost btn-sm">✎</button>
`;
div.onclick = () => editAgendaItem(it.id);
container.appendChild(div);
});
}
function openAddAgendaItem() {
document.getElementById('ai-id').value = '';
document.getElementById('agenda-item-modal-title').textContent = 'Agenda-Punkt hinzufügen';
document.getElementById('ai-label').value = '';
document.getElementById('ai-type').value = 'image';
document.getElementById('ai-embed-url').value = '';
document.getElementById('ai-notes').value = '';
document.getElementById('ai-delete-btn').style.display = 'none';
_agendaImageDataUrl = null; _agendaImageChanged = false;
document.getElementById('ai-image-thumb').style.display = 'none';
document.getElementById('ai-image-thumb-hint').style.display = 'none';
updateAgendaItemTypeUI();
openAdminModal('modal-agenda-item');
}
function editAgendaItem(id) {
const state = loadState();
const it = (state.agendaCatalog || []).find(a => a.id === id);
if (!it) return;
document.getElementById('ai-id').value = it.id;
document.getElementById('agenda-item-modal-title').textContent = 'Agenda-Punkt bearbeiten';
document.getElementById('ai-label').value = it.label || '';
document.getElementById('ai-type').value = it.type || 'image';
document.getElementById('ai-embed-url').value = it.embedUrl || '';
document.getElementById('ai-notes').value = it.notes || '';
document.getElementById('ai-delete-btn').style.display = 'inline-flex';
_agendaImageDataUrl = null; _agendaImageChanged = false;
const thumb = document.getElementById('ai-image-thumb');
const thumbHint = document.getElementById('ai-image-thumb-hint');
if (it.imagePath) {
thumb.onerror = () => {
thumb.style.display = 'none';
if (thumbHint) { thumbHint.textContent = '⚠ Bild nicht auf dem Server gefunden – bitte neu hochladen.'; thumbHint.style.display = 'block'; }
};
thumb.onload = () => { if (thumbHint) thumbHint.style.display = 'none'; };
if (thumbHint) thumbHint.style.display = 'none';
thumb.src = it.imagePath.startsWith('http') ? it.imagePath : giteaDataUrl(`chapters/${activeChapter.code}/${it.imagePath}`);
thumb.style.display = 'block';
} else {
thumb.style.display = 'none';
if (thumbHint) thumbHint.style.display = 'none';
}
updateAgendaItemTypeUI();
openAdminModal('modal-agenda-item');
}
function updateAgendaItemTypeUI() {
const type = document.getElementById('ai-type').value;
document.getElementById('ai-image-row').style.display = type === 'image' ? 'block' : 'none';
document.getElementById('ai-embed-row').style.display = type === 'embed' ? 'block' : 'none';
}
function handleAgendaImageFile(input) {
const file = input.files && input.files[0];
if (!file) return;
const hint = document.getElementById('ai-image-thumb-hint');
if (hint) { hint.textContent = 'Bild wird verarbeitet…'; hint.style.color = 'var(--muted)'; hint.style.display = 'block'; }
// Auf eine für Vollbild-Anzeige ausreichende Größe verkleinern/komprimieren, damit der
// Upload sicher unter dem Größenlimit der Gitea-Contents-API bleibt (typischerweise ~1 MB/Datei).
// _agendaImageProcessing merkt sich das laufende Promise, damit saveAgendaItem() nicht zu früh
// speichert, während das Bild noch verarbeitet wird (führte sonst zu einem stillen "kein Bild gewählt").
_agendaImageProcessing = resizePhotoToBase64(file, 1920).then(b64 => {
_agendaImageDataUrl = b64;
_agendaImageChanged = true;
const thumb = document.getElementById('ai-image-thumb');
thumb.src = b64;
thumb.style.display = 'block';
if (hint) {
const kb = Math.round((b64.length * 0.75) / 1024);
hint.textContent = `Für den Upload verkleinert auf max. 1920px, ~${kb} KB.`;
hint.style.color = 'var(--muted)';
hint.style.display = 'block';
}
}).catch(() => alert('Bild konnte nicht gelesen werden.'))
.finally(() => { _agendaImageProcessing = null; });
input.value = '';
}
async function uploadAgendaImage(path, dataUrl, token) {
const content = dataUrl.split(',')[1];
const url = giteaAuthUrl(path, token, 'data');
let sha = null;
const shaRes = await fetch(url);
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: `Agenda-Bild aktualisiert ${new Date().toLocaleDateString('de')}`, content, branch: 'data' };
if (sha) body.sha = sha;
const res = await fetch(url, { method: sha ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (!res.ok) { const t = await res.text(); throw new Error(`${res.status}: ${t.slice(0, 80)}`); }
}
async function saveAgendaItem() {
const label = document.getElementById('ai-label').value.trim();
if (!label) { alert('Bitte eine Bezeichnung eingeben.'); return; }
const type = document.getElementById('ai-type').value;
if (_agendaImageProcessing) {
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Bild wird noch verarbeitet…'; statusEl.style.color = 'var(--muted)';
await _agendaImageProcessing;
}
const state = loadState();
if (!state.agendaCatalog) state.agendaCatalog = [];
let id = document.getElementById('ai-id').value;
if (!id) {
const base = slugify(label);
const ids = new Set(state.agendaCatalog.map(a => a.id));
let candidate = base, n = 1;
while (ids.has(candidate)) candidate = `${base}-${++n}`;
id = candidate;
}
const existing = state.agendaCatalog.find(a => a.id === id) || {};
const notes = document.getElementById('ai-notes').value.trim();
const item = { id, label, type };
if (notes) item.notes = notes;
if (type === 'embed') {
const url = document.getElementById('ai-embed-url').value.trim();
if (!url) { alert('Bitte eine Einbettungs-URL eingeben.'); return; }
item.embedUrl = url;
} else {
item.imagePath = existing.imagePath || `agenda/${id}.jpg`;
if (!_agendaImageChanged && !existing.imagePath) { alert('Bitte ein Bild wählen.'); return; }
}
const statusEl = document.getElementById('agenda-status');
if (type === 'image' && _agendaImageChanged && _agendaImageDataUrl) {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
statusEl.textContent = 'Lade Bild hoch…'; statusEl.style.color = 'var(--muted)';
try {
await uploadAgendaImage(`chapters/${activeChapter.code}/${item.imagePath}`, _agendaImageDataUrl, token);
} catch(e) {
statusEl.textContent = `Fehler beim Bild-Upload: ${e.message}`; statusEl.style.color = '#dc2626';
alert(`Bild-Upload fehlgeschlagen: ${e.message}\n\nDer Agenda-Punkt wurde NICHT gespeichert.`);
return;
}
}
const idx = state.agendaCatalog.findIndex(a => a.id === id);
if (idx >= 0) state.agendaCatalog[idx] = item; else state.agendaCatalog.push(item);
saveState(state);
closeAdminModal('modal-agenda-item');
renderAgendaAdminList();
statusEl.textContent = '✓ Lokal gespeichert – "Auf Server hochladen" nicht vergessen, damit es im Steuerpult ankommt.';
statusEl.style.color = 'var(--success)';
}
function deleteAgendaItem() {
const id = document.getElementById('ai-id').value;
if (!id) return;
if (!confirm('Diesen Agenda-Punkt wirklich löschen?')) return;
const state = loadState();
state.agendaCatalog = (state.agendaCatalog || []).filter(a => a.id !== id);
saveState(state);
closeAdminModal('modal-agenda-item');
renderAgendaAdminList();
}
async function fetchAgendaAdmin() {
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Lade vom Server…'; statusEl.style.color = 'var(--muted)';
try {
const res = await fetch(giteaDataUrl(`chapters/${activeChapter.code}/agenda.json`), { cache: 'no-store' });
if (!res.ok) { statusEl.textContent = 'Noch keine Agenda auf dem Server.'; return; }
const data = await res.json();
const state = loadState();
state.agendaCatalog = Array.isArray(data.items) ? data.items : [];
saveState(state);
statusEl.textContent = `✓ ${state.agendaCatalog.length} Punkte geladen`; statusEl.style.color = 'var(--success)';
renderAgendaAdminList();
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
async function uploadAgendaAdmin() {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Lädt hoch…'; statusEl.style.color = 'var(--muted)';
try {
const state = loadState();
const data = { items: state.agendaCatalog || [] };
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const path = `chapters/${activeChapter.code}/agenda.json`;
const url = giteaAuthUrl(path, token, 'data');
let sha = null;
const shaRes = await fetch(url);
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: `Agenda aktualisiert ${new Date().toLocaleDateString('de')}`, content, branch: 'data' };
if (sha) body.sha = sha;
const res = await fetch(url, { method: sha ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (res.ok) { statusEl.textContent = `✓ ${data.items.length} Agenda-Punkte hochgeladen`; statusEl.style.color = 'var(--success)'; }
else { const t = await res.text(); statusEl.textContent = `Fehler ${res.status}: ${t.slice(0, 80)}`; statusEl.style.color = '#dc2626'; }
} catch(e) { statusEl.textContent = `Fehler: ${e.message}`; statusEl.style.color = '#dc2626'; }
}
// ============================================================ // ============================================================
// SUPERADMIN // SUPERADMIN
// ============================================================ // ============================================================
let _allChaptersData = [];
async function loadAllChapters() { async function loadAllChapters() {
const container = document.getElementById('all-chapters-list'); const container = document.getElementById('all-chapters-list');
container.innerHTML = '<div style="color:var(--muted);font-size:0.85rem;">Lade…</div>'; container.innerHTML = '<div style="color:var(--muted);font-size:0.85rem;">Lade…</div>';
try { try {
const res = await fetch(`${GITEA_API}/contents/chapters?ref=main`); const res = await fetch(`${GITEA_API}/contents/chapters?ref=data`);
const items = await res.json(); const items = await res.json();
const chapters = Array.isArray(items) ? items.filter(i => i.type === 'dir') : []; const chapters = Array.isArray(items) ? items.filter(i => i.type === 'dir') : [];
_allChaptersData = [];
container.innerHTML = ''; container.innerHTML = '';
for (const ch of chapters) { for (const ch of chapters) {
try { try {
const cfgRes = await fetch(giteaUrl(`chapters/${ch.name}/config.json`)); const cfgRes = await fetch(giteaDataUrl(`chapters/${ch.name}/config.json`), { cache: 'no-store' });
const cfg = cfgRes.ok ? await cfgRes.json() : {}; const cfg = cfgRes.ok ? await cfgRes.json() : {};
const membersRes = await fetch(giteaUrl(`chapters/${ch.name}/members.json`)); const membersRes = await fetch(giteaDataUrl(`chapters/${ch.name}/members.json`));
const membersData = membersRes.ok ? await membersRes.json() : {}; const membersData = membersRes.ok ? await membersRes.json() : {};
_allChaptersData.push({ code: ch.name, cfg });
const isEnabled = !!cfg.memberCoordinatorReport;
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'chapter-item'; div.className = 'chapter-item';
div.innerHTML = `<div style="flex:1;"> div.innerHTML = `<div style="flex:1;">
<div style="font-weight:600;">${escHtml(cfg.chapterName||ch.name)}</div> <div style="font-weight:600;">${escHtml(cfg.chapterName||ch.name)}</div>
<div style="font-size:0.78rem;color:var(--muted);">Code: <code>${ch.name}</code> · Mitglieder: ${(membersData.members||[]).length} · Stand: ${membersData.lastUpdated||'—'}</div> <div style="font-size:0.78rem;color:var(--muted);">Code: <code>${ch.name}</code> · Mitglieder: ${(membersData.members||[]).length} · Stand: ${membersData.lastUpdated||'—'}</div>
</div>`; </div>
<span style="font-size:0.72rem;padding:2px 8px;border-radius:20px;background:${isEnabled?'#dcfce7':'#f3f4f6'};color:${isEnabled?'#16a34a':'#6b7280'};font-weight:600;white-space:nowrap;">Bericht: ${isEnabled?'Aktiv':'Inaktiv'}</span>
<button class="btn btn-sm ${isEnabled?'btn-ghost':'btn-secondary'}" onclick="toggleCoordinatorReport('${ch.name}', ${isEnabled})" style="white-space:nowrap;font-size:0.75rem;">${isEnabled?'Deaktivieren':'Freischalten'}</button>`;
container.appendChild(div); container.appendChild(div);
} catch(e) { /* skip */ } } catch(e) { /* skip */ }
} }
if (!container.children.length) container.innerHTML = '<div style="color:var(--muted);">Keine Chapter gefunden.</div>'; if (!container.children.length) container.innerHTML = '<div style="color:var(--muted);">Keine Chapter gefunden.</div>';
_populateCoordChapterDropdown();
} catch(e) { } catch(e) {
container.innerHTML = '<div style="color:#dc2626;">Fehler beim Laden.</div>'; container.innerHTML = '<div style="color:#dc2626;">Fehler beim Laden.</div>';
} }
@@ -819,6 +1185,279 @@ function saveSettingsToken() {
setTimeout(() => { document.getElementById('token-status').textContent = ''; }, 3000); setTimeout(() => { document.getElementById('token-status').textContent = ''; }, 3000);
} }
// ============================================================
// COORDINATOR REPORT
// ============================================================
const _coordMetrics = [
{ key: 'attendance', label: 'Anwesenheit', unit: '%', cumulative: false },
{ key: 'members', label: 'Mitgliederzahl', unit: '', cumulative: false },
{ key: 'newMembers', label: 'Neue Mitglieder', unit: '', cumulative: true },
{ key: 'guests', label: 'Gäste / Besucher', unit: '', cumulative: true },
{ key: 'oneOnOne', label: '4-Augen-Gespräche', unit: '', cumulative: true },
{ key: 'cte', label: 'CTE', unit: '', cumulative: true },
{ key: 'referrals', label: 'Empfehlungen', unit: '', cumulative: true },
{ key: 'revenueThank', label: 'Umsatzdanke', unit: '', cumulative: true },
];
let _coordReportData = null;
function _getBniYearAdmin() {
const now = new Date();
const m = now.getMonth();
const y = now.getFullYear();
return m >= 3 ? `${y}-${y + 1}` : `${y - 1}-${y}`;
}
function _populateCoordChapterDropdown() {
const sel = document.getElementById('coord-chapter-select');
if (!sel) return;
const prev = sel.value;
sel.innerHTML = '<option value="">— Chapter auswählen —</option>';
for (const ch of _allChaptersData) {
if (!ch.cfg.memberCoordinatorReport) continue;
const opt = document.createElement('option');
opt.value = ch.code;
opt.textContent = ch.cfg.chapterName || ch.code;
if (ch.code === prev) opt.selected = true;
sel.appendChild(opt);
}
// Pre-select current BNI year
const ySel = document.getElementById('coord-year-select');
if (ySel && !ySel.value) ySel.value = _getBniYearAdmin();
if (!sel.value) document.getElementById('coord-metrics-form').style.display = 'none';
}
async function toggleCoordinatorReport(code, currentEnabled) {
const token = getAdminToken();
if (!token) { alert('Bitte Admin-Token in den Einstellungen eintragen.'); return; }
try {
const cfgRes = await fetch(giteaDataUrl(`chapters/${code}/config.json`), { cache: 'no-store' });
if (!cfgRes.ok) { alert('Config nicht gefunden.'); return; }
const cfg = await cfgRes.json();
const shaRes = await fetch(giteaAuthUrl(`chapters/${code}/config.json`, token, 'data'));
if (!shaRes.ok) { alert('SHA-Fetch fehlgeschlagen.'); return; }
const sha = (await shaRes.json()).sha;
cfg.memberCoordinatorReport = !currentEnabled;
const content = btoa(unescape(encodeURIComponent(JSON.stringify(cfg, null, 2))));
const res = await fetch(giteaAuthUrl(`chapters/${code}/config.json`, token, 'data'), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: `Koordinatoren-Bericht ${cfg.memberCoordinatorReport ? 'aktiviert' : 'deaktiviert'} für ${code}`, content, sha, branch: 'data' })
});
if (res.ok) { loadAllChapters(); }
else { alert('Fehler beim Speichern der Konfiguration.'); }
} catch(e) { alert('Verbindungsfehler: ' + e.message); }
}
async function loadCoordinatorReportData() {
const code = document.getElementById('coord-chapter-select').value;
const year = document.getElementById('coord-year-select').value;
const form = document.getElementById('coord-metrics-form');
if (!code) { form.style.display = 'none'; _coordReportData = null; return; }
form.style.display = 'block';
_coordReportData = null;
try {
const res = await fetch(giteaDataUrl(`chapters/${code}/coordinator-report-${year}.json`), { cache: 'no-store' });
if (res.ok) _coordReportData = await res.json();
} catch(e) {}
if (!_coordReportData) {
_coordReportData = { year, metrics: _coordMetrics.map(m => ({ key: m.key, label: m.label, unit: m.unit, annualTarget: 0, entries: [] })) };
}
// Ensure all metrics exist
for (const m of _coordMetrics) {
if (!_coordReportData.metrics.find(x => x.key === m.key)) {
_coordReportData.metrics.push({ key: m.key, label: m.label, unit: m.unit, annualTarget: 0, entries: [] });
}
}
// Drop malformed entries (missing month) to avoid sort/compare crashes downstream
for (const m of _coordReportData.metrics) {
if (Array.isArray(m.entries)) m.entries = m.entries.filter(e => e && e.month);
}
// Render target rows
const targetsContainer = document.getElementById('coord-targets-rows');
targetsContainer.innerHTML = '';
for (const m of _coordMetrics) {
const existing = _coordReportData.metrics.find(x => x.key === m.key) || {};
const row = document.createElement('div');
row.className = 'coord-metric-row';
row.innerHTML = `<div style="font-size:0.88rem;">${escHtml(m.label)}${m.unit ? ' (' + m.unit + ')' : ''}</div>
<input type="number" id="coord-target-${m.key}" value="${existing.annualTarget || ''}" placeholder="Ziel" min="0" step="any">`;
targetsContainer.appendChild(row);
}
// Populate month dropdown and select current month
_buildMonthDropdown(year);
loadMonthlyEntryForMonth();
updateEntriesSummary();
}
function _bniMonths(year) {
const [y1, y2] = year.split('-').map(Number);
const DE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
return [
{ val: `${y1}-04`, label: `April ${y1}` },
{ val: `${y1}-05`, label: `Mai ${y1}` },
{ val: `${y1}-06`, label: `Juni ${y1}` },
{ val: `${y1}-07`, label: `Juli ${y1}` },
{ val: `${y1}-08`, label: `August ${y1}` },
{ val: `${y1}-09`, label: `September ${y1}` },
{ val: `${y1}-10`, label: `Oktober ${y1}` },
{ val: `${y1}-11`, label: `November ${y1}` },
{ val: `${y1}-12`, label: `Dezember ${y1}` },
{ val: `${y2}-01`, label: `Januar ${y2}` },
{ val: `${y2}-02`, label: `Februar ${y2}` },
{ val: `${y2}-03`, label: `März ${y2}` },
];
}
function _buildMonthDropdown(year) {
const sel = document.getElementById('coord-month-select');
if (!sel) return;
const now = new Date();
const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
const months = _bniMonths(year);
sel.innerHTML = months.map(m =>
`<option value="${m.val}"${m.val === currentMonth ? ' selected' : ''}>${m.label}</option>`
).join('');
}
function loadMonthlyEntryForMonth() {
const monthStr = document.getElementById('coord-month-select').value;
const container = document.getElementById('coord-monthly-rows');
const deleteBtn = document.getElementById('coord-delete-month-btn');
container.innerHTML = '';
if (!_coordReportData || !monthStr) return;
const existing = {};
for (const m of _coordReportData.metrics) {
const entry = (m.entries || []).find(e => e.month === monthStr);
if (entry) existing[m.key] = entry.value;
}
const hasEntry = Object.keys(existing).length > 0;
if (deleteBtn) deleteBtn.style.display = hasEntry ? '' : 'none';
for (const m of _coordMetrics) {
const row = document.createElement('div');
row.className = 'coord-metric-row';
row.innerHTML = `<div style="font-size:0.88rem;">${escHtml(m.label)}${m.unit ? ' (' + m.unit + ')' : ''}</div>
<input type="number" id="coord-monthly-${m.key}" value="${existing[m.key] ?? ''}" placeholder="Ist" min="0" step="any">`;
container.appendChild(row);
}
}
function updateEntriesSummary() {
const el = document.getElementById('coord-entries-summary');
if (!_coordReportData || !el) return;
const allMonths = new Set();
for (const m of _coordReportData.metrics) {
for (const e of (m.entries || [])) allMonths.add(e.month);
}
const months = [...allMonths].sort();
el.textContent = months.length === 0
? 'Noch keine Einträge für dieses Jahr.'
: `${months.length} Monat(e) erfasst – zuletzt: ${months[months.length - 1]}`;
}
function _coordStatus(msg, kind) {
const el = document.getElementById('coord-status');
if (!el) return;
el.textContent = msg;
el.style.color = kind === 'error' ? '#dc2626' : kind === 'success' ? 'var(--success)' : 'var(--muted)';
}
async function saveCoordinatorTargets() {
const code = document.getElementById('coord-chapter-select').value;
if (!code) { _coordStatus('✗ Bitte zuerst ein Chapter auswählen.', 'error'); return; }
if (!_coordReportData) { _coordStatus('✗ Bericht nicht geladen – Chapter neu auswählen.', 'error'); return; }
try {
for (const m of _coordMetrics) {
const metric = _coordReportData.metrics.find(x => x.key === m.key);
const inputEl = document.getElementById(`coord-target-${m.key}`);
if (metric && inputEl) metric.annualTarget = parseFloat(inputEl.value) || 0;
}
_coordReportData.updatedAt = new Date().toISOString().slice(0, 10);
await _saveCoordinatorReportToGitea(code, _coordReportData);
} catch(e) {
_coordStatus('✗ Fehler: ' + (e && e.message ? e.message : e), 'error');
}
}
async function saveMonthlyEntry() {
const code = document.getElementById('coord-chapter-select').value;
const monthStr = document.getElementById('coord-month-select').value;
if (!code) { _coordStatus('✗ Bitte zuerst ein Chapter auswählen.', 'error'); return; }
if (!monthStr) { _coordStatus('✗ Bitte Monat auswählen.', 'error'); return; }
if (!_coordReportData) { _coordStatus('✗ Bericht nicht geladen – Chapter neu auswählen.', 'error'); return; }
try {
for (const m of _coordMetrics) {
const input = document.getElementById(`coord-monthly-${m.key}`);
if (!input || input.value === '') continue;
const value = parseFloat(input.value);
if (isNaN(value)) continue;
let metric = _coordReportData.metrics.find(x => x.key === m.key);
if (!metric) { metric = { key: m.key, label: m.label, unit: m.unit, annualTarget: 0, entries: [] }; _coordReportData.metrics.push(metric); }
if (!metric.entries) metric.entries = [];
const idx = metric.entries.findIndex(e => e.month === monthStr);
if (idx >= 0) metric.entries[idx].value = value;
else { metric.entries.push({ month: monthStr, value }); metric.entries.sort((a, b) => (a.month || '').localeCompare(b.month || '')); }
}
_coordReportData.updatedAt = new Date().toISOString().slice(0, 10);
await _saveCoordinatorReportToGitea(code, _coordReportData);
loadMonthlyEntryForMonth();
updateEntriesSummary();
} catch(e) {
_coordStatus('✗ Fehler: ' + (e && e.message ? e.message : e), 'error');
}
}
async function deleteMonthlyEntry() {
const code = document.getElementById('coord-chapter-select').value;
const monthStr = document.getElementById('coord-month-select').value;
if (!code || !monthStr || !_coordReportData) return;
if (!confirm(`Eintrag für ${monthStr} löschen?`)) return;
for (const m of _coordReportData.metrics) {
m.entries = (m.entries || []).filter(e => e.month !== monthStr);
}
_coordReportData.updatedAt = new Date().toISOString().slice(0, 10);
await _saveCoordinatorReportToGitea(code, _coordReportData);
loadMonthlyEntryForMonth();
updateEntriesSummary();
}
async function _saveCoordinatorReportToGitea(code, data) {
const token = getAdminToken();
if (!token) {
_coordStatus('✗ Admin-Token fehlt – bitte in den Einstellungen eintragen.', 'error');
alert('Bitte Admin-Token in den Einstellungen eintragen.');
return;
}
const year = data.year || document.getElementById('coord-year-select').value;
const path = `chapters/${code}/coordinator-report-${year}.json`;
_coordStatus('Speichere…');
let sha = null;
try {
const shaRes = await fetch(giteaAuthUrl(path, token, 'data'));
if (shaRes.ok) sha = (await shaRes.json()).sha;
} catch(e) {}
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const body = { message: `Update Koordinatoren-Bericht ${year} für ${code}`, content, branch: 'data' };
if (sha) body.sha = sha;
try {
const res = await fetch(giteaAuthUrl(path, token, 'data'), {
method: sha ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
_coordStatus('✓ Gespeichert (' + path + ')', 'success');
setTimeout(() => _coordStatus('', ''), 5000);
} else {
let detail = '';
try { detail = (await res.text()).slice(0, 200); } catch(e) {}
_coordStatus(`✗ Fehler beim Speichern (HTTP ${res.status}). ${detail}`, 'error');
}
} catch(e) {
_coordStatus('✗ Verbindungsfehler: ' + (e && e.message ? e.message : e), 'error');
}
}
// ============================================================ // ============================================================
// INIT // INIT
// ============================================================ // ============================================================

View File

@@ -1,500 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BNI Vorstellungsrunde – Anleitung</title>
<style>
:root {
--red: #cc0000; --navy: #1a2744; --bg: #f0f2f5;
--card: #fff; --muted: #6b7280; --border: #e5e7eb;
--success: #16a34a;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:var(--bg); color:#1a1a2e; line-height:1.6; }
.cover { background:linear-gradient(160deg,#0f1f45,#1a2744,#0d1b35); color:white; padding:60px 40px; text-align:center; }
.cover h1 { font-size:2rem; font-weight:800; letter-spacing:-0.5px; }
.cover .subtitle { color:rgba(255,255,255,0.6); margin-top:8px; font-size:1rem; }
.cover .badge { display:inline-block; margin-top:24px; background:var(--red); color:white; padding:6px 18px; border-radius:20px; font-size:0.82rem; font-weight:600; }
.toc { background:white; padding:24px 32px; border-bottom:1px solid var(--border); }
.toc h2 { font-size:0.78rem; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:1px; margin-bottom:12px; }
.toc ol { padding-left:18px; }
.toc li { font-size:0.9rem; padding:3px 0; }
.toc a { color:var(--navy); text-decoration:none; }
.toc a:hover { color:var(--red); }
.content { max-width:820px; margin:0 auto; padding:32px 24px 64px; }
.section { margin-bottom:48px; }
.section-header { display:flex; align-items:center; gap:14px; margin-bottom:20px; padding-bottom:12px; border-bottom:2px solid var(--red); }
.step-num { width:36px; height:36px; background:var(--red); color:white; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:800; font-size:1rem; flex-shrink:0; }
.section-header h2 { font-size:1.25rem; font-weight:700; color:var(--navy); }
.card { background:white; border-radius:12px; border:1px solid var(--border); padding:20px; margin-bottom:16px; }
.tip { background:#fef3c7; border:1px solid #fcd34d; border-radius:10px; padding:14px 16px; font-size:0.88rem; margin-bottom:16px; }
.tip strong { color:#92400e; }
.info { background:#eff6ff; border:1px solid #bfdbfe; border-radius:10px; padding:14px 16px; font-size:0.88rem; margin-bottom:16px; }
.steps { counter-reset:step; }
.step { display:flex; gap:14px; margin-bottom:12px; align-items:flex-start; }
.step::before { counter-increment:step; content:counter(step); width:24px; height:24px; background:var(--navy); color:white; border-radius:50%; font-size:0.78rem; font-weight:700; display:flex; align-items:center; justify-content:center; flex-shrink:0; margin-top:2px; }
.step p { font-size:0.9rem; }
.step code { background:#f3f4f6; padding:1px 6px; border-radius:4px; font-size:0.85rem; }
/* ——— MOCKUPS ——— */
.mockup { background:#1a1a2e; border-radius:12px; overflow:hidden; margin:16px 0; box-shadow:0 4px 20px rgba(0,0,0,0.2); }
.mockup-bar { background:#0f1420; padding:8px 14px; display:flex; align-items:center; gap:8px; }
.mockup-dot { width:10px; height:10px; border-radius:50%; }
.mockup-body { padding:16px; }
.mock-activation { background:linear-gradient(160deg,#0f1f45,#1a2744); padding:32px 20px; text-align:center; border-radius:8px; }
.mock-activation h3 { color:white; font-size:1.1rem; margin-bottom:6px; }
.mock-activation p { color:rgba(255,255,255,0.5); font-size:0.82rem; margin-bottom:20px; }
.mock-input { background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2); border-radius:8px; padding:10px 14px; color:white; font-size:0.9rem; text-align:center; margin-bottom:12px; width:100%; max-width:280px; }
.mock-btn-red { background:#cc0000; color:white; border:none; border-radius:8px; padding:10px 24px; font-weight:700; cursor:pointer; font-size:0.9rem; }
.mock-btn-gray { background:#374151; color:white; border:none; border-radius:8px; padding:8px 16px; font-weight:600; cursor:pointer; font-size:0.82rem; }
.mock-panel { background:#1e2235; border-radius:10px; padding:12px; }
.mock-member { display:flex; align-items:center; gap:10px; padding:8px 10px; background:rgba(255,255,255,0.05); border-radius:8px; margin-bottom:6px; }
.mock-avatar { width:36px; height:36px; border-radius:50%; object-fit:cover; flex-shrink:0; display:flex; align-items:center; justify-content:center; font-size:0.75rem; font-weight:700; color:white; }
.mock-name { font-size:0.85rem; font-weight:600; color:white; }
.mock-role { font-size:0.72rem; color:rgba(255,255,255,0.5); }
.mock-badge { padding:2px 8px; border-radius:12px; font-size:0.7rem; font-weight:700; margin-left:auto; }
.mock-badge.absent { background:rgba(204,0,0,0.2); color:#f87171; }
.mock-badge.present { background:rgba(22,163,74,0.2); color:#4ade80; }
.mock-timer { text-align:center; padding:20px; }
.mock-timer-ring { width:100px; height:100px; border-radius:50%; border:6px solid #cc0000; display:flex; align-items:center; justify-content:center; margin:0 auto 12px; }
.mock-timer-num { font-size:2rem; font-weight:800; color:white; }
.mock-timer-name { color:white; font-weight:700; font-size:1rem; }
.mock-timer-company { color:rgba(255,255,255,0.5); font-size:0.8rem; }
.mock-tabs { display:flex; border-bottom:1px solid rgba(255,255,255,0.1); margin-bottom:12px; }
.mock-tab { padding:8px 14px; font-size:0.78rem; color:rgba(255,255,255,0.4); border-bottom:2px solid transparent; }
.mock-tab.active { color:#cc0000; border-bottom-color:#cc0000; }
.mock-sub-modal { background:#2a2f45; border-radius:12px; padding:16px; }
.mock-sub-modal h4 { color:white; font-size:0.9rem; margin-bottom:12px; }
.mock-field { background:rgba(255,255,255,0.1); border-radius:6px; padding:8px 10px; color:rgba(255,255,255,0.7); font-size:0.8rem; margin-bottom:8px; }
.mock-sv-card { display:flex; align-items:center; gap:8px; background:rgba(255,255,255,0.07); border-radius:8px; padding:8px 10px; margin-bottom:6px; border:1px solid transparent; cursor:pointer; }
.mock-sv-card:hover { border-color:rgba(204,0,0,0.5); }
.mock-sv-name { color:white; font-size:0.82rem; font-weight:600; }
.mock-sv-meta { color:rgba(255,255,255,0.45); font-size:0.72rem; }
.admin-mockup { background:#f0f2f5; border-radius:12px; overflow:hidden; border:1px solid var(--border); margin:16px 0; }
.admin-header { background:#1a2744; color:white; padding:12px 16px; font-size:0.9rem; font-weight:700; }
.admin-tabs { background:white; display:flex; border-bottom:1px solid var(--border); padding:0 16px; }
.admin-tab { padding:10px 16px; font-size:0.82rem; font-weight:600; color:var(--muted); border-bottom:2px solid transparent; }
.admin-tab.active { color:var(--red); border-bottom-color:var(--red); }
.admin-body { padding:16px; }
.admin-member-row { display:flex; align-items:center; gap:10px; padding:8px 0; border-bottom:1px solid var(--border); cursor:pointer; }
.admin-member-row:last-child { border-bottom:none; }
.admin-photo-badge { font-size:0.72rem; margin-left:auto; }
.admin-photo-badge.ok { color:var(--success); }
.admin-photo-badge.missing { color:#d97706; }
.highlight-box { border-left:3px solid var(--red); padding:10px 16px; background:white; border-radius:0 8px 8px 0; margin:12px 0; font-size:0.88rem; }
@media print {
.cover { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
.section { page-break-inside:avoid; }
.mockup, .admin-mockup { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
}
</style>
</head>
<body>
<div class="cover">
<div style="font-size:2.5rem; margin-bottom:8px;">🌟</div>
<h1>BNI Vorstellungsrunde</h1>
<div class="subtitle">Benutzeranleitung für Chapter-Admins</div>
<div class="badge">Version 1.1 · April 2026</div>
</div>
<div class="toc">
<h2>Inhalt</h2>
<ol>
<li><a href="#start">Tool starten</a></li>
<li><a href="#aktivierung">Erstmalige Aktivierung</a></li>
<li><a href="#ansichten">Ansichten: Display &amp; Steuerung</a></li>
<li><a href="#vorbereitung">Meeting vorbereiten</a></li>
<li><a href="#durchfuehrung">Meeting durchführen</a></li>
<li><a href="#vertreter">Vertreter eintragen</a></li>
<li><a href="#besucher">Besucher eintragen</a></li>
<li><a href="#admin">Admin-Panel: Fotos &amp; Stammvertreter</a></li>
<li><a href="#offline">Offline-Nutzung</a></li>
</ol>
</div>
<div class="content">
<!-- ========== 1. START ========== -->
<div class="section" id="start">
<div class="section-header">
<div class="step-num">1</div>
<h2>Tool starten</h2>
</div>
<div class="card">
<div class="steps">
<div class="step"><p>Den Ordner <code>BNI Sonne</code> im Finder öffnen.</p></div>
<div class="step"><p>Die Datei <strong>start.command</strong> doppelklicken — es öffnet sich kurz ein Terminal-Fenster, dann startet Chrome automatisch mit dem Tool.</p></div>
<div class="step"><p>Das Terminal-Fenster im Hintergrund lassen (nicht schließen, solange du das Tool nutzt).</p></div>
</div>
</div>
<div class="tip">
<strong>Hinweis:</strong> Beim ersten Doppelklick erscheint eventuell eine Sicherheitswarnung von macOS. Dann: Rechtsklick auf <code>start.command</code> → „Öffnen" → „Öffnen" bestätigen. Ab dem zweiten Mal klappt der Doppelklick direkt.
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">Terminal</span>
</div>
<div class="mockup-body">
<p style="color:#4ade80;font-size:0.82rem;font-family:monospace;">$ python3 -m http.server 8181</p>
<p style="color:rgba(255,255,255,0.4);font-size:0.82rem;font-family:monospace;">Serving HTTP on 0.0.0.0 port 8181 …</p>
</div>
</div>
</div>
<!-- ========== 2. AKTIVIERUNG ========== -->
<div class="section" id="aktivierung">
<div class="section-header">
<div class="step-num">2</div>
<h2>Erstmalige Aktivierung</h2>
</div>
<div class="info">
Dieser Schritt ist nur beim <strong>allerersten Start</strong> auf einem neuen Gerät oder Browser nötig. Danach öffnet das Tool direkt.
</div>
<div class="card">
<div class="steps">
<div class="step"><p>Den <strong>Lizenzcode</strong> deines Chapters eingeben (erhältst du vom Administrator).</p></div>
<div class="step"><p>Auf <strong>„Aktivieren"</strong> klicken. Das Tool lädt einmalig die Chapter-Daten vom Server.</p></div>
<div class="step"><p>Nach erfolgreicher Aktivierung öffnet sich direkt die Steuerungsansicht.</p></div>
</div>
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">http://localhost:8181</span>
</div>
<div class="mockup-body">
<div class="mock-activation">
<h3>BNI Vorstellungsrunde</h3>
<p>Bitte Chapter-Code eingeben</p>
<div style="margin-bottom:8px;"><input class="mock-input" placeholder="z. B. sonne-7x4m9k" readonly></div>
<button class="mock-btn-red">Aktivieren</button>
</div>
</div>
</div>
</div>
<!-- ========== 3. ANSICHTEN ========== -->
<div class="section" id="ansichten">
<div class="section-header">
<div class="step-num">3</div>
<h2>Ansichten: Display &amp; Steuerung</h2>
</div>
<div class="card">
<p style="font-size:0.9rem;">Das Tool läuft in <strong>zwei Fenstern gleichzeitig</strong>:</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:14px;">
<div style="background:#f8f9fa;border-radius:8px;padding:14px;border-left:3px solid var(--navy);">
<div style="font-weight:700;font-size:0.88rem;color:var(--navy);margin-bottom:6px;">🖥 Display-Fenster</div>
<div style="font-size:0.82rem;color:var(--muted);">Wird auf dem Präsentationsmonitor / Beamer angezeigt. Zeigt das aktuelle Mitglied groß mit Timer.</div>
</div>
<div style="background:#f8f9fa;border-radius:8px;padding:14px;border-left:3px solid var(--red);">
<div style="font-weight:700;font-size:0.88rem;color:var(--red);margin-bottom:6px;">🎛 Steuerungs-Fenster</div>
<div style="font-size:0.82rem;color:var(--muted);">Dein Laptop-Bildschirm. Hier steuerst du den Ablauf, trägst Vertreter ein und navigierst.</div>
</div>
</div>
</div>
<div class="highlight-box">
Beim Öffnen des Tools wird automatisch ein zweites Tab für das Display geöffnet. Ziehe dieses Tab auf den Beamer-Bildschirm und drücke <strong>F11</strong> für Vollbild.
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">Display-Ansicht (Beamer)</span>
</div>
<div class="mockup-body">
<div class="mock-timer">
<div style="color:rgba(255,255,255,0.4);font-size:0.75rem;margin-bottom:10px;">JETZT SPRICHT</div>
<div class="mock-timer-ring">
<span class="mock-timer-num">45</span>
</div>
<div class="mock-timer-name">Maria Mustermann</div>
<div class="mock-timer-company">Muster GmbH · Steuerberatung</div>
<div style="color:rgba(255,255,255,0.3);font-size:0.75rem;margin-top:8px;">Nächster: Hans Müller</div>
</div>
</div>
</div>
</div>
<!-- ========== 4. VORBEREITUNG ========== -->
<div class="section" id="vorbereitung">
<div class="section-header">
<div class="step-num">4</div>
<h2>Meeting vorbereiten</h2>
</div>
<div class="card">
<p style="font-weight:700;font-size:0.9rem;margin-bottom:10px;">Vor dem Meeting: Abwesenheiten markieren</p>
<div class="steps">
<div class="step"><p>Im Steuerungs-Fenster: Tab <strong>„Mitglieder"</strong> öffnen.</p></div>
<div class="step"><p>Auf den Namen eines abwesenden Mitglieds klicken → kleines Menü öffnet sich.</p></div>
<div class="step"><p><strong>„Abwesend"</strong> wählen — das Mitglied wird aus der Vorstellungsrunde ausgeblendet (roter Punkt).</p></div>
<div class="step"><p>Optional: Reihenfolge per <strong>Drag &amp; Drop</strong> anpassen.</p></div>
</div>
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">Steuerung – Mitgliederliste</span>
</div>
<div class="mockup-body">
<div class="mock-tabs">
<div class="mock-tab active">Mitglieder</div>
<div class="mock-tab">Vertretungen</div>
<div class="mock-tab">Besucher</div>
</div>
<div class="mock-panel">
<div class="mock-member">
<div class="mock-avatar" style="background:#1a2744;">AM</div>
<div><div class="mock-name">Anna Müller</div><div class="mock-role">Steuerberatung</div></div>
<div class="mock-badge present">anwesend</div>
</div>
<div class="mock-member" style="opacity:0.5;">
<div class="mock-avatar" style="background:#7f1d1d;">KS</div>
<div><div class="mock-name">Klaus Schmidt</div><div class="mock-role">IT-Systeme</div></div>
<div class="mock-badge absent">abwesend</div>
</div>
<div class="mock-member">
<div class="mock-avatar" style="background:#1e3a5f;">SL</div>
<div><div class="mock-name">Sandra Lang</div><div class="mock-role">Marketing</div></div>
<div class="mock-badge present">anwesend</div>
</div>
</div>
</div>
</div>
</div>
<!-- ========== 5. DURCHFÜHRUNG ========== -->
<div class="section" id="durchfuehrung">
<div class="section-header">
<div class="step-num">5</div>
<h2>Meeting durchführen</h2>
</div>
<div class="card">
<div class="steps">
<div class="step"><p>Auf <strong>„Start"</strong> klicken — der Timer startet für das erste Mitglied.</p></div>
<div class="step"><p>Wenn die Zeit abgelaufen ist (Timer rot): <strong>„Weiter →"</strong> drücken für das nächste Mitglied.</p></div>
<div class="step"><p>Mit <strong>„← Zurück"</strong> kannst du jederzeit zum vorherigen Mitglied springen.</p></div>
<div class="step"><p>Timer lässt sich jederzeit mit <strong>„Pause / Fortsetzen"</strong> anhalten.</p></div>
<div class="step"><p>Am Ende erscheint eine Abschluss-Ansicht — mit <strong>„Neu starten"</strong> zurück zum Anfang.</p></div>
</div>
</div>
<div class="tip">
<strong>Tipp:</strong> Die Tastenkombination <strong>→ Pfeiltaste</strong> auf der Tastatur funktioniert als „Weiter" — praktisch für die Fernbedienung.
</div>
</div>
<!-- ========== 6. VERTRETER ========== -->
<div class="section" id="vertreter">
<div class="section-header">
<div class="step-num">6</div>
<h2>Vertreter eintragen</h2>
</div>
<div class="card">
<p style="font-size:0.9rem;margin-bottom:12px;">Wenn ein Mitglied einen Vertreter schickt, wird dieser in der Vorstellungsrunde an Stelle des Mitglieds angezeigt.</p>
<div class="steps">
<div class="step"><p>Tab <strong>„Vertretungen"</strong> öffnen.</p></div>
<div class="step"><p>Mitglied aus der Liste wählen, das vertreten wird.</p></div>
<div class="step"><p>Falls die Person bereits als <strong>Stammvertreter</strong> gespeichert ist: oben aus der Schnellauswahl wählen — alle Felder werden automatisch ausgefüllt.</p></div>
<div class="step"><p>Andernfalls: Name, Firma und Fachgebiet des Vertreters eingeben und ggf. ein Foto hochladen.</p></div>
<div class="step"><p>Auf <strong>„Speichern"</strong> klicken.</p></div>
</div>
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">Steuerung – Vertretung eintragen</span>
</div>
<div class="mockup-body">
<div class="mock-sub-modal">
<h4>Vertreter für Klaus Schmidt</h4>
<div style="font-size:0.72rem;color:rgba(255,255,255,0.4);margin-bottom:8px;">STAMMVERTRETER WÄHLEN</div>
<div class="mock-sv-card">
<div class="mock-avatar" style="background:#1a2744;width:32px;height:32px;">MF</div>
<div><div class="mock-sv-name">Max Fischer</div><div class="mock-sv-meta">Fischer GmbH · Versicherung</div></div>
</div>
<div class="mock-sv-card">
<div class="mock-avatar" style="background:#4a1942;width:32px;height:32px;">LB</div>
<div><div class="mock-sv-name">Lisa Berger</div><div class="mock-sv-meta">Berger &amp; Co · Marketing</div></div>
</div>
<div style="font-size:0.72rem;color:rgba(255,255,255,0.4);margin:10px 0 6px;">ODER MANUELL EINGEBEN</div>
<div class="mock-field">Name des Vertreters …</div>
<div class="mock-field">Firma …</div>
</div>
</div>
</div>
</div>
<!-- ========== 7. BESUCHER ========== -->
<div class="section" id="besucher">
<div class="section-header">
<div class="step-num">7</div>
<h2>Besucher eintragen</h2>
</div>
<div class="card">
<div class="steps">
<div class="step"><p>Tab <strong>„Besucher"</strong> öffnen.</p></div>
<div class="step"><p>Auf <strong>„+ Besucher hinzufügen"</strong> klicken.</p></div>
<div class="step"><p>Name und optional Firma eingeben und speichern.</p></div>
<div class="step"><p>Besucher erscheinen am Ende der Vorstellungsrunde nach allen Mitgliedern.</p></div>
</div>
</div>
<div class="tip">
<strong>Tipp:</strong> Besucher werden nicht dauerhaft gespeichert — sie werden nach dem Meeting automatisch gelöscht, wenn die Runde zurückgesetzt wird.
</div>
</div>
<!-- ========== 8. ADMIN ========== -->
<div class="section" id="admin">
<div class="section-header">
<div class="step-num">8</div>
<h2>Admin-Panel: Fotos &amp; Stammvertreter</h2>
</div>
<div class="info">
Das Admin-Panel öffnest du über <code>http://localhost:8181/admin.html</code> — am einfachsten über den Link im Steuerungs-Fenster unter dem Tab <strong>„Admin"</strong>.
</div>
<div class="card">
<p style="font-weight:700;font-size:0.9rem;margin-bottom:10px;">Mitgliederfotos hinzufügen</p>
<div class="steps">
<div class="step"><p>Im Admin-Panel Tab <strong>„Mitglieder"</strong> öffnen.</p></div>
<div class="step"><p>Auf ein Mitglied mit <strong>„+ Foto"</strong> klicken.</p></div>
<div class="step"><p>Foto aus dem Finder wählen (JPG/PNG, beliebige Größe — wird automatisch verkleinert).</p></div>
<div class="step"><p>Auf <strong>„Speichern"</strong> klicken — das Foto ist jetzt lokal gespeichert.</p></div>
<div class="step"><p>Nach allen Fotos: <strong>„Auf Server hochladen"</strong> — dann haben alle Geräte die Fotos beim nächsten „Vom Server laden".</p></div>
</div>
</div>
<div class="admin-mockup">
<div class="admin-header">BNI Admin-Panel · BNI Chapter Sonne</div>
<div class="admin-tabs">
<div class="admin-tab">Stammvertreter</div>
<div class="admin-tab active">Mitglieder</div>
<div class="admin-tab">Einstellungen</div>
</div>
<div class="admin-body">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<span style="font-weight:700;font-size:0.9rem;">Mitglieder</span>
<div style="display:flex;gap:8px;">
<span style="background:#f3f4f6;border:1px solid var(--border);border-radius:6px;padding:5px 10px;font-size:0.78rem;font-weight:600;">🔄 Vom Server laden</span>
<span style="background:var(--red);color:white;border-radius:6px;padding:5px 10px;font-size:0.78rem;font-weight:600;">📤 Auf Server hochladen</span>
</div>
</div>
<div class="admin-member-row">
<div class="mock-avatar" style="background:#1a2744;width:40px;height:40px;font-size:0.8rem;">AM</div>
<div style="flex:1;"><div style="font-size:0.88rem;font-weight:600;">Anna Müller</div><div style="font-size:0.75rem;color:var(--muted);">Steuerberatung</div></div>
<span class="admin-photo-badge ok">✓ Foto</span>
</div>
<div class="admin-member-row">
<div class="mock-avatar" style="background:#4a1942;width:40px;height:40px;font-size:0.8rem;">KS</div>
<div style="flex:1;"><div style="font-size:0.88rem;font-weight:600;">Klaus Schmidt</div><div style="font-size:0.75rem;color:var(--muted);">IT-Systeme</div></div>
<span class="admin-photo-badge missing">+ Foto</span>
</div>
<div class="admin-member-row">
<div class="mock-avatar" style="background:#1e3a5f;width:40px;height:40px;font-size:0.8rem;">SL</div>
<div style="flex:1;"><div style="font-size:0.88rem;font-weight:600;">Sandra Lang</div><div style="font-size:0.75rem;color:var(--muted);">Marketing</div></div>
<span class="admin-photo-badge ok">✓ Foto</span>
</div>
</div>
</div>
<div class="card" style="margin-top:16px;">
<p style="font-weight:700;font-size:0.9rem;margin-bottom:10px;">Stammvertreter anlegen</p>
<p style="font-size:0.88rem;color:var(--muted);margin-bottom:12px;">Stammvertreter sind Personen, die regelmäßig als Vertreter kommen. Einmal anlegen → bei jeder Vertretung schnell auswählen.</p>
<div class="steps">
<div class="step"><p>Tab <strong>„Stammvertreter"</strong> öffnen.</p></div>
<div class="step"><p>Auf <strong>„+ Hinzufügen"</strong> klicken.</p></div>
<div class="step"><p>Name, Firma, Fachgebiet und Foto eingeben.</p></div>
<div class="step"><p>Optional: <strong>„Aus anderem Chapter"</strong> — Mitglied aus einem anderen Chapter-Verzeichnis als Stammvertreter übernehmen.</p></div>
<div class="step"><p>Auf <strong>„Auf Server sichern"</strong> klicken, damit die Liste nicht verloren geht.</p></div>
</div>
</div>
</div>
<!-- ========== 9. OFFLINE ========== -->
<div class="section" id="offline">
<div class="section-header">
<div class="step-num">9</div>
<h2>Offline-Nutzung</h2>
</div>
<div class="card">
<p style="font-size:0.9rem;margin-bottom:12px;">Das Tool funktioniert <strong>vollständig ohne Internet</strong>, sobald es einmal online eingerichtet wurde.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div style="background:#f0fdf4;border-radius:8px;padding:12px;border:1px solid #bbf7d0;">
<div style="font-weight:700;color:var(--success);font-size:0.85rem;margin-bottom:6px;">✓ Funktioniert offline</div>
<ul style="font-size:0.82rem;color:#166534;padding-left:16px;line-height:1.7;">
<li>Meeting starten &amp; steuern</li>
<li>Timer, Navigation</li>
<li>Abwesenheiten markieren</li>
<li>Vertreter &amp; Besucher eintragen</li>
<li>Stammvertreter verwenden</li>
</ul>
</div>
<div style="background:#fef2f2;border-radius:8px;padding:12px;border:1px solid #fecaca;">
<div style="font-weight:700;color:#dc2626;font-size:0.85rem;margin-bottom:6px;">✗ Braucht Internet</div>
<ul style="font-size:0.82rem;color:#991b1b;padding-left:16px;line-height:1.7;">
<li>Erstmalige Aktivierung</li>
<li>Mitglieder vom Server laden</li>
<li>Fotos auf Server hochladen</li>
<li>Stammvertreter sichern</li>
</ul>
</div>
</div>
</div>
<div class="highlight-box">
<strong>Empfehlung:</strong> Vor jedem Meeting einmal kurz „Vom Server laden" klicken — so hast du immer die aktuellste Mitgliederliste &amp; Fotos, auch wenn beim Meeting kein Internet verfügbar ist.
</div>
</div>
<!-- FOOTER -->
<div style="text-align:center;padding:32px 0;color:var(--muted);font-size:0.8rem;border-top:1px solid var(--border);margin-top:32px;">
BNI Vorstellungsrunde · Interne Anleitung · Bei Fragen an den Administrator wenden
</div>
</div><!-- .content -->
</body>
</html>

BIN
apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
chapters/.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,6 @@
{
"chapterName": "BNI Chapter Epsilon",
"syncUrl": "https://bni-ulm.com/epsilon/de/memberlist?chapterName=&regionIds=43671&isChapterwebsite=true",
"websiteUrl": "https://bni-ulm.de/epsilon",
"defaultTimer": 60
}

View File

@@ -0,0 +1,6 @@
{
"chapterName": "BNI Chapter Helios",
"syncUrl": "https://bni-konstanz.de/helios/de/memberlist?chapterName=40832&regionIds=6023&isChapterwebsite=true",
"websiteUrl": "https://bni-konstanz.de/helios",
"defaultTimer": 60
}

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{ {
"chapterName": "BNI Chapter Callisto", "chapterName": "BNI Chapter Kallisto",
"syncUrl": "https://bni-ulm.com/kallisto/de/memberlist?chapterName=35844&regionIds=43671$isChapterwebsite", "syncUrl": "https://bni-ulm.com/kallisto/de/memberlist?chapterName=35844&regionIds=43671$isChapterwebsite",
"websiteUrl": "https://bni-ulm.de/kallisto", "websiteUrl": "https://bni-ulm.de/kallisto",
"defaultTimer": 60 "defaultTimer": 60

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +0,0 @@
{
"lastUpdated": "2026-04-17",
"members": []
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,90 @@
{
"items": [
{
"id": "willkommen",
"label": "Willkommen",
"type": "embed",
"notes": "wie könnte euer Produkt als Weihnachtsgeschenk vermarktet werden?\n\nVertreter:\nAlexander Kaya für Sabine Welp\nSandra Stengel für Nathalie Musto\nStefan Roth für Jennifer Born\nNiko Gäßler für Thomas Stetter\nAlexander Weimer für Jens Berger\n\n\nWert: Lebenslanges Lernen - was habt ihr dieses Jahr gelernt?",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g063c60ec44c3347ad841c139671232dff"
},
{
"id": "trainingsbeitrag",
"label": "Trainingsbeitrag",
"type": "embed",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g066608ae9da024f8e87c0e705dbe6852f"
},
{
"id": "visitenkarten",
"label": "Visitenkarten",
"type": "image",
"notes": "07:15",
"imagePath": "agenda/visitenkarten.jpg"
},
{
"id": "verleihung-der-lumora",
"label": "Verleihung der Lumora",
"type": "embed",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0385ba8f3e8a8422ab051f1591bcf598d"
},
{
"id": "bericht-des-mitgliederkoordinators",
"label": "Bericht des Mitgliederkoordinators",
"type": "embed",
"notes": "07:48",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0d27561ab34c141a39f981ab6b36fbd03"
},
{
"id": "break-out",
"label": "Break Out",
"type": "embed",
"notes": "07:51",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0fbb79876ba15437abf469c098a72b789"
},
{
"id": "naechste-hauptpraesentation",
"label": "Nächste Hauptpräsentation",
"type": "embed",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0ba90bb948a91426d9bb4bbb59bb5bf9d"
},
{
"id": "marketing-ergebnisrunde-erklaerung",
"label": "Marketing- Ergebnisrunde Erklärung",
"type": "embed",
"notes": "08:12",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0848053a6eea54f18bbaaee7d33861386"
},
{
"id": "feedback-gaeste",
"label": "Feedback Gäste",
"type": "embed",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0da62ecf51cdf4d9285097db2c9b80b26"
},
{
"id": "ziehung-des-tagespreises",
"label": "Ziehung des Tagespreises",
"type": "embed",
"notes": "08:20",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0b231f4b1bdf34850a8a8baca4b2d6972"
},
{
"id": "bericht-des-schatzmeisters",
"label": "Bericht des Schatzmeisters",
"type": "embed",
"notes": "wer am Termin in Dellmensingen nicht da ist, denk daran das Geld davor an Jens zu geben / überweisen - wir bringen ihn sonst in die blöde Lage, dass er privat das Geld auslegen muss",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0249a5f37a97e419db184bef50bcbdb8e"
},
{
"id": "bericht-des-eventkoordinators",
"label": "Bericht des Eventkoordinators",
"type": "embed",
"notes": "08:23",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0ad05a18b97214634818823cd3bd3ebca"
},
{
"id": "zitat",
"label": "Zitat",
"type": "embed",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g03d4fc1ad7a8141c098addbee9dfcbffb"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

@@ -1,6 +1,7 @@
{ {
"chapterName": "BNI Chapter Lumora Hybrid", "chapterName": "BNI Chapter Lumora Hybrid",
"syncUrl": "https://bni-ulm.com/lumora/de/memberlist?chapterName=44269&regionIds=43671$isChapterwebsite", "logoUrl": "BNI Logo.png",
"websiteUrl": "https://bni-ulm.com/lumora", "syncUrl": "https://bni-ulm.com/lumora/de/memberlist?chapterName=44269&regionIds=43671&isChapterwebsite=true",
"websiteUrl": "https://bni-ulm.de/lumora",
"defaultTimer": 60 "defaultTimer": 60
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,41 @@
{
"items": [
{
"id": "bericht-der-schatzmeisterin",
"label": "Bericht der Schatzmeisterin",
"type": "image",
"notes": "Hier sollte was stehen",
"imagePath": "agenda/bericht-der-schatzmeisterin.jpg"
},
{
"id": "beispieldatei",
"label": "Beispieldatei",
"type": "embed",
"embedUrl": "https://workdrive.digiseed.de/writer/open/a5u6x594bfa69700846d88739fa2756082ada"
},
{
"id": "willkommen",
"label": "Willkommen",
"type": "image",
"imagePath": "agenda/willkommen.jpg"
},
{
"id": "ende-logo",
"label": "Ende - Logo",
"type": "image",
"imagePath": "agenda/ende-logo.jpg"
},
{
"id": "witz-des-tages",
"label": "Witz des Tages",
"type": "embed",
"embedUrl": "https://digiseed.de"
},
{
"id": "schatzmeister-offline",
"label": "Schatzmeister Offline",
"type": "image",
"imagePath": "agenda/schatzmeister-offline.jpg"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -2,5 +2,6 @@
"chapterName": "BNI Chapter Sonne", "chapterName": "BNI Chapter Sonne",
"syncUrl": "https://bni-ulm.com/sonne/de/memberlist?chapterName=6035&regionIds=43671&isChapterwebsite=true", "syncUrl": "https://bni-ulm.com/sonne/de/memberlist?chapterName=6035&regionIds=43671&isChapterwebsite=true",
"websiteUrl": "https://bni-ulm.de/sonne", "websiteUrl": "https://bni-ulm.de/sonne",
"defaultTimer": 60 "defaultTimer": 60,
"memberCoordinatorReport": false
} }

View File

@@ -0,0 +1,134 @@
{
"year": "2025-2026",
"metrics": [
{
"key": "attendance",
"label": "Anwesenheit",
"unit": "%",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 98
},
{
"month": "2025-05",
"value": 98
}
]
},
{
"key": "members",
"label": "Mitgliederzahl",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 41
},
{
"month": "2025-05",
"value": 42
}
]
},
{
"key": "newMembers",
"label": "Neue Mitglieder",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 1
},
{
"month": "2025-05",
"value": 1
}
]
},
{
"key": "guests",
"label": "Gäste / Besucher",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 1
},
{
"month": "2025-05",
"value": 6
}
]
},
{
"key": "oneOnOne",
"label": "4-Augen-Gespräche",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 118
},
{
"month": "2025-05",
"value": 72
}
]
},
{
"key": "cte",
"label": "CTE",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 254
},
{
"month": "2025-05",
"value": 59
}
]
},
{
"key": "referrals",
"label": "Empfehlungen",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 70
},
{
"month": "2025-05",
"value": 32
}
]
},
{
"key": "revenueThank",
"label": "Umsatzdanke",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 106513
},
{
"month": "2025-05",
"value": 231210
}
]
}
],
"updatedAt": "2026-05-21"
}

View File

@@ -0,0 +1,134 @@
{
"year": "2026-2027",
"metrics": [
{
"key": "attendance",
"label": "Anwesenheit",
"unit": "%",
"annualTarget": 95,
"entries": [
{
"month": "2026-04",
"value": 96
},
{
"month": "2026-05",
"value": 97
}
]
},
{
"key": "members",
"label": "Mitgliederzahl",
"unit": "",
"annualTarget": 49,
"entries": [
{
"month": "2026-04",
"value": 42
},
{
"month": "2026-05",
"value": 42
}
]
},
{
"key": "newMembers",
"label": "Neue Mitglieder",
"unit": "",
"annualTarget": 7,
"entries": [
{
"month": "2026-04",
"value": 1
},
{
"month": "2026-05",
"value": 1
}
]
},
{
"key": "guests",
"label": "Gäste / Besucher",
"unit": "",
"annualTarget": 10.5,
"entries": [
{
"month": "2026-04",
"value": 0
},
{
"month": "2026-05",
"value": 6
}
]
},
{
"key": "oneOnOne",
"label": "4-Augen-Gespräche",
"unit": "",
"annualTarget": 168,
"entries": [
{
"month": "2026-04",
"value": 56
},
{
"month": "2026-05",
"value": 72
}
]
},
{
"key": "cte",
"label": "CTE",
"unit": "",
"annualTarget": 705.6,
"entries": [
{
"month": "2026-04",
"value": 208
},
{
"month": "2026-05",
"value": 59
}
]
},
{
"key": "referrals",
"label": "Empfehlungen",
"unit": "",
"annualTarget": 100.8,
"entries": [
{
"month": "2026-04",
"value": 34
},
{
"month": "2026-05",
"value": 32
}
]
},
{
"key": "revenueThank",
"label": "Umsatzdanke",
"unit": "",
"annualTarget": 146034,
"entries": [
{
"month": "2026-04",
"value": 26259
},
{
"month": "2026-05",
"value": 231210
}
]
}
],
"updatedAt": "2026-05-24"
}

View File

@@ -0,0 +1,61 @@
{
"updatedAt": "2026-04-27",
"metrics": [
{
"key": "attendance",
"label": "Anwesenheit",
"unit": "%",
"target": 95,
"current": 96
},
{
"key": "members",
"label": "Mitgliederzahl",
"unit": "",
"target": 42,
"current": 42
},
{
"key": "newMembers",
"label": "Neue Mitglieder",
"unit": "",
"target": 1.3,
"current": 1
},
{
"key": "guests",
"label": "Gäste / Besucher",
"unit": "",
"target": 10.5,
"current": 0
},
{
"key": "oneOnOne",
"label": "4-Augen-Gespräche",
"unit": "",
"target": 168,
"current": 56
},
{
"key": "cte",
"label": "CTE",
"unit": "",
"target": 705.6,
"current": 208
},
{
"key": "referrals",
"label": "Empfehlungen",
"unit": "",
"target": 100.8,
"current": 34
},
{
"key": "revenueThank",
"label": "Umsatzdanke",
"unit": "",
"target": 146034,
"current": 26259
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 29 KiB

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

10
favicon.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<g stroke="#F5D822" stroke-width="3.5" stroke-linecap="round">
<path d="M32 2v8M32 54v8M2 32h8M54 32h8M10.8 10.8l5.7 5.7M47.5 47.5l5.7 5.7M10.8 53.2l5.7-5.7M47.5 16.5l5.7-5.7"/>
</g>
<circle cx="32" cy="32" r="18" fill="#F5D822" stroke="#C70607" stroke-width="3"/>
<g fill="none" stroke="#C70607" stroke-width="2.6" stroke-linecap="round">
<path d="M23 29.5q3-4 6 0M35 29.5q3-4 6 0"/>
<path d="M23.5 36q8.5 8 17 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 512 B

1569
index.html

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

4
start.command Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname "$0")"
open "http://localhost:8181/index.html"
python3 -m http.server 8181

View File

@@ -1,4 +1,4 @@
{ {
"version": "1.1.0", "version": "2.2",
"notes": "Multi-Chapter-Support mit Lizenzcode" "notes": "Neue, verschlüsselte Serveradresse. Bitte jetzt aktualisieren – die alte Version kann ab 1. November keine Daten mehr laden oder speichern."
} }