Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e2d222e86 | ||
|
|
fcc63ff69a | ||
|
|
1f4753b753 | ||
|
|
cdda7df17a | ||
|
|
c6891f66ff |
@@ -7,6 +7,25 @@ Versionsnummer in der Fußzeile.
|
||||
> Diese Datei wird erzeugt: `npm run changelog`.
|
||||
> Gepflegt wird der Verlauf in `src/lib/changelog.ts`.
|
||||
|
||||
## 1.7.1 — 21.08.2026
|
||||
|
||||
Bilder zu den neuen Funktionen in der Bedienungsanleitung.
|
||||
|
||||
- Die Bedienungsanleitung zeigt die Funktionen aus 1.7.0 jetzt auch im Bild: Logo im Code, Dublettenwarnung, Filter nach Art, Freigabe je Gruppe, Kurzhilfe und Domain-Bericht.
|
||||
- Alle übrigen Bilder mit erneuert — sie stammten noch aus 1.2.0.
|
||||
- Das Skript, das die Bilder erzeugt, lief seit einer Umbenennung ins Leere und tut es wieder.
|
||||
|
||||
## 1.7.0 — 21.08.2026
|
||||
|
||||
Logo im Code, Freigabe je Gruppe und ein Domain-Check.
|
||||
|
||||
- Ein Logo in der Mitte des Codes — das der Installation oder ein eigenes je Code, 10 bis 25 % der Kantenlänge. Die Fehlerkorrektur springt dabei auf H.
|
||||
- QR-Typen lassen sich je Gruppe freigeben. Die Grundeinstellung bleibt die Grundlage; eine globale Abschaltung gewinnt immer.
|
||||
- Der Knopf „Prüfen“ bei jeder Domain meldet DNS, Zertifikat, Erreichbarkeit — und ob dahinter wirklich diese Installation steht.
|
||||
- Eine Kurzhilfe auf der Domain-Seite führt in sechs Schritten auf, was für eine neue Domain einzurichten ist.
|
||||
- „Meine Codes“ lässt sich nach Art filtern: alle, nur dynamische, nur statische.
|
||||
- Beim Ablegen eines statischen Codes wird gewarnt, wenn derselbe Inhalt schon in der Gruppe liegt.
|
||||
|
||||
## 1.6.0 — 21.08.2026
|
||||
|
||||
Sichtbare Version mit Änderungsverlauf.
|
||||
|
||||
@@ -20,16 +20,18 @@ entscheidet die Verwaltung — nicht gebrauchte lassen sich ausblenden.
|
||||
Fehlerkorrektur L/M/Q/H, einstellbarer Rand. Download als PNG und SVG.
|
||||
- **Logo in der Mitte:** das Logo der Installation oder ein eigenes je Code,
|
||||
10–25 % der Kantenlänge, mit erzwungener Fehlerkorrektur H.
|
||||
- **Meine Codes:** Liste oder Karten, Suche, Sortierung, Gruppenfilter,
|
||||
Ziel ändern, löschen. Statische Codes lassen sich auf Wunsch mit Namen
|
||||
ablegen, um sie wiederzufinden.
|
||||
- **Meine Codes:** Liste oder Karten, Suche, Sortierung, Filter nach Gruppe
|
||||
und nach Art, Ziel ändern, löschen. Statische Codes lassen sich auf Wunsch
|
||||
mit Namen ablegen; liegt derselbe Inhalt schon dort, wird gewarnt.
|
||||
- **Feedback auswerten:** Durchschnitt, Verteilung und Kommentare — anonym
|
||||
erhoben, ohne Name und IP-Adresse.
|
||||
- **Anmeldung abschaltbar.** Mit Anmeldung: Benutzer, Gruppen und die Rollen
|
||||
Administrator, Bearbeiter und Betrachter.
|
||||
- **Mehrere Domains** für Kurz-URLs, den Gruppen zugeteilt.
|
||||
- **Mehrere Domains** für Kurz-URLs, den Gruppen zugeteilt — mit Kurzhilfe
|
||||
zur Einrichtung und einer Prüfung, ob eine Domain wirklich hierher führt.
|
||||
- **Single Sign-on** über Authentik oder einen anderen OIDC-Anbieter.
|
||||
- **Typen abschaltbar:** Was ihr nicht braucht, erscheint gar nicht erst.
|
||||
- **Typen abschaltbar:** Was ihr nicht braucht, erscheint gar nicht erst —
|
||||
für die ganze Installation oder je Gruppe.
|
||||
- **Eigenes Erscheinungsbild:** Name, Logo, Favicon und Akzentfarbe im
|
||||
Admin-Bereich einstellbar.
|
||||
- **Hell und dunkel:** folgt dem Betriebssystem, mit Umschalter für eine
|
||||
@@ -609,7 +611,7 @@ Zwei Eigenschaften sind dabei zu unterscheiden:
|
||||
|
||||
| | Bedeutung | Woher |
|
||||
| --- | --- | --- |
|
||||
| **umgesetzt** | Der Typ ist ausprogrammiert. In Version 1 gilt das nur für „Website“. | Registry im Code |
|
||||
| **umgesetzt** | Der Typ ist ausprogrammiert. Seit 1.1.0 gilt das für alle fünfzehn. | Registry im Code |
|
||||
| **freigegeben** | Die Verwaltung zeigt den Typ an. | Datenbank, jederzeit änderbar |
|
||||
|
||||
Nur umgesetzt **und** freigegeben heißt benutzbar. Freigegeben, aber nicht
|
||||
@@ -673,7 +675,10 @@ Kurz-URL rückt dann unter den Namen. Zum Herunterladen dient dort die
|
||||
Kartenansicht.
|
||||
|
||||
Abgelegte **statische Codes** stehen mit in der Liste, tragen aber ein
|
||||
Abzeichen „statisch“ statt einer Kurz-URL und zählen keine Scans. Steht der
|
||||
Abzeichen „statisch“ statt einer Kurz-URL und zählen keine Scans. Beim
|
||||
Ablegen läuft eine **Dublettenprüfung**: Liegt in derselben Gruppe schon ein
|
||||
statischer Code mit identischem Inhalt, hält das Ablegen an und nennt ihn —
|
||||
eine Rückfrage mit dem Knopf „Trotzdem ablegen“, keine Sperre. Verglichen wird nur der Inhalt, nicht die Gestaltung. Steht der
|
||||
Filter auf „Nur statische“, fällt die Scan-Spalte weg und die Sortierung nach
|
||||
Scans mit ihr — bei einer Liste aus lauter Nullen sagt sie nichts.
|
||||
|
||||
@@ -1017,6 +1022,8 @@ deutschen Meldung.
|
||||
| `GET` | `/r/{slug}` | Weiterleitung, erhöht den Scan-Zähler |
|
||||
| `GET` | `/api/admin/types` | Zustand aller QR-Typen (`?group={id}` je Gruppe) |
|
||||
| `PATCH` | `/api/admin/types` | Typ freigeben oder abschalten (`groupId` optional) |
|
||||
| `POST` | `/api/admin/domains/{id}/check` | Domain prüfen: DNS, Zertifikat, Erreichbarkeit, Ziel |
|
||||
| `GET` | `/api/domain-check` | Beweis für die Domain-Prüfung (`?nonce=…`, ohne Anmeldung) |
|
||||
|
||||
Beispiel:
|
||||
|
||||
|
||||
@@ -112,12 +112,42 @@ gescannt zu werden.
|
||||
das Scannen erschweren, weil der Scanner den Code nicht mehr vom Hintergrund
|
||||
trennen kann.
|
||||
|
||||
### Ein Logo in die Mitte setzen
|
||||
|
||||

