Version 1.7.1: Bilder zu den neuen Funktionen
Die Bedienungsanleitung hatte zu jedem alten Abschnitt ein Bild und zu keinem der sechs neuen. Sechs Aufnahmen kommen dazu: Logo im Code, Dublettenwarnung, Filter nach Art, Freigabe je Gruppe, Kurzhilfe und der Bericht einer geprueften Domain. Das Skript dafuer lief seit einer Umbenennung ins Leere — es wartete auf einen Dialogtitel "Ziel bearbeiten", der inzwischen "Code bearbeiten" heisst. Damit sind auch die uebrigen sechzehn Bilder neu; sie stammten noch aus 1.2.0 und zeigten eine aeltere Oberflaeche. schuss() kann jetzt einen Ausschnitt aufnehmen statt der ganzen Seite — fuer Dialoge und einzelne Karten, bei denen die volle Seite mehr verdeckt als zeigt. Das Beispiellogo entsteht im Skript, damit kein ungepflegtes Bild im Repo liegt. In der Kopfdoku steht jetzt, dass BASE_URL der Instanz auf dieselbe Adresse zeigen muss wie BASE — sonst erreicht die Anwendung sich beim Domain-Check nicht selbst und das Bild zeigt einen Fehlschlag.
@@ -7,6 +7,14 @@ 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.
|
||||
|
||||
@@ -114,6 +114,8 @@ 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:
|
||||
|
||||
@@ -158,6 +160,7 @@ Projekte nebeneinander.
|
||||
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.
|
||||
@@ -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
|
||||
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
|
||||
@@ -296,6 +301,8 @@ 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 |
|
||||
@@ -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,
|
||||
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 |
|
||||
|
||||
|
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:
|
||||
* - 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.7.0",
|
||||
"version": "1.7.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -23,6 +23,16 @@ 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",
|
||||
|
||||