chore(scanner): alte Scanner-App zurueckgebaut
Die eigenstaendige Vorgaenger-App unter scanner.perinco.de ist auf dem Server entfernt: Unit project-scanner-backend (Port 5005), beide Apache-Vhosts, das Let's-Encrypt-Zertifikat samt Renewal-Config, /var/www/Projekt Scanner (486 MB) und die Datenbank project_scanner (Dump unter /root/backups). Damit entfallen die LEGACY_*-Variablen und das Importskript, dessen Quelle es nicht mehr gibt. /root/.cache/ms-playwright bleibt bewusst stehen: der Cache stammt aus der Installation der alten App, liegt aber ausserhalb beider Verzeichnisse und wird vom CRM-Scanner benutzt - playwright ist auf 1.60.0 gepinnt, damit es genau dieser Build (chromium-1223) bleibt. Der Hinweis steht jetzt in README und docs/06-betrieb.md, damit ihn beim naechsten Aufraeumen niemand mitnimmt. SCANNER_SCHEDULER_OFFSET geht auf 0: der Versatz sollte verhindern, dass beide Instanzen mit denselben Portal-Cookies gleichzeitig losziehen. Ohne zweite Instanz hat er keinen Zweck mehr, die Mechanik bleibt fuer kuenftige Erststarts im Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ab6b9f2372
commit
3c9d83161e
@@ -24,13 +24,6 @@ flowchart LR
|
||||
A -->|"/api"| E["Express :5006"]
|
||||
E --> C[("perinco_CRM")]
|
||||
E --> S[("perinco_scanner")]
|
||||
|
||||
U2(("Browser")) -->|"HTTPS"| A2["Apache :443<br/>scanner.perinco.de"]
|
||||
A2 -->|"/api, /uploads, /health"| E2["alte Scanner-App :5005"]
|
||||
E2 --> S2[("project_scanner")]
|
||||
|
||||
classDef alt fill:#f7f7f7,stroke:#9a9a9a,stroke-dasharray: 4 3
|
||||
class U2,A2,E2,S2 alt
|
||||
```
|
||||
|
||||
Der Scanner im CRM hängt unter `/api/scanner` und braucht damit **keine eigene
|
||||
@@ -115,6 +108,5 @@ Portbelegung `ss -tlnp` prüfen. Relevant hier:
|
||||
| Port | Dienst |
|
||||
| --- | --- |
|
||||
| 5006 | perinco CRM Backend (nur `127.0.0.1`) |
|
||||
| 5005 | alte Scanner-App (`0.0.0.0`) |
|
||||
| 3000 | weitere Vite-App |
|
||||
| 5678 | n8n |
|
||||
|
||||
+50
-90
@@ -2,7 +2,8 @@
|
||||
|
||||
Der Scanner durchsucht Projektbörsen nach Aufträgen und bewertet jeden Fund mit
|
||||
OpenAI gegen die Lebensläufe der Berater. Er ist eine vollständige Portierung
|
||||
der eigenständigen App und läuft parallel zu ihr.
|
||||
der eigenständigen App, die er seit dem 09.09.2026 ersetzt
|
||||
([Rückbau](#rückbau-der-alten-app---09092026)).
|
||||
|
||||
## Ein Suchlauf
|
||||
|
||||
@@ -71,10 +72,13 @@ Takt: mit dem Endzeitpunkt kam die Laufzeit von drei bis vier Minuten jedes Mal
|
||||
obendrauf, aus 15 Minuten wurden 19 - rund ein Fuenftel weniger Durchgaenge pro
|
||||
Tag, und die Trefferliste der Boersen zeigt nur die neuesten Projekte.
|
||||
|
||||
Die **Sperrfrist** ist die Antwort auf ein konkretes Problem: beide Instanzen
|
||||
benutzen dieselben Portal-Cookies. Beim ersten Start ziehen sonst beide
|
||||
gleichzeitig los, was Rate-Limits und CAPTCHAs provoziert - und zwei Chromium
|
||||
gleichzeitig belasten den Speicher doppelt.
|
||||
Die **Sperrfrist** war die Antwort auf ein konkretes Problem des
|
||||
Parallelbetriebs: beide Instanzen benutzten dieselben Portal-Cookies. Beim
|
||||
ersten Start zogen sonst beide gleichzeitig los, was Rate-Limits und CAPTCHAs
|
||||
provozierte - und zwei Chromium gleichzeitig belasten den Speicher doppelt. Seit
|
||||
dem Rückbau der alten App steht `SCANNER_SCHEDULER_OFFSET` auf 0; die Mechanik
|
||||
bleibt im Code, weil sie bei jedem Erststart einer weiteren Instanz wieder
|
||||
greift.
|
||||
|
||||
Der Zeitpunkt des letzten Laufs steht in `settings.last_scan_at`. Das ist kein
|
||||
Luxus, sondern nach zwei beobachteten Fehlern nötig:
|
||||
@@ -155,97 +159,53 @@ nie. Dagegen das **Notventil** `PIPELINE_MAX_RUNTIME_MS`: läuft die Sperre
|
||||
länger als eine Stunde, gibt der nächste Aufruf sie frei und startet neu. So
|
||||
erholt sich der Scanner selbst, statt auf ein Deployment zu warten.
|
||||
|
||||
## Parallelbetrieb - alles verdoppelt
|
||||
## Rückbau der alten App - 09.09.2026
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph alt["alte App (scanner.perinco.de)"]
|
||||
A1["Express :5005"] --> A2[("project_scanner")]
|
||||
A1 --> A3["uploads/ in<br/>/var/www/Projekt Scanner/backend"]
|
||||
A4["ENCRYPTION_KEY"] --- A1
|
||||
end
|
||||
Der Scanner lief seit dem Rollout am 06.08.2026 **parallel** zur eigenständigen
|
||||
Vorgänger-App unter `scanner.perinco.de`. Diese ist am 09.09.2026 vollständig
|
||||
zurückgebaut. Verschwunden sind:
|
||||
|
||||
subgraph neu["CRM (pcrm.perinco.de)"]
|
||||
N1["Express :5006<br/>/api/scanner"] --> N2[("perinco_scanner")]
|
||||
N1 --> N3["uploads/ in<br/>/var/www/perinco_CRM/server"]
|
||||
N4["SCANNER_ENCRYPTION_KEY"] --- N1
|
||||
end
|
||||
| Weg | War |
|
||||
| --- | --- |
|
||||
| Unit `project-scanner-backend` | Express auf `0.0.0.0:5005` |
|
||||
| Vhosts `scanner.perinco.de(.conf/-le-ssl.conf)` | eigener Apache-Vhost, Basic Auth |
|
||||
| Zertifikat `scanner.perinco.de` | eigenes Let's-Encrypt-Zert samt Renewal-Config |
|
||||
| `/var/www/Projekt Scanner` | 486 MB Code, `node_modules`, 19 CV-PDFs |
|
||||
| Datenbank `project_scanner` | 16,2 MB; Dump liegt unter `/root/backups/project_scanner_20260909-173447.sql.gz` |
|
||||
| `LEGACY_*` in `server/.env` | Quelle des Importskripts |
|
||||
| `server/src/scanner/importLegacy.js` | `npm run scanner:import` |
|
||||
|
||||
B["Projektbörsen"]
|
||||
O["OpenAI"]
|
||||
A1 --> B
|
||||
N1 --> B
|
||||
A1 --> O
|
||||
N1 --> O
|
||||
**Was bewusst stehen geblieben ist:** `/root/.cache/ms-playwright` (641 MB, Build
|
||||
`chromium-1223`). Der Cache stammt aus der Installation der alten App, aber
|
||||
`PLAYWRIGHT_BROWSERS_PATH` ist nirgends gesetzt - **beide** Apps haben ihn
|
||||
benutzt, und `playwright` ist im CRM auf exakt `1.60.0` gepinnt, damit es genau
|
||||
dieser Build bleibt. Wer den Cache mit dem alten Verzeichnis löscht, nimmt dem
|
||||
CRM-Scanner den Browser. Der Ordner `/var/www/Projekt Scanner/backend/.playwright`
|
||||
war dagegen toter Ballast einer Installation mit gesetztem
|
||||
`PLAYWRIGHT_BROWSERS_PATH` und ist mit dem Verzeichnis mitgegangen.
|
||||
|
||||
IMP["npm run scanner:import"] -.->|"liest"| A2
|
||||
IMP -.->|"schreibt"| N2
|
||||
IMP -.->|"kopiert"| N3
|
||||
```
|
||||
Was der Rückbau ändert:
|
||||
|
||||
| | alte App | CRM |
|
||||
| --- | --- | --- |
|
||||
| Datenbank | `project_scanner` | `perinco_scanner` |
|
||||
| Schlüssel | `ENCRYPTION_KEY` | `SCANNER_ENCRYPTION_KEY` |
|
||||
| CV-Dateien | eigener Ordner | eigener Ordner |
|
||||
| Aufräumen | 03:00 | 03:30 |
|
||||
| Anmeldung | HTTP Basic | CRM-Sitzung (JWT) |
|
||||
- **Kosten:** Beide Instanzen haben dieselben Projekte unabhängig voneinander
|
||||
bewertet, die OpenAI-Kosten fielen doppelt an. Faktisch war das schon am
|
||||
09.09.2026 um 08:49 vorbei - da wurde `scanning_active` in der alten
|
||||
Oberfläche auf `false` gesetzt, letzter Lauf dort 09:02.
|
||||
- **Portal-Sessions:** Beide Instanzen benutzten dieselben Cookies der
|
||||
Projektbörsen. Der Grund für `SCANNER_SCHEDULER_OFFSET` fällt damit weg, der
|
||||
Wert steht jetzt auf 0. Rate-Limits und CAPTCHAs durch zwei gleichzeitig
|
||||
scrapende Chromium-Sätze sind Geschichte.
|
||||
- **Speicher:** rund 200 MB RSS und ein zweiter Playwright-Satz weniger auf
|
||||
einer Box mit 7,7 GiB.
|
||||
- **Daten:** Nicht übernommen wurden 13380 Treffer, die es nur in der alten
|
||||
Datenbank gab, darunter 92 als *reviewed* und 5 als *applied* markierte. Ein
|
||||
nachträglicher Import war nicht sinnvoll: das Importskript vergibt die
|
||||
Original-Ids und wäre mit den vorhandenen Ids in `perinco_scanner`
|
||||
kollidiert. Der Bestand war ohnehin flüchtig - der Aufräumjob hält nur die
|
||||
jüngsten Tage. Wer doch etwas sucht, greift auf den Dump zurück.
|
||||
|
||||
**Der Preis:** Beide bewerten dieselben Projekte unabhängig voneinander, die
|
||||
OpenAI-Kosten fallen also doppelt an. Wer das senken will, stellt im CRM ein
|
||||
größeres Suchintervall ein - die Einstellung gilt nur für diese Instanz.
|
||||
|
||||
## Übernahme aus der alten Datenbank
|
||||
|
||||
`server/src/scanner/importLegacy.js`, aufrufbar als `npm run scanner:import`:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S["Start"] --> C{"Zieltabelle agents leer?"}
|
||||
C -->|nein, ohne --force| AB["Abbruch"]
|
||||
C -->|ja oder --force| E["Einstellungen kopieren<br/>inkl. OpenAI-Key"]
|
||||
E --> P["Portale über den Namen zuordnen:<br/>Benutzername, Session-Cookies, Status"]
|
||||
P --> PW{"LEGACY_ENCRYPTION_KEY gesetzt?"}
|
||||
PW -->|ja| PW2["Passwort entschlüsseln<br/>und neu verschlüsseln"]
|
||||
PW -->|nein| PW3["Passwort auslassen"]
|
||||
PW2 --> A["Agenten anlegen, Ids behalten<br/>CV-PDF kopieren"]
|
||||
PW3 --> A
|
||||
A --> M{"--with-matches?"}
|
||||
M -->|ja| M2["Projekte und Treffer<br/>mit Original-Ids"]
|
||||
M -->|nein| F["fertig"]
|
||||
M2 --> F
|
||||
```
|
||||
|
||||
Die Ids bleiben erhalten, damit übernommene Treffer weiter auf die richtigen
|
||||
Agenten und Projekte zeigen. Deshalb bricht das Skript bei bereits vorhandenen
|
||||
Agenten ab.
|
||||
|
||||
### Agenten nachträglich holen
|
||||
|
||||
Der Vollimport ist ein Erststart-Werkzeug. Um später nur die Suchagenten
|
||||
nachzuziehen, ohne Einstellungen, Portale und Treffer anzufassen:
|
||||
|
||||
```bash
|
||||
cd /var/www/perinco_CRM/server
|
||||
npm run scanner:import -- --agents-only --skip=Bilal --dry-run
|
||||
npm run scanner:import -- --agents-only --skip=Bilal
|
||||
```
|
||||
|
||||
`--agents-only` läuft **additiv**: vorhandene Agenten blockieren nicht, sondern
|
||||
werden über den Namen erkannt und übersprungen - was hier im CRM schon
|
||||
bearbeitet wurde, bleibt also unangetastet. Die Id der Quelle wird nur
|
||||
übernommen, solange sie in `perinco_scanner` frei ist; sonst vergibt die
|
||||
Datenbank eine neue. `--skip=` nimmt einen Namen oder eine Liste
|
||||
(`--skip=Bilal,Nico`), Groß- und Kleinschreibung spielt keine Rolle.
|
||||
`--force` ist in diesem Modus gesperrt: es würde Projekte und Treffer löschen,
|
||||
die der Lauf gar nicht anrührt.
|
||||
|
||||
> **Beim Rollout am 06.08.2026 festgestellt:** Die Portal-Passwörter der alten
|
||||
> Datenbank lassen sich mit dem dortigen `ENCRYPTION_KEY` nicht mehr
|
||||
> entschlüsseln - der Schlüssel wurde nach dem Speichern getauscht. Die alte App
|
||||
> kann sie damit ebenso wenig lesen, es ging also nichts verloren. Die
|
||||
> Session-Cookies liegen im Klartext und wurden übernommen; damit scrapt die
|
||||
> neue Instanz sofort.
|
||||
Die Domain `scanner.perinco.de` zeigt weiter auf den Server, hat aber keinen
|
||||
eigenen Vhost mehr: HTTP landet beim ersten aktivierten Vhost, HTTPS gibt eine
|
||||
Zertifikatswarnung. Der DNS-Eintrag muss beim Anbieter gelöscht werden.
|
||||
|
||||
## Oberfläche
|
||||
|
||||
|
||||
+20
-18
@@ -34,11 +34,10 @@ Das Passwort kommt aus `$env:PERINCO_CRM_SSH_PASSWORD` oder per `-Password`.
|
||||
| Unit | Verzeichnis | Port | Anmerkung |
|
||||
| --- | --- | --- | --- |
|
||||
| `perinco-crm-backend` | `/var/www/perinco_CRM/server` | 5006 | `MemoryMax=1536M` |
|
||||
| `project-scanner-backend` | `/var/www/Projekt Scanner/backend` | 5005 | alte App, läuft bis zur Ablosung |
|
||||
|
||||
Der Verzeichnisname der alten App enthält ein **Leerzeichen**. In Skripten und in
|
||||
der `.env` immer in Anführungszeichen setzen - systemd liest `.env` als
|
||||
`EnvironmentFile` und trennt sonst am Leerzeichen.
|
||||
Die zweite Unit `project-scanner-backend` (Port 5005, `/var/www/Projekt Scanner/backend`)
|
||||
ist mit dem [Rückbau der alten App](03-scanner.md#rückbau-der-alten-app---09092026)
|
||||
am 09.09.2026 entfallen. Port 5005 ist seither frei.
|
||||
|
||||
`MemoryMax=1536M` statt der ursprünglichen 512M: ein Scan-Lauf startet Chromium
|
||||
als Kindprozess in derselben cgroup. Mit 512M würde der OOM-Killer das Backend
|
||||
@@ -58,8 +57,7 @@ mitten im Scrapen beenden.
|
||||
| `SCANNER_ENCRYPTION_KEY` | AES-Schlüssel für Portal-Passwörter |
|
||||
| `SCANNER_UPLOAD_DIR` | CV-Verzeichnis, leer = `<server>/uploads` |
|
||||
| `SCANNER_SCHEDULER` | Hintergrund-Scraper, Standard an, nur `false` schaltet ab |
|
||||
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz für den ersten Lauf, Standard 30 |
|
||||
| `LEGACY_*` | Quelle für `npm run scanner:import` |
|
||||
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz für den ersten Lauf, seit dem Rückbau der alten App 0 |
|
||||
| `MS_TENANT_ID`, `MS_CLIENT_ID`, `MS_CLIENT_SECRET` | App-only-Zugriff auf Microsoft Graph |
|
||||
| `MS_MAILBOX` | abzugleichendes Postfach, Standard `crm@perinco.de` |
|
||||
| `MS_MAIL_FOLDER` | Ordner, Standard `inbox` |
|
||||
@@ -81,10 +79,17 @@ Exchange Online: [11 Postfachanbindung](11-postfach.md).
|
||||
|
||||
## Playwright
|
||||
|
||||
`playwright` ist auf **exakt 1.60.0** gepinnt - dieselbe Version wie die alte App.
|
||||
Deren Chromium-Build (`chromium-1223`) liegt in `/root/.cache/ms-playwright`, also
|
||||
fällt kein zweiter Browser-Download an. Ein `^` in der Version würde das
|
||||
aufgeben; die Begründung steht als `comments`-Feld in `server/package.json`.
|
||||
`playwright` ist auf **exakt 1.60.0** gepinnt. Der passende Chromium-Build
|
||||
(`chromium-1223`) liegt in `/root/.cache/ms-playwright` - dieser Cache stammt aus
|
||||
der Installation der alten App und hat deren Rückbau überlebt, weil beide Apps
|
||||
ihn geteilt haben und `PLAYWRIGHT_BROWSERS_PATH` nirgends gesetzt ist. Ein `^` in
|
||||
der Version würde einen zweiten Browser-Download ziehen; die Begründung steht als
|
||||
`comments`-Feld in `server/package.json`.
|
||||
|
||||
> **Beim Aufräumen nicht anfassen:** `/root/.cache/ms-playwright` (641 MB) liegt
|
||||
> ausserhalb beider Anwendungsverzeichnisse. Wer ihn mit löscht, nimmt dem
|
||||
> Scanner den Browser - Oberfläche und Treffer bleiben, Scan-Lauf und
|
||||
> Verbindungstest fallen aus.
|
||||
|
||||
Fehlt der Browser, funktioniert alles ausser Scan-Lauf und Verbindungstest:
|
||||
|
||||
@@ -96,17 +101,14 @@ cd /var/www/perinco_CRM/server && npx playwright install chromium
|
||||
|
||||
```bash
|
||||
bash /var/www/perinco_CRM/deploy/scanner-setup.sh # .env ergänzen, Schlüssel erzeugen
|
||||
cd /var/www/perinco_CRM/server
|
||||
npm run scanner:import -- --dry-run
|
||||
npm run scanner:import -- --with-matches
|
||||
systemctl restart perinco-crm-backend
|
||||
```
|
||||
|
||||
> **Reihenfolge:** Der Import schreibt `search_interval` aus der alten App in die
|
||||
> Einstellungen. Der laufende Scheduler merkt die Änderung binnen einer Minute
|
||||
> und plant sich neu - beim Rollout hat er direkt nach dem Import einen Scan
|
||||
> gestartet, den der folgende Restart mitten im Scrapen beendet hat. Harmlos, aber
|
||||
> vermeidbar: erst neu starten, dann importieren.
|
||||
Datenbank und Tabellen legt der Start selbst an, Portale und
|
||||
Standardeinstellungen werden geseedet. Agenten, Zugangsdaten und Cookies kommen
|
||||
danach über die Oberfläche. Das frühere `npm run scanner:import` holte diese
|
||||
Daten aus der Datenbank der alten App - beides ist mit dem Rückbau am 09.09.2026
|
||||
entfallen.
|
||||
|
||||
## Störungssuche
|
||||
|
||||
|
||||
@@ -187,7 +187,6 @@ Diese Punkte haben real Zeit gekostet:
|
||||
| **Reihenfolge in `server.js`** | Der 404-Fallback `app.use('/api', ...)` fängt alles, was danach registriert wird. Neue Router davor einhängen. |
|
||||
| **`Content-Type` bei FormData** | `apiRequest` setzt JSON nur, wenn der Body **keine** `FormData` ist - sonst fehlt die multipart-Grenze und der Upload scheitert. |
|
||||
| **Dateien in neuen Tabs** | Ein `<a href>` schickt kein Bearer-Token. Deshalb holt `openAgentCv()` das PDF per `fetch` und öffnet einen Blob. |
|
||||
| **Leerzeichen in Pfaden** | Der alte Scanner liegt unter `/var/www/Projekt Scanner/…`. In der `.env` muss der Wert gequotet sein, sonst schneidet systemd ihn am Leerzeichen ab. |
|
||||
| **`.env` ist gleichzeitig `EnvironmentFile`** | Also kein `export`, keine Shell-Ausdrücke, Werte mit Leerzeichen in Anführungszeichen. |
|
||||
| **Zeitstempel können nur vorziehen** | Ein „vorgealterter" `lastRunTime` verzögert keinen Cronlauf. Wer verzögern will, braucht eine eigene Frist. |
|
||||
| **Sperrfristen bei jedem Start neu** | Früher schob jedes Deployment den fälligen Scan um 30 Minuten - er fand nie statt. Deshalb liegt `last_scan_at` in der Datenbank. |
|
||||
|
||||
@@ -188,7 +188,6 @@ Die portierte Scanner-API unter `/api/scanner`, vollständig in
|
||||
| `services/pdfParser.js` | `pdf-parse`, doppelte Leerzeilen raus |
|
||||
| `utils/crypto.js` | AES über `crypto-js`; `decryptWith()` für fremde Schlüssel |
|
||||
| `uploadDir.js` | `UPLOAD_DIR`, `ensureUploadDir()`, `resolveCvPath()` |
|
||||
| `importLegacy.js` | einmalige Übernahme aus der alten Datenbank |
|
||||
|
||||
Zusagen, auf die sich der Rest verlässt:
|
||||
|
||||
|
||||
+3
-10
@@ -12,7 +12,7 @@ keine Vorkenntnisse voraus.
|
||||
| --- | --- |
|
||||
| [01 Architektur](01-architektur.md) | Topologie, Prozesse, Ports, Anfrageweg, Rollen |
|
||||
| [02 Datenmodell](02-datenmodell.md) | Tabellen beider Datenbanken als ER-Diagramme |
|
||||
| [03 Projekt Scanner](03-scanner.md) | Suchlauf von der Börse bis zum Treffer, Parallelbetrieb |
|
||||
| [03 Projekt Scanner](03-scanner.md) | Suchlauf von der Börse bis zum Treffer, Rückbau der alten App |
|
||||
| [04 Diktat](04-diktat.md) | Spracherkennung im Browser, Modell-Lebenszyklus |
|
||||
| [05 Frontend](05-frontend.md) | Ansichten, Menü, Komponentenkarte, Zustand |
|
||||
| [06 Betrieb](06-betrieb.md) | Deployment, Umgebungsvariablen, Dienste, Störungssuche |
|
||||
@@ -42,8 +42,6 @@ flowchart TB
|
||||
PW["Playwright<br/>Chromium"]
|
||||
DB1[("MariaDB<br/>perinco_CRM")]
|
||||
DB2[("MariaDB<br/>perinco_scanner")]
|
||||
ALT["alte Scanner-App<br/>:5005"]
|
||||
DB3[("MariaDB<br/>project_scanner")]
|
||||
end
|
||||
|
||||
OAI["OpenAI<br/>gpt-4o-mini"]
|
||||
@@ -62,13 +60,8 @@ flowchart TB
|
||||
API -.-> MSCH
|
||||
MSCH -->|"Delta Query, nur lesend"| GRAPH
|
||||
MSCH --> DB1
|
||||
ALT --> DB3
|
||||
ALT --> BOERSE
|
||||
SPA -.->|"Audio bleibt im Browser"| WSP
|
||||
|
||||
classDef alt fill:#f7f7f7,stroke:#9a9a9a,stroke-dasharray: 4 3
|
||||
class ALT,DB3 alt
|
||||
```
|
||||
|
||||
Gestrichelt: die ursprüngliche Scanner-App, die bis zur Ablosung parallel
|
||||
weiterläuft. Sie hat ihre eigene Datenbank und wird vom CRM nicht angefasst.
|
||||
Die ursprüngliche Scanner-App unter `scanner.perinco.de` ist am 09.09.2026
|
||||
zurückgebaut worden; das CRM ist seither die einzige Instanz.
|
||||
|
||||
Reference in New Issue
Block a user