|
||||
|
||||
Ganz unten in der Gestaltung steht **Logo in der Mitte**. Ein Haken genügt,
|
||||
dann fragt die Anwendung, woher das Bild kommen soll:
|
||||
|
||||
- **Logo dieser Installation** — das, was unter Verwaltung → Erscheinungsbild
|
||||
hinterlegt ist. Kein Upload je Code nötig. Die Wahl erscheint nur, wenn dort
|
||||
überhaupt eines liegt.
|
||||
- **Eigenes Logo** — je Code hochgeladen, für Kundenprojekte mit fremdem
|
||||
Zeichen. PNG, JPEG, WebP oder AVIF, bis 2 MB.
|
||||
|
||||
Die **Größe** liegt zwischen 10 und 25 % der Kantenlänge; voreingestellt sind
|
||||
20 %, das verdeckt rund 4 % der Fläche. Hinter dem Bild liegt eine Fläche in
|
||||
der Hintergrundfarbe, damit es sich absetzt.
|
||||
|
||||
**Die Fehlerkorrektur springt dabei auf H** und lässt sich nicht mehr ändern —
|
||||
was das Bild verdeckt, muss sie ausgleichen. Das steht auch so unter der
|
||||
Auswahl; heimlich geschieht es nicht.
|
||||
|
||||
Ab 22 % erscheint eine Warnung. Die Fehlerkorrektur trägt das zwar, aber
|
||||
manche Scanner-Apps suchen in der Mitte nach Ausrichtungsmustern.
|
||||
|
||||
> **Einmal selbst scannen, bevor etwas in den Druck geht.** Ein Code mit Logo
|
||||
> ist die einzige Stelle, an der die Anwendung etwas ausliefern kann, das auf
|
||||
> dem einen Gerät geht und auf dem anderen nicht. Zwei verschiedene Apps
|
||||
> kosten eine halbe Minute und ersparen eine Neuauflage.
|
||||
|
||||
---
|
||||
|
||||
## Meine Codes
|
||||
|
||||
Hier stehen alle **dynamischen** Codes. Statische erscheinen nicht — sie
|
||||
werden nicht gespeichert.
|
||||
Hier stehen die **dynamischen** Codes und die statischen, die du abgelegt
|
||||
hast. Beide Arten stehen nebeneinander in derselben Liste — ein statischer
|
||||
trägt statt einer Kurz-URL das Abzeichen „statisch“ und zählt keine Scans.
|
||||
|
||||

|
||||
|
||||
@@ -128,6 +158,11 @@ Projekte nebeneinander.
|
||||
- Die **Sortierung** kennt neueste, älteste, Name und Scan-Anzahl.
|
||||
- Ein **Gruppenfilter** erscheint, sobald Codes in mehr als einer Gruppe
|
||||
liegen.
|
||||
- Ein **Filter nach Art** erscheint, sobald dynamische und statische Codes
|
||||
nebeneinander liegen: *Alle Arten*, *Nur dynamische*, *Nur statische*.
|
||||

|
||||
Steht er auf „Nur statische“, fällt die Scan-Spalte weg und die Sortierung
|
||||
nach Scans mit ihr — bei einer Liste aus lauter Strichen sagt sie nichts.
|
||||
- Das Symbol neben der Kurz-URL kopiert sie in die Zwischenablage.
|
||||
|
||||
Über die beiden Schalter rechts geht es zur **Kartenansicht** mit großer
|
||||
@@ -135,6 +170,23 @@ Vorschau. Die Wahl merkt sich der Browser.
|
||||
|
||||

