Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e2d222e86 | ||
|
|
fcc63ff69a |
@@ -7,6 +7,14 @@ Versionsnummer in der Fußzeile.
|
|||||||
> Diese Datei wird erzeugt: `npm run changelog`.
|
> Diese Datei wird erzeugt: `npm run changelog`.
|
||||||
> Gepflegt wird der Verlauf in `src/lib/changelog.ts`.
|
> 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
|
## 1.7.0 — 21.08.2026
|
||||||
|
|
||||||
Logo im Code, Freigabe je Gruppe und ein Domain-Check.
|
Logo im Code, Freigabe je Gruppe und ein Domain-Check.
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ trennen kann.
|
|||||||
|
|
||||||
### Ein Logo in die Mitte setzen
|
### Ein Logo in die Mitte setzen
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Ganz unten in der Gestaltung steht **Logo in der Mitte**. Ein Haken genügt,
|
Ganz unten in der Gestaltung steht **Logo in der Mitte**. Ein Haken genügt,
|
||||||
dann fragt die Anwendung, woher das Bild kommen soll:
|
dann fragt die Anwendung, woher das Bild kommen soll:
|
||||||
|
|
||||||
@@ -158,6 +160,7 @@ Projekte nebeneinander.
|
|||||||
liegen.
|
liegen.
|
||||||
- Ein **Filter nach Art** erscheint, sobald dynamische und statische Codes
|
- Ein **Filter nach Art** erscheint, sobald dynamische und statische Codes
|
||||||
nebeneinander liegen: *Alle Arten*, *Nur dynamische*, *Nur statische*.
|
nebeneinander liegen: *Alle Arten*, *Nur dynamische*, *Nur statische*.
|
||||||
|

|
||||||
Steht er auf „Nur statische“, fällt die Scan-Spalte weg und die Sortierung
|
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.
|
nach Scans mit ihr — bei einer Liste aus lauter Strichen sagt sie nichts.
|
||||||
- Das Symbol neben der Kurz-URL kopiert sie in die Zwischenablage.
|
- Das Symbol neben der Kurz-URL kopiert sie in die Zwischenablage.
|
||||||
@@ -173,6 +176,8 @@ 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
|
steckt in ihm. Der Knopf **In „Meine Codes“ ablegen** legt ihn trotzdem in die
|
||||||
Liste, damit du ihn wiederfindest und erneut herunterladen kannst.
|
Liste, damit du ihn wiederfindest und erneut herunterladen kannst.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Liegt dort schon ein statischer Code mit **identischem Inhalt**, hält das
|
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
|
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
|
dann **Trotzdem ablegen** — es ist eine Warnung, keine Sperre. Verglichen wird
|
||||||
@@ -296,6 +301,8 @@ bearbeiten. Nur neue lassen sich nicht mehr anlegen.
|
|||||||
|
|
||||||
**Freigabe je Gruppe.** Über der Liste steht die Auswahl **Freigabe für**:
|
**Freigabe je Gruppe.** Über der Liste steht die Auswahl **Freigabe für**:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
| Auswahl | Wirkung |
|
| Auswahl | Wirkung |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Alle Gruppen (Grundeinstellung) | gilt für die ganze Installation |
|
| Alle Gruppen (Grundeinstellung) | gilt für die ganze Installation |
|
||||||
@@ -346,9 +353,13 @@ Knopf **Kurzhilfe** über der Liste führt es in sechs Schritten auf — DNS,
|
|||||||
Weiterleitung im Proxy samt der drei nötigen Kopfzeilen, Zertifikat,
|
Weiterleitung im Proxy samt der drei nötigen Kopfzeilen, Zertifikat,
|
||||||
Registrieren, Gruppenzuteilung, Nachprüfen.
|
Registrieren, Gruppenzuteilung, Nachprüfen.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
| Schritt | Was er sagt |
|
| Schritt | Was er sagt |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| DNS | ob der Name auflöst, und auf welche Adressen |
|
| DNS | ob der Name auflöst, und auf welche Adressen |
|
||||||
|
|||||||
|
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 |
@@ -7,6 +7,9 @@
|
|||||||
* Voraussetzungen:
|
* Voraussetzungen:
|
||||||
* - eine **leere** Instanz (die Beispieldaten würden sonst mit fremden
|
* - eine **leere** Instanz (die Beispieldaten würden sonst mit fremden
|
||||||
* Codes im Bild landen), erreichbar unter BASE
|
* 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
|
* - AUTH_ENABLED=true, damit Kopfzeile und Verwaltung so aussehen wie im
|
||||||
* Regelbetrieb
|
* Regelbetrieb
|
||||||
* - Playwright: npm i -D playwright && npx playwright install chromium
|
* - 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 { fileURLToPath } from "node:url";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { chromium } from "playwright";
|
import { chromium } from "playwright";
|
||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
const BASE = (process.argv[2] ?? "http://localhost:3000").replace(/\/+$/, "");
|
const BASE = (process.argv[2] ?? "http://localhost:3000").replace(/\/+$/, "");
|
||||||
const ZIEL = join(dirname(fileURLToPath(import.meta.url)), "bilder");
|
const ZIEL = join(dirname(fileURLToPath(import.meta.url)), "bilder");
|
||||||
@@ -197,16 +201,47 @@ const handy = await browser.newContext({
|
|||||||
locale: "de-DE",
|
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();
|
const page = await context.newPage();
|
||||||
await page.goto(BASE + pfad, { waitUntil: "networkidle" });
|
await page.goto(BASE + pfad, { waitUntil: "networkidle" });
|
||||||
if (vorher) await vorher(page);
|
if (vorher) await vorher(page);
|
||||||
await page.waitForTimeout(300);
|
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();
|
await page.close();
|
||||||
console.log(" ✓", datei);
|
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:");
|
console.log("Bilder werden erzeugt:");
|
||||||
|
|
||||||
await schuss(desktop, "/", "01-startseite.png");
|
await schuss(desktop, "/", "01-startseite.png");
|
||||||
@@ -242,7 +277,7 @@ await schuss(desktop, "/meine-codes", "06-ziel-bearbeiten.png", {
|
|||||||
await page.waitForTimeout(400);
|
await page.waitForTimeout(400);
|
||||||
// Bewusst die letzte Zeile: ein Website-Code, dessen Dialog ganz ins Bild passt.
|
// 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.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, "/verwaltung/domains", "14-verwaltung-domains.png");
|
||||||
await schuss(desktop, "/konto", "15-konto.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();
|
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 */
|
/* ------------------------------------------------------------ Aufräumen */
|
||||||
|
|
||||||
for (const eintrag of angelegt) {
|
for (const eintrag of angelegt) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "qrcode-generator",
|
"name": "qrcode-generator",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ export type ChangelogEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: readonly 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",
|
version: "1.7.0",
|
||||||
date: "2026-08-21",
|
date: "2026-08-21",
|
||||||
|
|||||||