Viel neues
This commit is contained in:
460
qa-tool/README.MD
Normal file
460
qa-tool/README.MD
Normal file
@@ -0,0 +1,460 @@
|
||||
# o‑Byte QA – Testprotokoll‑Tool (Full README)
|
||||
|
||||
**Stand:** 2025-09-04
|
||||
**Stack:** PHP 8.3 ( Apache ), JS (Vanilla), MariaDB, Docker & Compose, Portainer
|
||||
|
||||
Dieses Repository enthält ein vollwertiges QA‑Tool mit Login (OIDC), GitLab‑Template‑Loader, DocBee‑Posting, PDF‑Erzeugung im Browser und persistenter Speicherung (DB + PDF‑Ablage). Diese README basiert ausschließlich auf dem mitgelieferten Projektstand.
|
||||
|
||||
---
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
|
||||
1. [Überblick](#überblick)
|
||||
2. [Architektur & Verzeichnisstruktur](#architektur--verzeichnisstruktur)
|
||||
3. [Funktionen](#funktionen)
|
||||
4. [UI‑Bedienung (Kurzüberblick)](#ui-bedienung-kurzüberblick)
|
||||
5. [Datenmodell (DB)](#datenmodell-db)
|
||||
6. [API‑Endpunkte](#api-endpunkte)
|
||||
7. [Konfiguration per Umgebungsvariablen (.env)](#konfiguration-per-umgebungsvariablen-env)
|
||||
8. [Lokale Entwicklung](#lokale-entwicklung)
|
||||
9. [Deployment mit Docker Compose](#deployment-mit-docker-compose)
|
||||
10. [Deployment mit Portainer (Stack)](#deployment-mit-portainer-stack)
|
||||
11. [GitLab‑Vorlagen](#gitlab-vorlagen)
|
||||
12. [DocBee‑Integration](#docbee-integration)
|
||||
13. [PDF‑Export](#pdf-export)
|
||||
14. [Troubleshooting](#troubleshooting)
|
||||
15. [Sicherheitshinweise](#sicherheitshinweise)
|
||||
16. [Lizenz / Nutzung](#lizenz--nutzung)
|
||||
|
||||
---
|
||||
|
||||
## Überblick
|
||||
|
||||
- **Ziel:** Manuelle QA‑Testläufe effizient vorbereiten, durchführen und revisionssicher exportieren.
|
||||
- **Workflow:** Vorlage laden → Metadaten eintragen (inkl. **OLM‑Nummer**) → Schritte bearbeiten (Gruppen, Status, Evidenzen) → **Exportieren (DocBee, DB, PDF)**.
|
||||
- **Login:** OpenID Connect (konfigurierbar), optional **bypassbar** über `AUTH_DISABLED=true` für lokale Tests.
|
||||
- **Persistenz:** Reports (Metadaten & Steps) in MariaDB, PDFs serverseitig abgelegt.
|
||||
- **Integrationen:** GitLab (YAML‑Vorlagen), DocBee (Login + Notiz‑Erzeugung via REST).
|
||||
|
||||
---
|
||||
|
||||
## Architektur & Verzeichnisstruktur
|
||||
|
||||
```
|
||||
o-byte-qa-tool/
|
||||
├─ compose/
|
||||
│ ├─ Dockerfile # PHP 8.3 + Apache, Composer
|
||||
│ └─ docker-compose.yml # app, mariadb, schema-loader, phpmyadmin, fix-pdf-perms
|
||||
└─ htdocs/ # Webroot
|
||||
├─ index.php # App-Entry, Token-Bootstrap für DocBee, UI
|
||||
├─ app.js # UI-Logik, Exporte, DocBee-Push, GitLab-Loader
|
||||
├─ style.css # Styles
|
||||
├─ api/
|
||||
│ ├─ export.php # Report+Steps in DB speichern, PDF entgegennehmen
|
||||
│ ├─ echo.php # einfache Echo-Route (Debug)
|
||||
│ └─ health.php # Health‑Check (DB/PDF‑Pfad)
|
||||
├─ config/config.php # ENV‑Konfiguration (OIDC, DocBee, DB, APP_BASE_URL, …)
|
||||
├─ login.php # OIDC‑Start
|
||||
├─ callback.php # OIDC‑Callback, Session‑Bootstrap
|
||||
├─ logout.php # Session reset (OLM beibehalten)
|
||||
├─ logo.png / logo_light.png
|
||||
├─ favicon.ico
|
||||
├─ vendor/ # Composer (z. B. jumbojett/openid-connect-php, phpseclib, …)
|
||||
└─ oidc/ # phpseclib Assets (lokal eingebunden)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Funktionen
|
||||
|
||||
### Vorlagen & Schritte
|
||||
- **Vorlagen laden**: YAML/JSON (lokal per Datei **oder** aus **GitLab**‑Repo/Pfad/Ref).
|
||||
- **Gruppen**: Gruppenzeilen, ein-/ausklappbar; Schritte via Drag‑Handle verschiebbar (innerhalb und zwischen Gruppen).
|
||||
- **Automatische Neu‑Nummerierung** der Steps bei Änderungen/Verschieben.
|
||||
- **Pflichtschritte** (📌) werden geprüft – Export blockiert, wenn offen.
|
||||
- **Gruppenstatus setzen** auf *pass/fail/skip/blocked*, inkl. **Warnung** bei Überschreiben bestehender Stati.
|
||||
- **Kommentare & Evidenzen** (Freitext + URL).
|
||||
|
||||
### Metadaten
|
||||
- **Module**, **Modul‑Version**, **PBX‑Version**, **Tester** (aus Session), **DocBee‑Ticket‑URL**, **OLM‑Nummer** (`olm_nummer`).
|
||||
|
||||
### Exporte
|
||||
- **Exportieren (DocBee, DB, PDF)** per **einem** Button:
|
||||
1. DocBee‑Notiz erstellen (falls Token vorhanden) → Ticket‑URL ggf. aktualisiert.
|
||||
2. **PDF** im Browser generieren (jsPDF+AutoTable).
|
||||
3. **/api/export.php**: JSON (Run) + PDF (multipart) an Server → DB‑Speicherung + PDF‑Ablage.
|
||||
4. Server sendet `report_id` und `pdf_path` zurück.
|
||||
- **Lauf speichern / laden** als JSON (Zwischenspeichern/Weiterarbeiten).
|
||||
- **Template als YAML exportieren** (inkl. `olm_nummer`, Gruppen & Steps).
|
||||
|
||||
> Hinweis: Dedizierte Buttons für CSV/MD/PDF wurden in dieser Version **konsolidiert** – CSV/MD‑Funktionen existieren noch im Code, UI‑seitig aber nicht exponiert.
|
||||
|
||||
---
|
||||
|
||||
## UI‑Bedienung (Kurzüberblick)
|
||||
|
||||
- **GitLab Vorlage**: Dropdown befüllen (`GitLab: …` Tag zeigt Status), Datei wählen → Vorlage wird geladen.
|
||||
- **Metadaten**: Felder ausfüllen (**OLM‑Nummer** nicht vergessen).
|
||||
- **Steps**: `+ Step` / `+ Gruppe`, per Griff `⋮⋮` ziehen, Status setzen, Kommentare/Evidenzen pflegen.
|
||||
- **Exportieren (DocBee, DB, PDF)**: Startet kompletten Lauf‑Export. Ergebnisdialog zeigt Ticket/PDF‑Pfad/Report‑ID.
|
||||
|
||||
---
|
||||
|
||||
## Datenmodell (DB)
|
||||
|
||||
Beim Export werden folgende Tabellen (idempotent) angelegt/aktualisiert:
|
||||
|
||||
**reports**
|
||||
- `id` BIGINT (PK)
|
||||
- `created_at` TIMESTAMP (Default `CURRENT_TIMESTAMP`)
|
||||
- `module` VARCHAR(255)
|
||||
- `module_version` VARCHAR(100)
|
||||
- `pbx_version` VARCHAR(100)
|
||||
- `olm_nummer` VARCHAR(100)
|
||||
- `tester` VARCHAR(255)
|
||||
- `docbee_url` TEXT
|
||||
- `summary` VARCHAR(255) → z. B. `12/15 pass, 2 fail, 1 skip`
|
||||
- `pdf_path` TEXT
|
||||
|
||||
**steps**
|
||||
- `id` BIGINT (PK)
|
||||
- `report_id` BIGINT (FK → reports.id, ON DELETE CASCADE)
|
||||
- `step_index` INT
|
||||
- `step_id` VARCHAR(50)
|
||||
- `title` TEXT
|
||||
- `expected` TEXT
|
||||
- `status` ENUM('pass','fail','skip','na','')
|
||||
- `comment` TEXT
|
||||
- `evidence` TEXT
|
||||
- `group_title` VARCHAR(255)
|
||||
- `group_index` INT
|
||||
|
||||
Der **schema‑loader** Service in `docker-compose.yml` erstellt die Tabellen automatisch beim ersten Start.
|
||||
|
||||
---
|
||||
|
||||
## API‑Endpunkte
|
||||
|
||||
- `GET /api/health.php`
|
||||
- Prüft PDO / MariaDB‑Erreichbarkeit, Schreibrechte des PDF‑Ordners.
|
||||
- `POST /api/export.php` (multipart/form-data)
|
||||
- **Felder:**
|
||||
- `run` – JSON mit Metadaten (`module`, `module_version`, `pbx_version`, `olm_nummer`, `tester`, `docbee_url`, `ts`, `steps`[…])
|
||||
- `pdf` – erzeugte PDF‑Datei (optional; Export läuft auch ohne PDF durch)
|
||||
- **Antwort:** `{{"ok":true,"report_id":<int>,"pdf_path":"…","summary":"…"}}`
|
||||
- `POST /api/echo.php`
|
||||
- Debug/Echo.
|
||||
|
||||
---
|
||||
|
||||
## Konfiguration per Umgebungsvariablen (.env)
|
||||
|
||||
Beispiel **.env** (für Compose/Portainer):
|
||||
|
||||
```env
|
||||
# App
|
||||
APP_PORT=8009
|
||||
APP_BASE_URL=http://localhost:8009
|
||||
AUTH_DISABLED=false # true = Login-BYPASS (nur lokal verwenden)
|
||||
|
||||
# OpenID Connect (Login)
|
||||
OIDC_PROVIDER=https://auth.o-byte.com/realms/o-byte.com
|
||||
OIDC_CLIENT=qa-tool
|
||||
OIDC_SECRET=<client-secret>
|
||||
|
||||
# MariaDB
|
||||
DB_HOST=mariadb
|
||||
DB_PORT=3306
|
||||
DB_NAME=qa_tool
|
||||
DB_USER=qa
|
||||
DB_PASS=change_me
|
||||
DB_ROOT_PASS=change_me_root
|
||||
|
||||
# Persistenz
|
||||
PDF_STORAGE_DIR=/var/reports
|
||||
|
||||
# DocBee
|
||||
DOCBEE_BASEURL=https://obyte.docbee.com
|
||||
DOCBEE_USER=OBYTE/service
|
||||
DOCBEE_PASS=<password>
|
||||
DOCBEE_TIME=1440 # Minuten Token-Gültigkeit
|
||||
|
||||
# GitLab Templates
|
||||
GITLAB_HOST=https://git.steinert.cc
|
||||
GITLAB_PROJECT_ID=qa/templates # oder numerische ID
|
||||
GITLAB_REF=main
|
||||
GITLAB_PATH=templates
|
||||
GITLAB_TOKEN= # optional (PRIVATE-TOKEN)
|
||||
|
||||
# Container-User (Dateirechte)
|
||||
APP_UID=0
|
||||
APP_GID=0
|
||||
|
||||
# phpMyAdmin Port (optional)
|
||||
PHPMYADMIN_PORT=8010
|
||||
```
|
||||
|
||||
> **Wichtig:** Keine Secrets in Repos commiten. Für Produktion Token/Passwörter über Portainer‑UI/Secrets setzen.
|
||||
|
||||
---
|
||||
|
||||
## Lokale Entwicklung
|
||||
|
||||
1. **Composer‑Abhängigkeiten** sind bereits im Repo (`vendor/`). Falls nötig:
|
||||
```bash
|
||||
cd htdocs && composer install
|
||||
```
|
||||
2. **PHP‑Built‑in** (nur für schnellen Test, ohne DB/PDF‑Ablage):
|
||||
```bash
|
||||
php -S 127.0.0.1:8009 -t htdocs
|
||||
```
|
||||
3. **Login‑Bypass** lokal aktivieren:
|
||||
```bash
|
||||
export AUTH_DISABLED=true
|
||||
```
|
||||
4. Browser: `http://127.0.0.1:8009`
|
||||
|
||||
---
|
||||
|
||||
## Deployment mit Docker Compose
|
||||
|
||||
Im Ordner `compose/` befindet sich ein funktionsfähiges Setup mit:
|
||||
- **app** (Apache+PHP 8.3), bind‑mountet `htdocs` und nutzt `PDF_STORAGE_DIR` Volume
|
||||
- **mariadb** (11.x)
|
||||
- **schema-loader** (initialisiert Tabellen)
|
||||
- **phpmyadmin** (optional)
|
||||
- **fix-pdf-perms** (setzt 0777 auf PDF‑Volume)
|
||||
|
||||
**Starten:**
|
||||
```bash
|
||||
cd compose
|
||||
cp ../.env .env # eigene Werte setzen
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Zugriff:** `http://<host>:${APP_PORT:-8009}`
|
||||
**phpMyAdmin:** `http://<host>:${PHPMYADMIN_PORT:-8010}`
|
||||
|
||||
---
|
||||
|
||||
## Deployment mit Portainer (Stack)
|
||||
|
||||
1. **Stack → Add Stack**
|
||||
2. **Environment Vars** nach obiger `.env` setzen (oder Datei hochladen).
|
||||
3. **Compose** verwenden (aus `compose/docker-compose.yml`), z. B.:
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
services:
|
||||
app:
|
||||
image: thecodingmachine/php:8.3-v4-apache
|
||||
user: "${APP_UID:-0}:${APP_GID:-0}"
|
||||
ports:
|
||||
- "${APP_PORT:-8009}:80"
|
||||
environment:
|
||||
PHP_EXTENSION_PDO_MYSQL: "1"
|
||||
APP_BASE_URL: ${APP_BASE_URL}
|
||||
AUTH_DISABLED: ${AUTH_DISABLED}
|
||||
DB_HOST: ${DB_HOST}
|
||||
DB_PORT: ${DB_PORT}
|
||||
DB_NAME: ${DB_NAME}
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASS: ${DB_PASS}
|
||||
PDF_STORAGE_DIR: ${PDF_STORAGE_DIR}
|
||||
OIDC_PROVIDER: ${OIDC_PROVIDER}
|
||||
OIDC_CLIENT: ${OIDC_CLIENT}
|
||||
OIDC_SECRET: ${OIDC_SECRET}
|
||||
DOCBEE_BASEURL: ${DOCBEE_BASEURL}
|
||||
DOCBEE_USER: ${DOCBEE_USER}
|
||||
DOCBEE_PASS: ${DOCBEE_PASS}
|
||||
DOCBEE_TIME: ${DOCBEE_TIME}
|
||||
GITLAB_HOST: ${GITLAB_HOST}
|
||||
GITLAB_PROJECT_ID: ${GITLAB_PROJECT_ID}
|
||||
GITLAB_REF: ${GITLAB_REF}
|
||||
GITLAB_PATH: ${GITLAB_PATH}
|
||||
GITLAB_TOKEN: ${GITLAB_TOKEN}
|
||||
volumes:
|
||||
- ${APP_HTDOCS_HOST:-/opt/qa-tool/htdocs}:/var/www/html
|
||||
- pdf_storage:/var/reports
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
fix-pdf-perms:
|
||||
condition: service_completed_successfully
|
||||
restart: unless-stopped
|
||||
|
||||
mariadb:
|
||||
image: mariadb:11
|
||||
environment:
|
||||
MYSQL_DATABASE: ${DB_NAME}
|
||||
MYSQL_USER: ${DB_USER}
|
||||
MYSQL_PASSWORD: ${DB_PASS}
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mariadb-admin", "ping", "-h", "127.0.0.1", "-p${DB_PASS}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 20s
|
||||
restart: unless-stopped
|
||||
|
||||
schema-loader:
|
||||
image: mariadb:11
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DB_HOST: ${DB_HOST}
|
||||
DB_PORT: ${DB_PORT}
|
||||
DB_NAME: ${DB_NAME}
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASS: ${DB_PASS}
|
||||
command: >
|
||||
sh -lc "
|
||||
echo 'Waiting for DB…'; sleep 3;
|
||||
mysql -h $DB_HOST -P $DB_PORT -u$DB_USER -p$DB_PASS $DB_NAME <<SQL
|
||||
CREATE TABLE IF NOT EXISTS reports (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
module VARCHAR(255),
|
||||
module_version VARCHAR(100),
|
||||
pbx_version VARCHAR(100),
|
||||
olm_nummer VARCHAR(100),
|
||||
tester VARCHAR(255),
|
||||
docbee_url TEXT,
|
||||
summary VARCHAR(255),
|
||||
pdf_path TEXT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE IF NOT EXISTS steps (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
report_id BIGINT NOT NULL,
|
||||
step_index INT,
|
||||
step_id VARCHAR(50),
|
||||
title TEXT,
|
||||
expected TEXT,
|
||||
status ENUM('pass','fail','skip','na','') DEFAULT '',
|
||||
comment TEXT,
|
||||
evidence TEXT,
|
||||
group_title VARCHAR(255),
|
||||
group_index INT,
|
||||
CONSTRAINT fk_steps_report FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
SQL
|
||||
"
|
||||
restart: "no"
|
||||
|
||||
fix-pdf-perms:
|
||||
image: busybox:1.36
|
||||
volumes:
|
||||
- pdf_storage:/target
|
||||
command: |
|
||||
sh -lc '
|
||||
set +e
|
||||
mkdir -p /target
|
||||
chmod -R 0777 /target 2>/dev/null || true
|
||||
chown -R 0:0 /target 2>/dev/null || true
|
||||
exit 0
|
||||
'
|
||||
restart: "no"
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin:5
|
||||
ports:
|
||||
- "${PHPMYADMIN_PORT:-8010}:80"
|
||||
environment:
|
||||
PMA_HOST: mariadb
|
||||
PMA_USER: ${DB_USER}
|
||||
PMA_PASSWORD: ${DB_PASS}
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
pdf_storage:
|
||||
```
|
||||
|
||||
4. **Deploy Stack** → öffnen: `http://<host>:${APP_PORT}`.
|
||||
|
||||
---
|
||||
|
||||
## GitLab‑Vorlagen
|
||||
|
||||
- Konfiguration via ENV (`GITLAB_HOST`, `GITLAB_PROJECT_ID`, `GITLAB_REF`, `GITLAB_PATH`, `GITLAB_TOKEN`).
|
||||
- UI lädt per GitLab API die Liste der YAML‑Dateien aus `GITLAB_PATH` (nur `.yaml`/`.yml`).
|
||||
- Parser akzeptiert **YAML** und **JSON** (Fallback).
|
||||
|
||||
---
|
||||
|
||||
## DocBee‑Integration
|
||||
|
||||
- Beim Laden von `index.php` wird via `DOCBEE_BASEURL/restApi/login` ein Token geholt und **clientseitig** als `window.DOCBEE_TOKEN` bereitgestellt.
|
||||
- Exportablauf:
|
||||
1. **postToDocBee** erstellt eine Notiz im angegebenen **Ticket** (aus `DocBee Ticket‑URL`).
|
||||
2. Bei Erfolg wird die URL im Lauf aktualisiert.
|
||||
3. Danach erfolgt Server‑Export (DB + optional PDF).
|
||||
|
||||
**Erforderliche ENV:** `DOCBEE_BASEURL`, `DOCBEE_USER`, `DOCBEE_PASS`, `DOCBEE_TIME` (Minuten).
|
||||
|
||||
---
|
||||
|
||||
## PDF‑Export
|
||||
|
||||
- Erzeugung **im Browser** via **jsPDF** + **AutoTable** (CDN‑Fallbacks vorhanden).
|
||||
- **Logo** `logo_light.png` wird proportional (max 26×12 mm) **rechtsbündig** im Kopf platziert.
|
||||
- Fallback: Falls Libraries fehlen, wird ein Text‑PDF erzeugt.
|
||||
- Server speichert PDFs in `PDF_STORAGE_DIR` (Default `/var/reports`).
|
||||
|
||||
---
|
||||
|
||||
## YAML‑Vorlage (Beispiel inkl. OLM)
|
||||
|
||||
```yaml
|
||||
name: "Modul XYZ – Basis‑Tests"
|
||||
module: "Modul XYZ"
|
||||
module_version: "1.2.3"
|
||||
pbx_version: "8.1"
|
||||
olm_nummer: "OLM-12345"
|
||||
steps:
|
||||
- type: "group"
|
||||
title: "Telefonie Basis"
|
||||
- type: "step"
|
||||
id: "s1"
|
||||
title: "Anruf initiieren"
|
||||
expected: "Ruf wird erfolgreich aufgebaut"
|
||||
required: true
|
||||
- type: "step"
|
||||
id: "s2"
|
||||
title: "Rufumleitung"
|
||||
expected: "Ruf wird korrekt umgeleitet"
|
||||
required: false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Login‑Loop / 401:** OIDC‑Konfiguration prüfen (`OIDC_PROVIDER`, Redirect‑URL `/callback.php`). Lokal ggf. `AUTH_DISABLED=true`.
|
||||
- **DocBee 401/403:** `DOCBEE_USER/DOCBEE_PASS/DOCBEE_BASEURL/DOCBEE_TIME` prüfen.
|
||||
- **DB‑Fehler (PDO not loaded):** Image/Extension prüfen (`PHP_EXTENSION_PDO_MYSQL=1` gesetzt? Compose).
|
||||
- **Keine Schreibrechte PDF:** Volume/`fix-pdf-perms` prüfen; `PDF_STORAGE_DIR` existiert/schreibbar? `/api/health.php` ansehen.
|
||||
- **GitLab‑Liste leer:** Pfad/Ref/Token prüfen; nur `.yaml/.yml` werden angezeigt.
|
||||
- **Export blockiert:** Pflichtschritte haben keinen Status (📌).
|
||||
|
||||
---
|
||||
|
||||
## Sicherheitshinweise
|
||||
|
||||
- **AUTH_DISABLED** ausschließlich in Dev/Stage verwenden.
|
||||
- DocBee‑Token wird **clientseitig** verwendet – nur in internen Netzen nutzen oder alternative Server‑Proxy‑Variante vorsehen.
|
||||
- Secrets per Portainer‑Secrets/ENV setzen, nicht im Repo.
|
||||
- DB‑Backups/Retention für `reports`/`steps` einplanen; PDF‑Ablage regelmäßig bereinigen/archivieren.
|
||||
|
||||
---
|
||||
|
||||
## Lizenz / Nutzung
|
||||
|
||||
Interne Nutzung innerhalb o‑Byte / Kundenprojekten. Anpassungen willkommen.
|
||||
Reference in New Issue
Block a user