|
||||
|
||||
### Statische Codes ablegen
|
||||
|
||||
Ein statischer Code ist mit dem Download eigentlich fertig — sein Inhalt
|
||||
steckt in ihm. Der Knopf **In „Meine Codes“ ablegen** legt ihn trotzdem in die
|
||||
Liste, damit du ihn wiederfindest und erneut herunterladen kannst.
|
||||
|
||||

|
||||
|
||||
Liegt dort schon ein statischer Code mit **identischem Inhalt**, hält das
|
||||
Ablegen an und zeigt, welcher das ist und wann er entstand. Der Knopf heißt
|
||||
dann **Trotzdem ablegen** — es ist eine Warnung, keine Sperre. Verglichen wird
|
||||
nur der Inhalt, nicht die Gestaltung: Derselbe Code in Schwarz-Weiß und in
|
||||
Hausfarben gilt hier als Dublette.
|
||||
|
||||
Bei **dynamischen** Codes fragt niemand nach. Zwei Kurz-Links auf dasselbe
|
||||
Ziel sind der Normalfall, sobald zwei Kampagnen getrennt gezählt werden.
|
||||
|
||||
### Ziel ändern
|
||||
|
||||

|
||||
@@ -247,6 +299,27 @@ sich Typen abschalten; sie verschwinden dann aus der Auswahl.
|
||||
unverändert weiter** — sie leiten weiter, zählen Scans und lassen sich
|
||||
bearbeiten. Nur neue lassen sich nicht mehr anlegen.
|
||||
|
||||
**Freigabe je Gruppe.** Über der Liste steht die Auswahl **Freigabe für**:
|
||||
|
||||

|
||||
|
||||
| Auswahl | Wirkung |
|
||||
| --- | --- |
|
||||
| Alle Gruppen (Grundeinstellung) | gilt für die ganze Installation |
|
||||
| Nur: *eine Gruppe* | weicht für diese eine Gruppe ab |
|
||||
|
||||
So bekommt die Marketingabteilung Social Media und die Werkstatt nur Website
|
||||
und PDF. Fehlt eine Abweichung, folgt die Gruppe der Grundeinstellung.
|
||||
|
||||
**Die Grundeinstellung gewinnt immer.** Ein Typ, der dort abgeschaltet ist,
|
||||
lässt sich in keiner Gruppe einschalten — der Schalter steht dann ausgegraut
|
||||
und nennt den Grund. Andersherum wäre „hier abgeschaltet“ keine verlässliche
|
||||
Aussage mehr.
|
||||
|
||||
Wer in mehreren Gruppen ist, sieht auf der Startseite die **Vereinigung**.
|
||||
Welche Freigabe zählt, entscheidet sich beim Anlegen anhand der gewählten
|
||||
Gruppe.
|
||||
|
||||
### Gruppen
|
||||
|
||||

|
||||
@@ -275,7 +348,34 @@ mehreren Gruppen dienen; die Codes bleiben trotzdem strikt nach Gruppe
|
||||
getrennt.
|
||||
|
||||
Damit das funktioniert, muss der Reverse Proxy jede dieser Domains auf diese
|
||||
Anwendung leiten — siehe README, Abschnitt „Mehrere Domains“.
|
||||
Anwendung leiten. Was dafür einzurichten ist, steht nicht nur im README: Der
|
||||
Knopf **Kurzhilfe** über der Liste führt es in sechs Schritten auf — DNS,
|
||||
Weiterleitung im Proxy samt der drei nötigen Kopfzeilen, Zertifikat,
|
||||
Registrieren, Gruppenzuteilung, Nachprüfen.
|
||||
|
||||

|
||||
|
||||
**Prüfen, ob eine Domain wirklich hierher führt.** Bei jedem Eintrag steht ein
|
||||
Knopf **Prüfen**. Er geht den Weg der Scanner nach und meldet vier Dinge:
|
||||
|
||||

|
||||
|
||||
| Schritt | Was er sagt |
|
||||
| --- | --- |
|
||||
| DNS | ob der Name auflöst, und auf welche Adressen |
|
||||
| Zertifikat | gültig bis wann, von wem ausgestellt |
|
||||
| Erreichbar | ob dort ein Webserver antwortet, und wie schnell |
|
||||
| Ziel | ob dahinter **diese** Installation steht |
|
||||
|
||||
Der letzte Schritt ist der eigentliche Wert. Dass irgendwo etwas antwortet,
|
||||
sagt wenig; die Prüfung lässt sich beweisen, dass es diese Anwendung ist.
|
||||
Scheitert schon DNS, fallen die drei Folgeschritte aus — dreimal dieselbe
|
||||
Meldung würde die eine wichtige verdecken.
|
||||
|
||||
> **Der Aufruf startet dort, wo der Container läuft.** Bei internem DNS oder
|
||||
> Hairpin-NAT kann er scheitern, obwohl von außen alles stimmt. Und
|
||||
> umgekehrt: Ein Erfolg von innen beweist noch nicht, dass die Domain von
|
||||
> außen erreichbar ist. Dafür bleibt der Blick von einem fremden Netz aus.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 72 KiB |
@@ -315,8 +315,9 @@ Die **Vorschau** wird serverseitig gerendert und aktualisiert sich bei jeder
|
||||
|
||||
## Meine Codes
|
||||
|
||||
Zeigt alle dynamischen Codes — statische erscheinen nicht, weil sie nicht
|
||||
gespeichert werden.
|
||||
Zeigt die dynamischen Codes und die statischen, die abgelegt wurden. Ein
|
||||
abgelegter statischer Code trägt statt einer Kurz-URL das Abzeichen
|
||||
„statisch“ und zählt keine Scans.
|
||||
|
||||
- **Zwei Ansichten:** Liste (Voreinstellung, eine Zeile je Code) und Karten
|
||||
mit großer Vorschau. Die Wahl merkt sich der Browser.
|
||||
@@ -324,6 +325,10 @@ gespeichert werden.
|
||||
- **Sortierung** nach neueste, älteste, Name oder Scan-Anzahl.
|
||||
- **Gruppenfilter**, sobald Codes in mehr als einer Gruppe liegen; dann gibt
|
||||
es auch eine Gruppenspalte.
|
||||
- **Filter nach Art** (*Alle Arten*, *Nur dynamische*, *Nur statische*),
|
||||
sobald beide Arten vorkommen. Bei „Nur statische“ verschwindet die
|
||||
Scan-Spalte und mit ihr die Sortierung danach: Statische zählen keine
|
||||
Scans, die Spalte wäre eine Reihe aus Strichen.
|
||||
- **Kurz-URL kopieren** mit einem Klick.
|
||||
- **Ziel ändern** — die Kurz-URL und damit der gedruckte Code bleiben gültig.
|
||||
- **Löschen** mit Rückfrage, die den betroffenen Code nennt.
|
||||
@@ -426,6 +431,53 @@ Domains werden in der Verwaltung angelegt und Gruppen zugeteilt.
|
||||
Damit das funktioniert, muss der Reverse Proxy jede dieser Domains auf diese
|
||||
Anwendung leiten.
|
||||
|
||||
### Kurzhilfe zur Einrichtung
|
||||
|
||||
Was außerhalb der Anwendung einzurichten ist, steht nicht nur im README: Auf
|
||||
der Domain-Seite öffnet der Knopf **Kurzhilfe** eine Erklärung in sechs
|
||||
Schritten — DNS-Eintrag, Weiterleitung im Reverse Proxy samt der drei nötigen
|
||||
Kopfzeilen zum Abtippen, Zertifikat, Registrieren, Gruppenzuteilung,
|
||||
Nachprüfen. Der Hostname aus `BASE_URL` wird als Beispiel eingesetzt.
|
||||
|
||||
Bewusst ein Dialog und kein Dauertext: Die Erklärung braucht man einmal beim
|
||||
Einrichten und danach nie wieder.
|
||||
|
||||
### Prüfen, ob eine Domain hierher führt
|
||||
|
||||
Bei jedem Eintrag steht ein Knopf **Prüfen**. Er geht den Weg der Scanner nach
|
||||
und meldet vier Dinge:
|
||||
|
||||
| Schritt | Was geprüft wird |
|
||||
| --- | --- |
|
||||
| DNS | Löst der Name auf? Auf welche Adressen? |
|
||||
| Zertifikat | Gültig, von wem ausgestellt, wie lange noch? |
|
||||
| Erreichbar | Antwortet dort ein Webserver, und wie schnell? |
|
||||
| Ziel | Steht dahinter **diese** Installation? |
|
||||
|
||||
Der letzte Schritt ist der eigentliche Wert. Dass irgendwo etwas antwortet,
|
||||
sagt wenig — es könnte die Parkseite des Registrars sein oder eine andere
|
||||
Installation dieser Anwendung. Deshalb ruft die Prüfung unter der Domain eine
|
||||
eigene Route auf und lässt sich einen HMAC über eine frische Zufallszahl
|
||||
zurückgeben. Das Geheimnis dafür entsteht beim ersten Check, liegt in der
|
||||
Datenbank und verlässt den Server nie.
|
||||
|
||||
Die Meldungen sind im Klartext: abgelaufen, selbst ausgestellt, gilt für einen
|
||||
anderen Namen, Zwischenzertifikat fehlt, Port 443 nimmt nichts an, „es
|
||||
antwortet etwas, aber nicht diese Anwendung“. Läuft ein Zertifikat in
|
||||
höchstens 14 Tagen ab, gilt der Schritt als Warnung statt als Erfolg.
|
||||
Scheitert schon DNS, fallen die drei Folgeschritte aus — dreimal dieselbe
|
||||
Meldung würde die eine wichtige verdecken.
|
||||
|
||||
**Die Grenze der Aussage** steht unter jedem Bericht: Der Aufruf startet dort,
|
||||
wo der Container läuft. Bei internem DNS oder Hairpin-NAT kann er scheitern,
|
||||
obwohl von außen alles stimmt; umgekehrt beweist ein Erfolg von innen nicht,
|
||||
dass die Domain von außen erreichbar ist.
|
||||
|
||||
Geprüft wird ausschließlich ein Hostname **aus der Datenbank**, nie einer aus
|
||||
der Anfrage — sonst wäre die Route ein Werkzeug, mit dem sich vom Server aus
|
||||
beliebige Adressen abklopfen ließen. Näheres in
|
||||
[sicherheit.md](sicherheit.md).
|
||||
|
||||
---
|
||||
|
||||
## Erscheinungsbild
|
||||
|
||||
@@ -15,8 +15,13 @@ Fußzeile.
|
||||
Kurz gefasst: Version 1.0.0 (13.08.2026) brachte das Grundgerüst mit dem Typ
|
||||
Website, 1.1.0 alle fünfzehn Typen samt Anmeldung, Gruppen und Domains,
|
||||
1.2.0 Single Sign-on und die Sicherheitsdurchsicht, 1.3.0 Erscheinungsbild
|
||||
und Dark Mode, 1.4.0 sprechende Kurz-Links, 1.5.0 benennbare statische Codes
|
||||
und 1.6.0 diesen Versionshinweis.
|
||||
und Dark Mode, 1.4.0 sprechende Kurz-Links, 1.5.0 benennbare statische Codes,
|
||||
1.6.0 den Versionshinweis und 1.7.0 sechs Erweiterungen auf einmal — Logo im
|
||||
Code, Freigabe je Gruppe, Domain-Check, Dublettenprüfung, Filter nach Art und
|
||||
die Kurzhilfe zu den Domains.
|
||||
|
||||
Damit sind zwei Punkte erledigt, die hier lange unter „Als Nächstes“ standen:
|
||||
**QR-Typen je Gruppe freigeben** und **Logo im QR-Code**.
|
||||
|
||||
---
|
||||
|
||||
@@ -25,27 +30,7 @@ und 1.6.0 diesen Versionshinweis.
|
||||
In dieser Reihenfolge. Die Einschätzungen sind grobe Hausnummern für eine
|
||||
Person, die den Code kennt.
|
||||
|
||||
### 1. QR-Typen je Gruppe freigeben · mittel
|
||||
|
||||
Heute gilt die Freigabe eines Typs für die ganze Installation: Was
|
||||
abgeschaltet ist, ist für alle weg. Bei mehreren Gruppen ist das zu grob — die
|
||||
Marketingabteilung braucht Social Media, die Werkstatt nur Website und PDF.
|
||||
|
||||
Geplant: Die Freigabe bekommt eine zweite Dimension. Es bleibt eine globale
|
||||
Einstellung als Grundlage, und je Gruppe lässt sich davon abweichen. Wer in
|
||||
mehreren Gruppen ist, sieht auf der Startseite die Vereinigung; beim Anlegen
|
||||
entscheidet die gewählte Gruppe.
|
||||
|
||||
Zu klären ist der Fall, in dem ein Typ global aus, in einer Gruppe aber an
|
||||
ist — entweder die Gruppe darf das überstimmen, oder die globale Abschaltung
|
||||
gewinnt immer. Die zweite Lesart ist die weniger überraschende und
|
||||
wahrscheinlich die richtige.
|
||||
|
||||
Berührt: `QrTypeSetting` (zusammengesetzter Schlüssel aus Typ und Gruppe),
|
||||
`src/lib/qr/availability.ts`, die Startseite und die Typ-Verwaltung, die dann
|
||||
eine Gruppenauswahl bekommt.
|
||||
|
||||
### 2. Statistik über die Zeit · mittel
|
||||
### 1. Statistik über die Zeit · mittel
|
||||
|
||||
Der Zähler weiß heute nur, wie oft und wann zuletzt. Gebraucht wird der
|
||||
Verlauf: Wie lief die Kampagne in den ersten Tagen, wann ist Ruhe
|
||||
@@ -59,7 +44,7 @@ sagt nichts über einzelne Menschen aus und braucht deshalb weder Einwilligung
|
||||
noch Löschfristen. Wer mehr will, muss sich mit beidem befassen — das ist eine
|
||||
andere Entscheidung als „ein Diagramm wäre schön“.
|
||||
|
||||
### 3. Papierkorb statt Löschen · klein
|
||||
### 2. Papierkorb statt Löschen · klein
|
||||
|
||||
Heute ist ein gedruckter Code nach einem Fehlklick unwiederbringlich tot,
|
||||
samt seiner Dateien und Rückmeldungen.
|
||||
@@ -69,20 +54,7 @@ Geplant: Gelöschte Codes 30 Tage aufheben und dann endgültig entfernen. In
|
||||
im Papierkorb liegt, bleibt sein Slug belegt — sonst könnte ihn ein neuer Code
|
||||
bekommen und die gedruckten Codes zeigten plötzlich woandershin.
|
||||
|
||||
### 4. Logo im QR-Code · mittel
|
||||
|
||||
Ein kleines Bild in die Mitte setzen, wie man es von Firmen-Codes kennt.
|
||||
|
||||
Geplant: Upload eines Logos je Code, mittig eingesetzt, mit erzwungener
|
||||
Fehlerkorrektur **H** und einer Warnung, sobald zu viel Fläche verdeckt wird.
|
||||
|
||||
Das ist der Punkt mit dem größten Risiko, etwas Kaputtes auszuliefern: Ein
|
||||
Code, der auf dem eigenen Handy noch funktioniert, kann bei anderen
|
||||
Scanner-Apps durchfallen. Gehört deshalb nicht ohne eine ehrliche Prüfung mit
|
||||
mehreren Geräten und Apps ausgeliefert — und mit einer klaren Obergrenze für
|
||||
die verdeckte Fläche.
|
||||
|
||||
### 5. Ablaufdatum und Zeitsteuerung · mittel
|
||||
### 3. Ablaufdatum und Zeitsteuerung · mittel
|
||||
|
||||
Ein Code, der ab einem Datum woandershin führt oder abläuft — für
|
||||
Veranstaltungen, Aktionen und Aushänge mit Verfallsdatum.
|
||||
@@ -99,6 +71,20 @@ Geplant, in zwei Stufen:
|
||||
Zu bedenken: Zeitzonen und die Frage, was beim Bearbeiten mit vergangenen
|
||||
Fenstern geschieht.
|
||||
|
||||
### 4. Gestaltung eines gespeicherten Codes ändern · klein
|
||||
|
||||
Aufgefallen beim Einbau des Logos: In „Meine Codes“ lässt sich die Gestaltung
|
||||
eines abgelegten Codes gar nicht mehr anfassen — weder Farben noch Größe noch
|
||||
Logo. Wer ein Logo nachrüsten will, kommt nur über die Schnittstelle daran.
|
||||
|
||||
Bei einem **dynamischen** Code ist das unnötig streng: Das gedruckte Bild
|
||||
bleibt zwar, wie es ist, aber der erneute Download soll ja gerade die neue
|
||||
Gestaltung tragen. Geplant: derselbe Gestaltungsbereich wie im Generator, im
|
||||
Bearbeiten-Dialog.
|
||||
|
||||
Bei einem **statischen** Code bleibt es beim Nein — er ist ein Schnappschuss,
|
||||
und eine geänderte Gestaltung ergäbe ein anderes Bild als das gedruckte.
|
||||
|
||||
---
|
||||
|
||||
## Später denkbar
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
* Voraussetzungen:
|
||||
* - eine **leere** Instanz (die Beispieldaten würden sonst mit fremden
|
||||
* Codes im Bild landen), erreichbar unter BASE
|
||||
* - `BASE_URL` der Instanz muss auf **dieselbe** Adresse zeigen wie BASE.
|
||||
* Sonst scheitert der Domain-Check im Bild 21, weil die Anwendung sich
|
||||
* selbst unter der eingetragenen Adresse nicht erreicht.
|
||||
* - AUTH_ENABLED=true, damit Kopfzeile und Verwaltung so aussehen wie im
|
||||
* Regelbetrieb
|
||||
* - Playwright: npm i -D playwright && npx playwright install chromium
|
||||
@@ -18,6 +21,7 @@ import { mkdir } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import { chromium } from "playwright";
|
||||
import sharp from "sharp";
|
||||
|
||||
const BASE = (process.argv[2] ?? "http://localhost:3000").replace(/\/+$/, "");
|
||||
const ZIEL = join(dirname(fileURLToPath(import.meta.url)), "bilder");
|
||||
@@ -197,16 +201,47 @@ const handy = await browser.newContext({
|
||||
locale: "de-DE",
|
||||
});
|
||||
|
||||
async function schuss(context, pfad, datei, { ganzeSeite = true, vorher } = {}) {
|
||||
/**
|
||||
* Nimmt eine Seite auf.
|
||||
*
|
||||
* `bereich` grenzt auf einen Ausschnitt ein — für Dialoge und einzelne Karten,
|
||||
* bei denen die ganze Seite mehr verdeckt als zeigt.
|
||||
*/
|
||||
async function schuss(
|
||||
context,
|
||||
pfad,
|
||||
datei,
|
||||
{ ganzeSeite = true, vorher, bereich } = {},
|
||||
) {
|
||||
const page = await context.newPage();
|
||||
await page.goto(BASE + pfad, { waitUntil: "networkidle" });
|
||||
if (vorher) await vorher(page);
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: join(ZIEL, datei), fullPage: ganzeSeite });
|
||||
const ziel = join(ZIEL, datei);
|
||||
if (bereich) {
|
||||
await page.locator(bereich).first().screenshot({ path: ziel });
|
||||
} else {
|
||||
await page.screenshot({ path: ziel, fullPage: ganzeSeite });
|
||||
}
|
||||
await page.close();
|
||||
console.log(" ✓", datei);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Beispiellogo für die Aufnahmen zum Logo im Code.
|
||||
*
|
||||
* Erzeugt statt mitgeliefert: So liegt kein Bild im Repo, das niemand pflegt,
|
||||
* und die Farbe passt zu den übrigen Aufnahmen.
|
||||
*/
|
||||
async function beispiellogo() {
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">
|
||||
<rect width="256" height="256" rx="56" fill="#312E81"/>
|
||||
<circle cx="128" cy="128" r="66" fill="none" stroke="#FFFFFF" stroke-width="18"/>
|
||||
<circle cx="128" cy="128" r="20" fill="#FFFFFF"/>
|
||||
</svg>`;
|
||||
return sharp(Buffer.from(svg)).png().toBuffer();
|
||||
}
|
||||
|
||||
console.log("Bilder werden erzeugt:");
|
||||
|
||||
await schuss(desktop, "/", "01-startseite.png");
|
||||
@@ -242,7 +277,7 @@ await schuss(desktop, "/meine-codes", "06-ziel-bearbeiten.png", {
|
||||
await page.waitForTimeout(400);
|
||||
// Bewusst die letzte Zeile: ein Website-Code, dessen Dialog ganz ins Bild passt.
|
||||
await page.locator("tbody tr").last().getByRole("button", { name: /bearbeiten/ }).click();
|
||||
await page.getByRole("heading", { name: "Ziel bearbeiten" }).waitFor();
|
||||
await page.getByRole("heading", { name: "Code bearbeiten" }).waitFor();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -258,8 +293,129 @@ await schuss(desktop, "/verwaltung/gruppen", "13-verwaltung-gruppen.png");
|
||||
await schuss(desktop, "/verwaltung/domains", "14-verwaltung-domains.png");
|
||||
await schuss(desktop, "/konto", "15-konto.png");
|
||||
|
||||
/* ------------------------------------------- Seit 1.7.0: die neuen Funktionen */
|
||||
|
||||
const logo = await beispiellogo();
|
||||
|
||||
// Logo in der Mitte: ganze Seite, damit Auswahl und Vorschau zusammen im Bild
|
||||
// sind — das eine erklärt das andere.
|
||||
await schuss(desktop, "/erstellen/website", "16-logo-im-code.png", {
|
||||
vorher: async (page) => {
|
||||
await page.locator("#website-url").fill("https://example.com/nordwind");
|
||||
await page.getByRole("checkbox", { name: /Logo in die Mitte/ }).check();
|
||||
await page.setInputFiles('input[type="file"]', {
|
||||
name: "logo.png",
|
||||
mimeType: "image/png",
|
||||
buffer: logo,
|
||||
});
|
||||
await page.waitForTimeout(2500);
|
||||
},
|
||||
});
|
||||
|
||||
// Für die Dublettenwarnung braucht es einen bereits abgelegten statischen Code.
|
||||
const statisch = await api("/api/codes", {
|
||||
method: "POST",
|
||||
json: {
|
||||
type: "website",
|
||||
data: { url: "https://example.com/aushang" },
|
||||
design: DESIGN,
|
||||
mode: "static",
|
||||
label: "Aushang Eingang",
|
||||
},
|
||||
});
|
||||
if (statisch.status === 201) angelegt.push(statisch.body.code);
|
||||
|
||||
await schuss(desktop, "/erstellen/website", "17-dublettenwarnung.png", {
|
||||
bereich: "section:has-text('Art des Codes')",
|
||||
vorher: async (page) => {
|
||||
await page.locator("#website-url").fill("https://example.com/aushang");
|
||||
await page.locator('button[data-mode="static"]').click();
|
||||
await page.waitForTimeout(900);
|
||||
await page.getByRole("button", { name: /ablegen/i }).click();
|
||||
await page.getByText("Diesen Inhalt hast du schon abgelegt").waitFor({
|
||||
timeout: 15000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Filter nach Art — sichtbar, weil jetzt beide Arten in der Liste stehen.
|
||||
await schuss(desktop, "/meine-codes", "18-meine-codes-filter.png", {
|
||||
vorher: async (page) => {
|
||||
await page.getByLabel("Nach Art filtern").selectOption("static");
|
||||
await page.waitForTimeout(600);
|
||||
},
|
||||
});
|
||||
|
||||
// Freigabe je Gruppe: eine zweite Gruppe, damit die Auswahl etwas zu wählen hat.
|
||||
const gruppe = await api("/api/admin/groups", {
|
||||
method: "POST",
|
||||
json: { name: "Marketing" },
|
||||
});
|
||||
const gruppeId = gruppe.body?.group?.id ?? null;
|
||||
|
||||
if (gruppeId) {
|
||||
// Ein Typ in dieser Gruppe abgeschaltet, damit das Bild den Fall zeigt,
|
||||
// um den es geht.
|
||||
await api("/api/admin/types", {
|
||||
method: "PATCH",
|
||||
json: { type: "payment", enabled: false, groupId: gruppeId },
|
||||
});
|
||||
}
|
||||
|
||||
await schuss(desktop, "/verwaltung/typen", "19-verwaltung-typen-gruppe.png", {
|
||||
vorher: async (page) => {
|
||||
if (gruppeId) {
|
||||
await page.getByLabel("Freigabe für").selectOption(gruppeId);
|
||||
await page.waitForTimeout(900);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Kurzhilfe: nur der Dialog, die Seite dahinter trägt nichts bei.
|
||||
await schuss(desktop, "/verwaltung/domains", "20-domain-kurzhilfe.png", {
|
||||
bereich: "dialog[open]",
|
||||
vorher: async (page) => {
|
||||
await page.getByRole("button", { name: "Kurzhilfe" }).click();
|
||||
await page.locator("dialog[open]").waitFor();
|
||||
await page.waitForTimeout(400);
|
||||
},
|
||||
});
|
||||
|
||||
// Domain-Check: der Bericht zur eigenen Adresse. Weil BASE_URL und der Port
|
||||
// des Containers hier zusammenpassen, geht die Prüfung durch — genau der
|
||||
// Fall, den die Anleitung zeigen soll.
|
||||
await schuss(desktop, "/verwaltung/domains", "21-domain-check.png", {
|
||||
vorher: async (page) => {
|
||||
await page.getByRole("button", { name: "Prüfen" }).first().click();
|
||||
await page
|
||||
.locator("text=/Die Domain führt hierher|So kommt niemand hier an/")
|
||||
.first()
|
||||
.waitFor({ timeout: 30000 });
|
||||
await page.waitForTimeout(400);
|
||||
},
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
/* --- Was über die Oberfläche entstand, kennt `angelegt` noch nicht --------- */
|
||||
|
||||
const bestand = await api("/api/codes");
|
||||
for (const eintrag of bestand.body?.codes ?? []) {
|
||||
if (!angelegt.some((vorhanden) => vorhanden.id === eintrag.id)) {
|
||||
angelegt.push(eintrag);
|
||||
}
|
||||
}
|
||||
|
||||
// Die Abweichung und die Gruppe wieder zurücknehmen — das Skript soll eine
|
||||
// Instanz so hinterlassen, wie es sie vorgefunden hat.
|
||||
if (gruppeId) {
|
||||
await api("/api/admin/types", {
|
||||
method: "PATCH",
|
||||
json: { type: "payment", enabled: true, groupId: gruppeId },
|
||||
});
|
||||
await api(`/api/admin/groups/${gruppeId}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ Aufräumen */
|
||||
|
||||
for (const eintrag of angelegt) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "qrcode-generator",
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -98,13 +98,21 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`}</HelpCode>
|
||||
</HelpStep>
|
||||
|
||||
<HelpStep n={6} title="Nachprüfen">
|
||||
<p>Von außerhalb des eigenen Netzes:</p>
|
||||
<p>
|
||||
Dafür gibt es den Knopf <strong>Prüfen</strong> bei jedem Eintrag
|
||||
in der Liste. Er meldet DNS, Zertifikat, Erreichbarkeit — und ob
|
||||
hinter der Domain wirklich diese Installation steht.
|
||||
</p>
|
||||
<p>
|
||||
Der Aufruf startet dort, wo dieser Container läuft. Bei internem
|
||||
DNS oder Hairpin-NAT kann er scheitern, obwohl von außen alles
|
||||
stimmt. Von einem fremden Netz aus hilft dann:
|
||||
</p>
|
||||
<HelpCode>{`curl -I https://${beispiel}/`}</HelpCode>
|
||||
<p>
|
||||
Kommt eine Antwort dieser Anwendung, stimmen DNS, Proxy und
|
||||
Zertifikat. Danach einen Code der zugeteilten Gruppe anlegen, die
|
||||
Domain auswählen und den fertigen Code einmal mit dem Handy
|
||||
scannen — das ist die einzige Prüfung, die wirklich zählt.
|
||||
Zum Schluss einen Code der zugeteilten Gruppe anlegen, die Domain
|
||||
auswählen und den fertigen Code einmal mit dem Handy scannen — das
|
||||
ist die einzige Prüfung, die wirklich zählt.
|
||||
</p>
|
||||
</HelpStep>
|
||||
</ol>
|
||||
|
||||
@@ -8,10 +8,10 @@ import pkg from "../../package.json";
|
||||
* unweigerlich auseinander; eine Prüfung wacht darüber, dass die erzeugte
|
||||
* Datei zu dieser Quelle passt.
|
||||
*
|
||||
* Die Einträge sind aus dem Abschnitt „Gebaut“ der Roadmap und der
|
||||
* Git-Historie zusammengetragen. Versionsnummern gab es dabei nie — es
|
||||
* existieren keine Tags —, deshalb ist je **Erscheinungstag** eine Version
|
||||
* gebildet. Das ist nachträglich vergeben, aber die Daten selbst stimmen.
|
||||
* Die Einträge bis 1.5.0 sind aus dem Abschnitt „Gebaut“ der Roadmap und der
|
||||
* Git-Historie zusammengetragen: Versionsnummern gab es damals nicht, deshalb
|
||||
* ist je **Erscheinungstag** eine gebildet. Nachträglich vergeben, aber die
|
||||
* Daten selbst stimmen. Ab 1.6.0 trägt jede Fassung einen Git-Tag.
|
||||
*/
|
||||
export type ChangelogEntry = {
|
||||
version: string;
|
||||
@@ -23,6 +23,29 @@ export type ChangelogEntry = {
|
||||
};
|
||||
|
||||
export const CHANGELOG: readonly ChangelogEntry[] = [
|
||||
{
|
||||
version: "1.7.1",
|
||||
date: "2026-08-21",
|
||||
summary: "Bilder zu den neuen Funktionen in der Bedienungsanleitung.",
|
||||
changes: [
|
||||
"Die Bedienungsanleitung zeigt die Funktionen aus 1.7.0 jetzt auch im Bild: Logo im Code, Dublettenwarnung, Filter nach Art, Freigabe je Gruppe, Kurzhilfe und Domain-Bericht.",
|
||||
"Alle übrigen Bilder mit erneuert — sie stammten noch aus 1.2.0.",
|
||||
"Das Skript, das die Bilder erzeugt, lief seit einer Umbenennung ins Leere und tut es wieder.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.7.0",
|
||||
date: "2026-08-21",
|
||||
summary: "Logo im Code, Freigabe je Gruppe und ein Domain-Check.",
|
||||
changes: [
|
||||
"Ein Logo in der Mitte des Codes — das der Installation oder ein eigenes je Code, 10 bis 25 % der Kantenlänge. Die Fehlerkorrektur springt dabei auf H.",
|
||||
"QR-Typen lassen sich je Gruppe freigeben. Die Grundeinstellung bleibt die Grundlage; eine globale Abschaltung gewinnt immer.",
|
||||
"Der Knopf „Prüfen“ bei jeder Domain meldet DNS, Zertifikat, Erreichbarkeit — und ob dahinter wirklich diese Installation steht.",
|
||||
"Eine Kurzhilfe auf der Domain-Seite führt in sechs Schritten auf, was für eine neue Domain einzurichten ist.",
|
||||
"„Meine Codes“ lässt sich nach Art filtern: alle, nur dynamische, nur statische.",
|
||||
"Beim Ablegen eines statischen Codes wird gewarnt, wenn derselbe Inhalt schon in der Gruppe liegt.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.6.0",
|
||||
date: "2026-08-21",
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
* Zwei voneinander unabhängige Eigenschaften bestimmen, was ein Typ kann:
|
||||
*
|
||||
* - `implemented` — steht in der Registry und sagt, ob der Typ überhaupt
|
||||
* ausprogrammiert ist. In V1 gilt das nur für „Website“.
|
||||
* ausprogrammiert ist. Seit 1.1.0 gilt das für alle fünfzehn; die
|
||||
* Unterscheidung bleibt für Typen, die später dazukommen.
|
||||
* - `enabled` — kommt aus der Datenbank und sagt, ob die Verwaltung den Typ
|
||||
* freigegeben hat. Fehlt der Eintrag, ist der Typ freigegeben.
|
||||
*
|
||||
|
||||