Compare commits

..
Author SHA1 Message Date
patrickandClaude Sonnet 5.5 7515b8db4d refactor: weitere identische Pakete nach common/ (svcauth, auditclient, tenantregistryclient, policyclient, httpharden, loadtest, httpclient)
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
Archive-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
AI-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Archive-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
Archive-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Connect-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
Workflow-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
Connect-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Mail-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Mail-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Workflow-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Mail-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
Workflow-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Nur Pakete, deren Code ohne Kommentare/Importpfade byte-gleich ist:
- httpclient: ai, connect (Root behaelt eigene Kopie wegen strengerem Waechtertest)
- svcauth: archive, ai, workflow
- auditclient: ai, connect, archive
- tenantregistryclient: dms, ai, workflow (archive weicht ab, bleibt)
- policyclient: mail, connect (dms weicht ab, bleibt)
- httpharden: Root, archive (dms weicht ab, bleibt)
- loadtest: mail, ai (archive, connect weichen ab, bleiben)

go.mod (require + replace) fuer ai, connect, workflow; Pflichttest-Gates
von ai/workflow und CI-Pfadfilter um common/ erweitert, damit die
sicherheitskritischen Pakete (svcauth, tenantregistryclient) weiter
gegated werden.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 14:06:07 +01:00
patrickandClaude Sonnet 5.5 cd5ca8da53 refactor: geteiltes Modul common/ (Pilot httpclient, apierror)
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
Archive-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Mail-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
- common/httpclient: bisher byte-identisch in dms, mail und archive
- common/apierror: bisher byte-identisch in Root und archive
- Importpfade, go.mod (require + replace), archtest-Ausnahmen und
  CI-Pfadfilter der mail-/archive-Pflichttest-Gates angepasst
- Waechtertest fuer den Standard-Client prueft weiter dms, mail, archive
- dms/internal/apierror bleibt eigenstaendig (5xx-Fix in eigenem PR)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 13:52:24 +01:00
480 changed files with 2075 additions and 11004 deletions

No files matched your search

-2
View File
@@ -1,2 +0,0 @@
# common/ ist modulübergreifend: Änderungen brauchen Review (Auswirkungsliste im PR)
/common/ @patrick
+1
View File
@@ -4,6 +4,7 @@ on:
pull_request:
paths:
- "ai/**"
- "common/**"
jobs:
pflichttest-gate:
@@ -4,6 +4,7 @@ on:
pull_request:
paths:
- "archive/**"
- "common/**"
jobs:
pflichttest-gate:
@@ -1,23 +0,0 @@
name: common Pflichttest-Gate
on:
pull_request:
paths:
- "common/**"
jobs:
pflichttest-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Gate bauen
working-directory: common
run: go build -o /tmp/pflichttestgate ./cmd/pflichttestgate
- name: Geänderte Dateien gegen Pflichttest-Regel pruefen
run: |
git diff --name-only "origin/${{ github.base_ref }}...HEAD" -- common/ | /tmp/pflichttestgate
-33
View File
@@ -1,33 +0,0 @@
name: common Versions-Tag
on:
push:
branches: [main]
paths:
- "common/**"
jobs:
tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Build, Vet, Test
working-directory: common
run: |
go build ./...
go vet ./...
go test ./... -count=1
- name: Naechsten Tag setzen
run: |
minor=$(tr -d '[:space:]' < common/MINOR)
last=$(git tag -l "common/v0.${minor}.*" --sort=-v:refname | head -n1)
if [ -z "$last" ]; then next="common/v0.${minor}.0"; else
patch=${last##*.}; base=${last%.*}; next="${base}.$((patch + 1))"
fi
git tag "$next"
git push origin "$next"
-37
View File
@@ -1,37 +0,0 @@
name: DB-Integrationstests
on:
pull_request:
paths:
- "common/**"
- "archive/**"
- "ai/**"
- "connect/**"
- "scripts/ci-pg-cluster.sh"
- ".gitea/workflows/db-tests.yml"
jobs:
db-tests:
runs-on: nexarch-test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Wegwerf-Postgres starten
run: scripts/ci-pg-cluster.sh start
- name: common
working-directory: common
run: go test ./... -count=1 -p 1
- name: archive
working-directory: archive
run: go test ./... -count=1 -p 1
- name: ai
working-directory: ai
run: go test ./... -count=1 -p 1
- name: connect
working-directory: connect
run: go test ./... -count=1 -p 1
- name: Wegwerf-Postgres stoppen
if: always()
run: scripts/ci-pg-cluster.sh stop
@@ -4,6 +4,7 @@ on:
pull_request:
paths:
- "mail/**"
- "common/**"
jobs:
pflichttest-gate:
@@ -4,6 +4,7 @@ on:
pull_request:
paths:
- "workflow/**"
- "common/**"
jobs:
pflichttest-gate:
-564
View File
@@ -604,567 +604,3 @@ Keine Commits in dieser Session.
- scripts/qa02-foundation-gate.sh | 2 +-
---
## 2026-10-19 12:07 – 12:07 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/audit-log/app/page.tsx | 6 +++---
- web/dms-browser/app/layout.tsx | 6 +++---
- web/lic-admin/app/page.tsx | 18 +++++++++---------
- web/portal/app/layout.tsx | 12 ++----------
- web/portal/app/page.tsx | 4 ++--
- web/shl/components/ModuleNavShell.tsx | 18 +++++++++++++++---
- web/shl/components/Shell.tsx | 2 ++
- web/shl/index.ts | 2 +-
- web/shl/tokens/tokens.ts | 24 ++++++++++++++++++++++++
- web/status-page/app/page.tsx | 12 ++++++------
- web/tenant-admin/app/page.tsx | 8 ++++----
---
## 2026-10-19 12:08 – 12:08 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/audit-log/app/page.tsx | 6 +++---
- web/dms-browser/app/layout.tsx | 6 +++---
- web/lic-admin/app/page.tsx | 18 +++++++++---------
- web/portal/app/layout.tsx | 12 ++----------
- web/portal/app/page.tsx | 4 ++--
- web/shl/components/ModuleNavShell.tsx | 18 +++++++++++++++---
- web/shl/components/Shell.tsx | 2 ++
- web/shl/index.ts | 2 +-
- web/shl/tokens/tokens.ts | 24 ++++++++++++++++++++++++
- web/status-page/app/page.tsx | 12 ++++++------
- web/tenant-admin/app/page.tsx | 8 ++++----
---
## 2026-10-19 12:14 – 12:15 (1m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/audit-log/app/page.tsx | 6 +++---
- web/dms-browser/app/layout.tsx | 6 +++---
- web/lic-admin/app/page.tsx | 18 +++++++++---------
- web/portal/app/layout.tsx | 12 ++----------
- web/portal/app/page.tsx | 4 ++--
- web/shl/components/ModuleNavShell.tsx | 18 +++++++++++++++---
- web/shl/components/Shell.tsx | 2 ++
- web/shl/index.ts | 2 +-
- web/shl/tokens/tokens.ts | 24 ++++++++++++++++++++++++
- web/status-page/app/page.tsx | 12 ++++++------
- web/tenant-admin/app/page.tsx | 8 ++++----
---
## 2026-10-19 12:29 – 12:30 (1m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- dms/internal/upload/service.go | 38 ++++++++++++++++++++++++++++++---
- dms/internal/upload/service_test.go | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
---
## 2026-10-19 12:31 – 12:31 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- dms/internal/upload/service.go | 25 ++++++++++++++++++++++++-
- dms/internal/upload/service_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++
---
## 2026-10-19 12:44 – 12:53 (8m)
**Beschreibung:** Claude Code Session
**Projekt:** banking
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- dms/internal/upload/service.go | 25 ++++++++++++++++++++++++-
- dms/internal/upload/service_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++
---
## 2026-10-19 12:54 – 12:54 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- dms/internal/upload/service.go | 25 ++++++++++++++++++++++++-
- dms/internal/upload/service_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++
---
## 2026-10-19 13:47 – 13:50 (2m)
**Beschreibung:** Claude Code Session
**Projekt:** banking
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/mail-postfach/app/layout.tsx | 8 +++++---
- web/shl/components/ModuleNavShell.tsx | 4 ++++
---
## 2026-10-19 14:49 – 14:50 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** archivmail
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- MIGRATIONSKONZEPT.md | 2 +-
- PRODUKTARCHITEKTUR.md | 2 +-
- SICHERHEITSKONZEPT.md | 16 ++++++++--------
- SKALIERUNGSKONZEPT.md | 2 +-
- STORAGE-KONZEPT.md | 4 ++--
- dms/internal/searchsync/searchsync_test.go | 4 ++--
- web/portal/app/ModuleGrid.test.tsx | 3 ++-
- web/portal/app/ModuleGrid.tsx | 17 +----------------
- web/portal/lib/modules.test.ts | 4 +++-
- web/shl/components/ModuleNavShell.tsx | 27 +++++++++++++++++++--------
- web/shl/index.ts | 2 +-
---
## 2026-10-19 14:58 – 23:11 (8h 12m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-20 04:03 – 04:03 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-20 04:03 – 04:03 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-20 04:03 – 04:03 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-20 04:03 – 04:03 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-20 04:03 – 04:03 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 11:18 – 11:18 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 11:20 – 11:20 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 11:54 – 11:54 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 11:55 – 11:55 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 11:57 – 11:57 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 11:58 – 11:58 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 12:37 – 12:38 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 12:38 – 12:38 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 12:40 – 12:40 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 12:42 – 12:42 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 12:42 – 12:44 (1m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 12:47 – 12:52 (4m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 12:53 – 12:54 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 12:55 – 12:56 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 13:00 – 13:01 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** Trad-Bot
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 13:35 – 13:39 (4m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 13:42 – 13:52 (10m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 13:53 – 13:53 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 13:54 – 14:06 (12m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 14:08 – 14:41 (32m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 15:03 – 15:25 (22m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
## 2026-10-27 15:27 – 15:43 (15m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
- web/shl/index.ts | 3 +++
---
+1 -12
View File
@@ -6,17 +6,6 @@ Stand: 2026-08-26. Zusammenführung aller bisherigen Entscheidungen aus `nexarch
NEXARCH ist die Nachfolgeplattform von **archivmail** (E-Mail-Archivierung) und **archivdms** (Dokumentenmanagement) — nicht als Umbenennung, sondern als modulare Neuentwicklung unter der Positionierung **"Next Generation Information Management"**: E-Mails, Dokumente, digitale Archivierung, OCR, Volltextsuche, Workflows, Compliance, Integrationen, KI — als ein zusammenhängendes System, nicht als zwei getrennte Werkzeuge nebeneinander.
## 1a. Vorbildsysteme
NEXARCH vereint die Stärken bestehender Systeme und vermeidet deren bekannte Schwächen (Details: `dms-kanban/known-issues-archivdms.md`, `dms-kanban/known-issues-archivmail.md`).
| Bereich | Vorbildsysteme |
|---|---|
| Mail | **archivmail** (eigenes Produkt, Vorgänger), **MailPiler** |
| DMS | **paperless-ngx**, **Alfresco**, **Docspell**, **ecoDMS**, **archivdms** (eigenes Produkt, Vorgänger) |
Weitere Vergleichsprojekte (z. B. OpenKM, Nuxeo, Mayan EDMS, Seafile) sind noch zu prüfen. Für das DMS-Modul siehe auch `dms/README.md`.
## 2. Architekturprinzip
**Ein modularer Monolith aus sieben eigenständigen, aber koordinierten Diensten** — kein Microservice-Zwang, aber klare Dienstgrenzen mit eigenem Datenmodell, eigenem Go-Binary, eigenem Kanban-Board:
@@ -75,7 +64,7 @@ Kein Modul kopiert die Daten eines anderen — stattdessen ein wiederkehrendes *
Gemeinsamer Nenner aller drei: **Referenz statt Kopie, Adapter statt Direktkopplung.**
Go-`internal/`-Pakete sind zwischen den Modulen nicht sichtbar; identische Querschnittspakete liegen deshalb im versionierten Modul `common/` (Git-Tags `common/vX.Y.Z`, siehe `common/README.md`). So bleiben Module einzeln baubar (Docker, eigenes Repo, Partner-Build). Fachlich verschiedene Pakete (`audittrail`, `cutoverstage`, `crypto`) bleiben modul-lokal.
Go-`internal/`-Pakete sind zwischen den Modulen nicht sichtbar; identische Querschnittspakete werden deshalb im geteilten Modul `common/` gehalten (per `replace` in den `go.mod` eingebunden, keine eigenen Abhängigkeiten außer der Standardbibliothek). Pilot: `common/httpclient`, `common/apierror`. Fachlich verschiedene Pakete (`audittrail`, `cutoverstage`, `crypto`) bleiben modul-lokal.
Dasselbe Prinzip gilt auch innerhalb eines Moduls, auf Code-Ebene: Hauptdateien/Entry-Points bleiben klein, Fachlogik wandert in eigene Module. Vor einer neuen Funktions-/Modul-Datei wird geprüft, ob dieselbe Funktionalität nicht schon woanders existiert (auch modulübergreifend, z. B. in Core) — keine doppelten Funktionsdateien. Das eigene Kanban-Planungswerkzeug wurde nach genau diesem Muster korrigiert (sieben `kanban.html` enthielten identisches JS/CSS als Volltext-Duplikat, damals in `_shared/kanban.js`/`kanban.css` ausgelagert) — die Boards selbst sind inzwischen komplett aus dem Repo entfernt (Ticket-Historie liegt in der Gitea-PR-Historie), das Prinzip "keine doppelten Funktionsdateien" gilt unverändert für den eigentlichen NEXARCH-Code.
-40
View File
@@ -1,40 +0,0 @@
:root {
--bg: #0B0F17; --card: #131A26; --card2: #18212F; --border: #253044;
--fg: #E6EDF7; --muted: #93A1B8; --primary: #3B82F6;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--fg); font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
header { position: sticky; top: 0; z-index: 20; background: rgba(11,15,23,.92); backdrop-filter: blur(8px); border-bottom: 1px solid var(--border); padding: 14px 20px; }
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
h1 { font-size: 17px; margin: 0 14px 0 0; letter-spacing: -.01em; }
.sub { color: var(--muted); font-size: 12px; }
input, select, button { background: var(--card2); color: var(--fg); border: 1px solid var(--border); border-radius: 8px; padding: 7px 10px; font-size: 12px; font-family: inherit; }
input:focus, select:focus { outline: 1px solid var(--primary); }
button { cursor: pointer; }
button:hover { border-color: var(--primary); }
button.primary { background: rgba(59,130,246,.14); color: #93BBFB; border-color: rgba(59,130,246,.3); }
.stats { display: flex; gap: 14px; font-size: 11px; color: var(--muted); margin-top: 8px; flex-wrap: wrap; }
.stats b { color: var(--fg); }
.board { display: grid; grid-template-columns: repeat(5, minmax(240px, 1fr)); gap: 12px; padding: 16px 20px 40px; align-items: start; overflow-x: auto; }
.col { background: var(--card); border: 1px solid var(--border); border-radius: 12px; min-height: 160px; padding: 10px; }
.col.over { border-color: var(--primary); background: #16203042; }
.col h2 { font-size: 12px; margin: 2px 4px 10px; display: flex; justify-content: space-between; align-items: center; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.count { background: var(--card2); border: 1px solid var(--border); border-radius: 999px; padding: 1px 7px; font-size: 10px; color: var(--fg); }
.card { background: var(--card2); border: 1px solid var(--border); border-radius: 10px; padding: 10px; margin-bottom: 8px; cursor: grab; }
.card:hover { border-color: #3a4a66; }
.card.dragging { opacity: .45; }
.card .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; color: var(--muted); }
.card .title { font-size: 13px; margin: 4px 0 7px; line-height: 1.35; }
.tag { display: inline-block; font-size: 10px; padding: 2px 7px; border-radius: 999px; background: #202b3d; color: var(--muted); margin-right: 4px; margin-top: 3px; }
.tag.blocked { background: rgba(245,158,11,.15); color: #FBBF24; }
.tag.ready { background: rgba(34,197,94,.15); color: #4ADE80; }
.tag.wave { background: rgba(59,130,246,.14); color: #93BBFB; }
dialog { background: var(--card); color: var(--fg); border: 1px solid var(--border); border-radius: 14px; max-width: 860px; width: 94%; padding: 0; }
dialog::backdrop { background: rgba(0,0,0,.72); }
.dlg-head { padding: 18px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; }
.dlg-body { padding: 18px 20px 24px; max-height: 66vh; overflow: auto; }
pre { background: #0E1420; border: 1px solid var(--border); border-radius: 10px; padding: 14px; font-size: 11px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; color: #C7D3E5; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 18px 0 8px; }
ul { margin: 0; padding-left: 18px; font-size: 13px; color: #C7D3E5; line-height: 1.6; }
.chip { font-family: ui-monospace, Menlo, monospace; font-size: 11px; border: 1px solid var(--border); border-radius: 6px; padding: 2px 6px; margin: 0 4px 4px 0; display: inline-block; color: var(--muted); }
.empty { color: #55627a; font-size: 11px; text-align: center; padding: 14px 0; }
-152
View File
@@ -1,152 +0,0 @@
(function () {
var DATA = JSON.parse(document.getElementById('data').textContent);
var COLUMNS = ['Backlog', 'Bereit', 'In Arbeit', 'Prüfung', 'Fertig'];
var KEY = 'kanban-status-v1:' + location.pathname;
var state = {};
try { state = JSON.parse(localStorage.getItem(KEY) || '{}') || {}; } catch (e) { state = {}; }
var onlyReady = false;
function statusOf(t) { return state[t.code] || t.status || 'Backlog'; }
function isDone(code) { return statusOf(byCode[code] || { code: code }) === 'Fertig'; }
var byCode = {};
DATA.tickets.forEach(function (t) { byCode[t.code] = t; });
function ready(t) { return (t.dependsOn || []).every(function (d) { return isDone(d); }); }
function save() { localStorage.setItem(KEY, JSON.stringify(state)); }
// Filter befüllen
var fEpic = document.getElementById('fEpic'), fRole = document.getElementById('fRole'), fWave = document.getElementById('fWave');
DATA.epics.forEach(function (e) { var o = document.createElement('option'); o.value = e.key; o.textContent = e.emoji + ' ' + e.name; fEpic.appendChild(o); });
Array.from(new Set(DATA.tickets.map(function (t) { return t.role; }))).sort().forEach(function (r) { var o = document.createElement('option'); o.value = r; o.textContent = r; fRole.appendChild(o); });
var maxWave = Math.max.apply(null, DATA.tickets.map(function (t) { return t.wave; }));
for (var w = 1; w <= maxWave; w++) { var o = document.createElement('option'); o.value = String(w); o.textContent = 'Welle ' + w; fWave.appendChild(o); }
function visible() {
var q = document.getElementById('q').value.trim().toLowerCase();
return DATA.tickets.filter(function (t) {
if (fEpic.value && t.epic !== fEpic.value) return false;
if (fRole.value && t.role !== fRole.value) return false;
if (fWave.value && String(t.wave) !== fWave.value) return false;
if (onlyReady && (!ready(t) || statusOf(t) === 'Fertig')) return false;
if (q) {
var hay = (t.code + ' ' + t.title + ' ' + t.tech + ' ' + t.description).toLowerCase();
if (hay.indexOf(q) === -1) return false;
}
return true;
});
}
function render() {
var board = document.getElementById('board');
board.innerHTML = '';
var list = visible();
COLUMNS.forEach(function (col) {
var el = document.createElement('div');
el.className = 'col';
el.dataset.col = col;
var items = list.filter(function (t) { return statusOf(t) === col; });
var h = document.createElement('h2');
h.innerHTML = '<span>' + col + '</span><span class="count">' + items.length + '</span>';
el.appendChild(h);
if (!items.length) { var e = document.createElement('div'); e.className = 'empty'; e.textContent = '– leer –'; el.appendChild(e); }
items.forEach(function (t) { el.appendChild(card(t)); });
el.addEventListener('dragover', function (ev) { ev.preventDefault(); el.classList.add('over'); });
el.addEventListener('dragleave', function () { el.classList.remove('over'); });
el.addEventListener('drop', function (ev) {
ev.preventDefault(); el.classList.remove('over');
var code = ev.dataTransfer.getData('text/plain');
if (!code) return;
state[code] = col; save(); render();
});
board.appendChild(el);
});
stats(list);
}
function card(t) {
var d = document.createElement('div');
d.className = 'card';
d.draggable = true;
var blocked = !ready(t);
d.innerHTML = '<div class="code">' + t.code + ' · ' + t.estimateDays + ' PT</div>' +
'<div class="title">' + esc(t.title) + '</div>' +
'<span class="tag wave">Welle ' + t.wave + '</span>' +
'<span class="tag">' + t.role + '</span>' +
'<span class="tag">' + t.complexity + '</span>' +
(blocked ? '<span class="tag blocked">wartet auf ' + t.dependsOn.filter(function (c) { return !isDone(c); }).join(', ') + '</span>' : '<span class="tag ready">startbar</span>');
d.addEventListener('dragstart', function (ev) { ev.dataTransfer.setData('text/plain', t.code); d.classList.add('dragging'); });
d.addEventListener('dragend', function () { d.classList.remove('dragging'); });
d.addEventListener('click', function () { openDlg(t); });
return d;
}
function esc(s) { return String(s).replace(/[&<>]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]; }); }
function stats(list) {
var done = DATA.tickets.filter(function (t) { return statusOf(t) === 'Fertig'; });
var donePT = done.reduce(function (a, t) { return a + t.estimateDays; }, 0);
var readyNow = DATA.tickets.filter(function (t) { return ready(t) && statusOf(t) !== 'Fertig'; }).length;
document.getElementById('stats').innerHTML =
'<span><b>' + DATA.tickets.length + '</b> Kacheln</span>' +
'<span>sichtbar: <b>' + list.length + '</b></span>' +
'<span>fertig: <b>' + done.length + '</b> (' + donePT + ' von ' + DATA.totalDays + ' PT)</span>' +
'<span>jetzt startbar: <b>' + readyNow + '</b></span>' +
'<span>Wellen: <b>' + maxWave + '</b></span>';
}
var dlg = document.getElementById('dlg');
var current = null;
function openDlg(t) {
current = t;
document.getElementById('dCode').textContent = t.code + ' · ' + t.epicName;
document.getElementById('dTitle').textContent = t.title;
document.getElementById('dMeta').innerHTML =
'<span class="tag wave">Welle ' + t.wave + '</span><span class="tag">' + t.role + '</span><span class="tag">' + t.complexity + '</span><span class="tag">' + t.estimateDays + ' PT</span><span class="tag">' + statusOf(t) + '</span>';
document.getElementById('dDesc').textContent = t.description;
chips('dDeps', t.dependsOn);
chips('dNext', t.unlocks);
items('dAcc', t.akzeptanz);
items('dChk', t.pruefungen);
document.getElementById('dPrompt').textContent = t.prompt;
dlg.showModal();
}
function chips(id, arr) {
var el = document.getElementById(id);
el.innerHTML = (arr && arr.length) ? arr.map(function (c) { return '<span class="chip">' + c + (byCode[c] ? ' – ' + esc(byCode[c].title) : '') + '</span>'; }).join('') : '<span class="empty" style="padding:0">keine</span>';
}
function items(id, arr) {
document.getElementById(id).innerHTML = (arr || []).map(function (a) { return '<li>' + esc(a) + '</li>'; }).join('');
}
document.getElementById('closeBtn').onclick = function () { dlg.close(); };
document.getElementById('copyBtn').onclick = function () {
if (!current) return;
var btn = this;
navigator.clipboard.writeText(current.prompt).then(function () {
btn.textContent = 'Kopiert ✓';
setTimeout(function () { btn.textContent = 'Prompt kopieren'; }, 1800);
});
};
document.getElementById('q').addEventListener('input', render);
[fEpic, fRole, fWave].forEach(function (s) { s.addEventListener('change', render); });
document.getElementById('onlyReady').onclick = function () {
onlyReady = !onlyReady;
this.style.borderColor = onlyReady ? 'var(--primary)' : 'var(--border)';
this.textContent = onlyReady ? 'Filter: nur startbare' : 'Nur startbare';
render();
};
document.getElementById('resetBtn').onclick = function () {
if (confirm('Alle lokalen Statusänderungen verwerfen?')) { state = {}; save(); render(); }
};
document.getElementById('exportBtn').onclick = function () {
var rows = [['Code', 'Titel', 'Status']].concat(DATA.tickets.map(function (t) { return [t.code, t.title, statusOf(t)]; }));
var csv = rows.map(function (r) { return r.map(function (c) { return '"' + String(c).replace(/"/g, '""') + '"'; }).join(','); }).join('\n');
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }));
a.download = 'kanban-stand.csv';
a.click();
};
render();
})();
+15 -40
View File
@@ -43,7 +43,6 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/summaryapi"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/threadsummarize"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
)
@@ -53,24 +52,13 @@ const defaultListenAddr = "127.0.0.1:9600"
const defaultModel = "default"
type config struct {
dsn string
serviceToken string
addr string
model string
moduleURLs map[string]string
coreBaseURL string
coreServiceToken string
policyURL string
policyServiceToken string
policyDisabled bool
}
// policyClient liefert nil nur mit NEXARCH_AI_APP_POLICY_DISABLED=1.
func (c config) policyClient() *policyclient.Client {
if c.policyURL == "" {
return nil
}
return policyclient.New(c.policyURL, c.policyServiceToken)
dsn string
serviceToken string
addr string
model string
moduleURLs map[string]string
coreBaseURL string
coreServiceToken string
}
// parseModuleBaseURLs liest "modul=url,modul2=url2" (EXT-05: je Modul eine
@@ -96,19 +84,13 @@ func parseModuleBaseURLs(raw string) map[string]string {
// Programmabbruchs weiter unten im Code.
func loadConfig(getenv func(string) string) (config, error) {
cfg := config{
dsn: getenv("NEXARCH_AI_APP_TENANT_DSN"),
serviceToken: getenv("NEXARCH_AI_APP_SERVICE_TOKEN"),
addr: getenv("NEXARCH_AI_APP_LISTEN_ADDR"),
model: getenv("NEXARCH_AI_APP_MODEL"),
moduleURLs: parseModuleBaseURLs(getenv("NEXARCH_AI_APP_MODULE_BASE_URLS")),
coreBaseURL: getenv("NEXARCH_AI_APP_CORE_BASE_URL"),
coreServiceToken: getenv("NEXARCH_AI_APP_CORE_SERVICE_TOKEN"),
policyURL: getenv("NEXARCH_AI_APP_POLICY_URL"),
policyServiceToken: getenv("NEXARCH_AI_APP_POLICY_SERVICE_TOKEN"),
policyDisabled: getenv("NEXARCH_AI_APP_POLICY_DISABLED") == "1",
}
if cfg.policyURL != "" && cfg.policyServiceToken == "" {
return config{}, errors.New("ai-app: NEXARCH_AI_APP_POLICY_SERVICE_TOKEN ist erforderlich, wenn NEXARCH_AI_APP_POLICY_URL gesetzt ist")
dsn: getenv("NEXARCH_AI_APP_TENANT_DSN"),
serviceToken: getenv("NEXARCH_AI_APP_SERVICE_TOKEN"),
addr: getenv("NEXARCH_AI_APP_LISTEN_ADDR"),
model: getenv("NEXARCH_AI_APP_MODEL"),
moduleURLs: parseModuleBaseURLs(getenv("NEXARCH_AI_APP_MODULE_BASE_URLS")),
coreBaseURL: getenv("NEXARCH_AI_APP_CORE_BASE_URL"),
coreServiceToken: getenv("NEXARCH_AI_APP_CORE_SERVICE_TOKEN"),
}
if cfg.dsn == "" {
return config{}, errors.New("ai-app: NEXARCH_AI_APP_TENANT_DSN ist erforderlich")
@@ -116,9 +98,6 @@ func loadConfig(getenv func(string) string) (config, error) {
if cfg.serviceToken == "" {
return config{}, errors.New("ai-app: NEXARCH_AI_APP_SERVICE_TOKEN ist erforderlich")
}
if cfg.policyURL == "" && !cfg.policyDisabled {
return config{}, errors.New("ai-app: NEXARCH_AI_APP_POLICY_URL ist erforderlich (oder NEXARCH_AI_APP_POLICY_DISABLED=1 als Uebergang)")
}
if cfg.addr == "" {
cfg.addr = defaultListenAddr
}
@@ -150,11 +129,7 @@ func build(pool *pgxpool.Pool, cfg config, prov provider.Provider, workerID stri
).RegisterRoutes(mux)
searchSvc := semanticsearch.New(prov, vectorindex.New(pool), cfg.model)
policy := cfg.policyClient()
if policy == nil {
log.Print("ai-app: WARNUNG: Rechtepruefung ai.search.read deaktiviert (NEXARCH_AI_APP_POLICY_DISABLED=1)")
}
semanticsearchapi.NewServer(searchSvc, cfg.serviceToken).WithPolicy(policy).RegisterRoutes(mux)
semanticsearchapi.NewServer(searchSvc, cfg.serviceToken).RegisterRoutes(mux)
chunkStore := vectorindex.New(pool)
docchatapi.NewServer(
+2 -43
View File
@@ -17,7 +17,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/mock"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/queue"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
// AK3 + AK4: eine fehlende Pflicht-Umgebungsvariable liefert einen klaren
@@ -44,36 +44,10 @@ func TestLoadConfig_MissingRequiredVars(t *testing.T) {
}
}
// Issue #369: Policy-URL ohne Service-Token darf nicht still ohne
// Rechtepruefung starten.
func TestLoadConfig_PolicyURLRequiresServiceToken(t *testing.T) {
func TestLoadConfig_Defaults(t *testing.T) {
env := map[string]string{
"NEXARCH_AI_APP_TENANT_DSN": "postgres://x",
"NEXARCH_AI_APP_SERVICE_TOKEN": "tok",
"NEXARCH_AI_APP_POLICY_URL": "http://127.0.0.1:8093",
}
get := func(k string) string { return env[k] }
if _, err := loadConfig(get); err == nil || !strings.Contains(err.Error(), "NEXARCH_AI_APP_POLICY_SERVICE_TOKEN") {
t.Fatalf("erwartete fehler zur fehlenden NEXARCH_AI_APP_POLICY_SERVICE_TOKEN, bekam %v", err)
}
env["NEXARCH_AI_APP_POLICY_SERVICE_TOKEN"] = "policy-tok"
cfg, err := loadConfig(get)
if err != nil {
t.Fatalf("unerwarteter fehler: %v", err)
}
if cfg.policyClient() == nil {
t.Fatal("policy-client muss bei gesetzter URL aktiv sein")
}
if (config{}).policyClient() != nil {
t.Fatal("ohne URL darf kein policy-client entstehen")
}
}
func TestLoadConfig_Defaults(t *testing.T) {
env := map[string]string{
"NEXARCH_AI_APP_TENANT_DSN": "postgres://x",
"NEXARCH_AI_APP_SERVICE_TOKEN": "tok",
"NEXARCH_AI_APP_POLICY_DISABLED": "1",
}
cfg, err := loadConfig(func(k string) string { return env[k] })
if err != nil {
@@ -210,18 +184,3 @@ func TestBuild_QueueWorkerProcessesJobWithQuota(t *testing.T) {
t.Errorf("quota-zaehlerstand = %d, want 1 (GOV-04-Verbrauchszaehlung nicht verdrahtet)", value)
}
}
func TestLoadConfig_PolicyRequiredUnlessDisabled(t *testing.T) {
env := map[string]string{
"NEXARCH_AI_APP_TENANT_DSN": "postgres://x",
"NEXARCH_AI_APP_SERVICE_TOKEN": "tok",
}
get := func(k string) string { return env[k] }
if _, err := loadConfig(get); err == nil || !strings.Contains(err.Error(), "NEXARCH_AI_APP_POLICY_URL") {
t.Fatalf("erwartete fehler zur fehlenden NEXARCH_AI_APP_POLICY_URL, bekam %v", err)
}
env["NEXARCH_AI_APP_POLICY_DISABLED"] = "1"
if _, err := loadConfig(get); err != nil {
t.Fatalf("mit POLICY_DISABLED=1 darf der Start nicht scheitern: %v", err)
}
}
+1 -2
View File
@@ -19,7 +19,7 @@ import (
)
const (
fixtureTenant = "beispiel-mandant"
fixtureTenant = "beispiel-mandant"
fixtureSourceType = "dms_document"
fixtureSourceID = "beispiel-dokument-1"
)
@@ -77,6 +77,5 @@ func main() {
docchatapi.NewServer(chatSvc, "test-token").RegisterRoutes(mux)
log.Println("searchchatapi-mockserver listening on 127.0.0.1:19603")
// archtest:allow Einmaliges lokales pa11y-Testwerkzeug, kein Produktdienst
log.Fatal(http.ListenAndServe("127.0.0.1:19603", mux))
}
+5 -4
View File
@@ -4,10 +4,7 @@ go 1.25.0
toolchain go1.25.13
require (
gitea.perlbach24.de/scripte/nexarch/common v0.2.0
github.com/jackc/pgx/v5 v5.11.0
)
require github.com/jackc/pgx/v5 v5.11.0
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -16,3 +13,7 @@ require (
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
)
require gitea.perlbach24.de/scripte/nexarch/common v0.0.0
replace gitea.perlbach24.de/scripte/nexarch/common => ../common
-2
View File
@@ -1,5 +1,3 @@
gitea.perlbach24.de/scripte/nexarch/common v0.2.0 h1:gcOjoOvUkT0Zmqq3m7WQp1MfXSgmzrGTF2E/+NkO2bw=
gitea.perlbach24.de/scripte/nexarch/common v0.2.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"testing"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
func newStore(t *testing.T) *Store {
@@ -9,7 +9,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/moduletype"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
// --- Vertrag 1: GOV-01 registrierter Objekttyp (moduletype.ObjectType) ---
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"testing"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
func newRecorder(t *testing.T) *Recorder {
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/classify"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
"github.com/jackc/pgx/v5/pgxpool"
)
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/queue"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
func newPool(t *testing.T) *pgxpool.Pool {
@@ -13,7 +13,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/extractmapping"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/metadatapushclient"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
func newMappingRegistry(t *testing.T) *extractmapping.Registry {
@@ -10,7 +10,7 @@ import (
"testing"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
func newRegistry(t *testing.T) *Registry {
@@ -81,7 +81,7 @@ func TestPush_UnknownFieldKey_ReportedNotDropped(t *testing.T) {
c := New(srv.URL, "svc-token-123")
result, err := c.Push(context.Background(), "doc-1", "ai", map[string]string{
"rechnungsdatum": "2026-01-15",
"rechnungsdatum": "2026-01-15",
"nicht_existent_feld": "wert",
})
if err != nil {
+205 -11
View File
@@ -1,27 +1,221 @@
// Package migrate bindet die gemeinsame Migrationslogik aus common an den
// modulspezifischen Fortschrittstabellennamen.
// Package migrate implements versioned, revertible SQL migrations (no ORM) — a
// standalone copy of the Core/DMS pattern, since AI is its own Go module
// and cannot import other modules' internal/ packages.
package migrate
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
cmigrate "gitea.perlbach24.de/scripte/nexarch/common/migrate"
)
type Migration = cmigrate.Migration
// Migration ist eine einzelne versionierte Migrationsdatei.
type Migration struct {
Version string // Dateiname ohne .up.sql/.down.sql, z.B. "0001_documents"
UpSQL string
DownSQL string
}
// Load liest alle *.up.sql/*.down.sql-Paare aus dir, sortiert nach
// Dateiname (Akzeptanzkriterium: Migrationen sind versioniert).
func Load(dir string) ([]Migration, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("migrationsverzeichnis %q lesen: %w", dir, err)
}
var versions []string
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".up.sql") {
continue
}
versions = append(versions, strings.TrimSuffix(e.Name(), ".up.sql"))
}
sort.Strings(versions)
migrations := make([]Migration, 0, len(versions))
for _, v := range versions {
up, err := os.ReadFile(filepath.Join(dir, v+".up.sql"))
if err != nil {
return nil, fmt.Errorf("migration %q: up.sql lesen: %w", v, err)
}
down, err := os.ReadFile(filepath.Join(dir, v+".down.sql"))
if err != nil {
return nil, fmt.Errorf("migration %q: down.sql lesen (jede Migration braucht ein Rollback): %w", v, err)
}
migrations = append(migrations, Migration{Version: v, UpSQL: string(up), DownSQL: string(down)})
}
return migrations, nil
}
// TrackingTable ist modulspezifisch benannt, damit sich Module in
// derselben Datenbank/demselben Schema nicht gegenseitig den
// Migrationsfortschritt ueberschreiben (FDN-13).
const TrackingTable = "connect_schema_migrations"
var migrator = cmigrate.MustNew(TrackingTable)
const legacyTrackingTable = "schema_migrations"
func Load(dir string) ([]Migration, error) { return cmigrate.Load(dir) }
// ensureTrackingTable legt TrackingTable an. Beim erstmaligen Anlegen wird
// ein vorhandener Bestand aus der frueheren, unqualifizierten
// schema_migrations uebernommen (nur Versionen, die dieses Modul kennt, und
// nur wenn dessen Basisversion dort steht — eine schema_migrations eines
// anderen Moduls oder eines anderen Schemas wird weder gelesen noch
// veraendert). Die alte Tabelle
// bleibt unangetastet.
func ensureTrackingTable(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) error {
tx, err := pool.Begin(ctx)
if err != nil {
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
}
defer func() { _ = tx.Rollback(ctx) }()
func Up(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) ([]string, error) {
return migrator.Up(ctx, pool, migrations)
// Serialisiert gleichzeitige Erstanlage/Uebernahme (mehrere App-Instanzen).
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtext('connect_schema_migrations'))`); err != nil {
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
}
var existed bool
if err := tx.QueryRow(ctx, `SELECT to_regclass(current_schema() || '.' || $1::text) IS NOT NULL`, TrackingTable).Scan(&existed); err != nil {
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
}
if existed {
return tx.Commit(ctx)
}
if _, err := tx.Exec(ctx, `
CREATE TABLE `+TrackingTable+` (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
`); err != nil {
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
}
if len(migrations) > 0 {
var legacy bool
if err := tx.QueryRow(ctx, `SELECT to_regclass(current_schema() || '.' || $1::text) IS NOT NULL`, legacyTrackingTable).Scan(&legacy); err != nil {
return fmt.Errorf("%s pruefen: %w", legacyTrackingTable, err)
}
if legacy {
versions := make([]string, len(migrations))
for i, m := range migrations {
versions[i] = m.Version
}
var hasBase bool
if err := tx.QueryRow(ctx,
`SELECT EXISTS (SELECT 1 FROM `+legacyTrackingTable+` WHERE version = $1)`, versions[0],
).Scan(&hasBase); err != nil {
return fmt.Errorf("%s lesen: %w", legacyTrackingTable, err)
}
if hasBase {
if _, err := tx.Exec(ctx, `
INSERT INTO `+TrackingTable+` (version, applied_at)
SELECT version, applied_at FROM `+legacyTrackingTable+` WHERE version = ANY($1)
`, versions); err != nil {
return fmt.Errorf("bestand aus %s uebernehmen: %w", legacyTrackingTable, err)
}
}
}
}
return tx.Commit(ctx)
}
func DownOne(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) (string, error) {
return migrator.DownOne(ctx, pool, migrations)
func appliedVersions(ctx context.Context, pool *pgxpool.Pool) (map[string]bool, error) {
rows, err := pool.Query(ctx, `SELECT version FROM `+TrackingTable)
if err != nil {
return nil, fmt.Errorf("angewendete migrationen lesen: %w", err)
}
defer rows.Close()
applied := map[string]bool{}
for rows.Next() {
var v string
if err := rows.Scan(&v); err != nil {
return nil, fmt.Errorf("migrationsversion lesen: %w", err)
}
applied[v] = true
}
return applied, rows.Err()
}
// Up wendet alle noch nicht angewendeten Migrationen in Reihenfolge an
// (Akzeptanzkriterium 2: vorwaerts ausfuehrbar) — bereits angewendete
// werden uebersprungen, damit Up auf einer leeren UND auf einer bestehenden
// DB funktioniert (Pruefung 1).
func Up(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) (applied []string, err error) {
if err := ensureTrackingTable(ctx, pool, migrations); err != nil {
return nil, err
}
already, err := appliedVersions(ctx, pool)
if err != nil {
return nil, err
}
for _, m := range migrations {
if already[m.Version] {
continue
}
tx, err := pool.Begin(ctx)
if err != nil {
return applied, fmt.Errorf("transaktion fuer %q starten: %w", m.Version, err)
}
if _, err := tx.Exec(ctx, m.UpSQL); err != nil {
_ = tx.Rollback(ctx)
return applied, fmt.Errorf("migration %q anwenden: %w", m.Version, err)
}
if _, err := tx.Exec(ctx, `INSERT INTO `+TrackingTable+` (version) VALUES ($1)`, m.Version); err != nil {
_ = tx.Rollback(ctx)
return applied, fmt.Errorf("migration %q als angewendet markieren: %w", m.Version, err)
}
if err := tx.Commit(ctx); err != nil {
return applied, fmt.Errorf("migration %q committen: %w", m.Version, err)
}
applied = append(applied, m.Version)
}
return applied, nil
}
// DownOne macht die zuletzt angewendete Migration rueckgaengig
// (Akzeptanzkriterium 2: rueckwaerts ausfuehrbar) und liefert deren Version,
// oder "" falls keine Migration angewendet war.
func DownOne(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) (version string, err error) {
if err := ensureTrackingTable(ctx, pool, migrations); err != nil {
return "", err
}
already, err := appliedVersions(ctx, pool)
if err != nil {
return "", err
}
var last *Migration
for i := len(migrations) - 1; i >= 0; i-- {
if already[migrations[i].Version] {
last = &migrations[i]
break
}
}
if last == nil {
return "", nil
}
tx, err := pool.Begin(ctx)
if err != nil {
return "", fmt.Errorf("transaktion fuer rollback von %q starten: %w", last.Version, err)
}
if _, err := tx.Exec(ctx, last.DownSQL); err != nil {
_ = tx.Rollback(ctx)
return "", fmt.Errorf("migration %q zurueckrollen: %w", last.Version, err)
}
if _, err := tx.Exec(ctx, `DELETE FROM `+TrackingTable+` WHERE version = $1`, last.Version); err != nil {
_ = tx.Rollback(ctx)
return "", fmt.Errorf("migration %q aus %s entfernen: %w", last.Version, TrackingTable, err)
}
if err := tx.Commit(ctx); err != nil {
return "", fmt.Errorf("rollback von %q committen: %w", last.Version, err)
}
return last.Version, nil
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/modelconfig"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
type fakeLister struct {
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"testing"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
// fakeVerifier simuliert Core TEN-01 fuer die Registry-Tests, ohne einen
@@ -12,7 +12,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/moduletype"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
type fakeVerifier struct{ validTenants map[string]bool }
+2
View File
@@ -17,6 +17,8 @@ import (
// sensitivePathPatterns beschreibt die Bereiche aus
// TESTSTRATEGIE-AI.md Abschnitt 3.
var sensitivePathPatterns = []*regexp.Regexp{
regexp.MustCompile(`(^|/)ai/internal/svcauth/`),
regexp.MustCompile(`(^|/)common/svcauth/`),
regexp.MustCompile(`(^|/)ai/internal/tenantfeature/`),
regexp.MustCompile(`(^|/)ai/internal/semanticsearch/`),
regexp.MustCompile(`(^|/)ai/internal/semanticsearchapi/`),
+5 -5
View File
@@ -1,24 +1,24 @@
// Negativtest des Gates selbst (QA-01 Prüfung 1): ein Diff mit
// geänderter ai/internal/tenantfeature/tenantfeature.go ohne begleitende Testdatei
// geänderter ai/internal/svcauth/svcauth.go ohne begleitende Testdatei
// muss als Verstoß erkannt werden.
package pflichttestgate
import "testing"
func TestCheckDiff_FlagsSensitiveChangeWithoutTest(t *testing.T) {
violations := CheckDiff([]string{"ai/internal/tenantfeature/tenantfeature.go"})
violations := CheckDiff([]string{"ai/internal/svcauth/svcauth.go"})
if len(violations) != 1 {
t.Fatalf("erwartet genau 1 verstoß, habe %d: %+v", len(violations), violations)
}
if violations[0].Package != "ai/internal/tenantfeature" {
if violations[0].Package != "ai/internal/svcauth" {
t.Fatalf("falsches package gemeldet: %+v", violations[0])
}
}
func TestCheckDiff_PassesWhenTestFileAccompanies(t *testing.T) {
violations := CheckDiff([]string{
"ai/internal/tenantfeature/tenantfeature.go",
"ai/internal/tenantfeature/tenantfeature_test.go",
"ai/internal/svcauth/svcauth.go",
"ai/internal/svcauth/svcauth_test.go",
})
if len(violations) != 0 {
t.Fatalf("erwartet keine verstöße, habe: %+v", violations)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"testing"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
func newStore(t *testing.T) *Store {
@@ -1,10 +1,10 @@
// Package classification ist das Prüfgate für QA-03: misst Klassifizierungs- und
// Package qa03gate ist das Prüfgate für QA-03: misst Klassifizierungs- und
// Extraktionsgenauigkeit am Referenzkorpus (QA-01, ai/internal/refcorpus)
// und belegt die Wirkung von Korrektur-Feedback auf künftige Vorschläge.
// Reines Prüfgate — ruft nur bestehende, bereits gemergte Pakete auf
// (classify CLS-02, extract EXT-05-Grundlage, correction/historysuggest
// CLS-05), ohne deren Code zu ändern.
package classification
package qa03gate
import (
"context"
@@ -1,4 +1,4 @@
// Package searchquality ist das Prüfgate für QA-04: es misst Suchqualität,
// Package qa04gate ist das Prüfgate für QA-04: es misst Suchqualität,
// RAG-Antwortgüte und Quellenverweis-Nachvollziehbarkeit auf einem festen,
// synthetischen Testkorpus (SEM-01..06 bereits gemergt) und ändert keines der
// geprüften Pakete. Der Korpus stammt aus refcorpus (QA-01), die "Einbettung"
@@ -7,7 +7,7 @@
// einem externen Modell abhängt. Reine Termfrequenz ohne IDF-Gewichtung wurde
// probiert und verworfen: seltene, fachlich entscheidende Wörter (z.B.
// "Gesamtbetrag") gingen gegen häufige, generische Wörter unter.
package searchquality
package qa04gate
import (
"context"
@@ -231,3 +231,4 @@ func (p *embedProvider) Chat(ctx context.Context, req provider.ChatRequest) (pro
func (p *embedProvider) Complete(ctx context.Context, req provider.CompletionRequest) (provider.CompletionResponse, error) {
return provider.CompletionResponse{}, errors.New("qa04gate: Complete wird vom Prüfgate nicht genutzt")
}
@@ -1,4 +1,4 @@
package searchquality
package qa04gate
import (
"context"
@@ -1,4 +1,4 @@
package searchquality
package qa04gate
// testQuestion ist eine feste Testfrage des QA-04-Prüfgates gegen den
// refcorpus-Testkorpus (QA-01). erwarteteSourceID ist leer, wenn die Frage
@@ -1,9 +1,9 @@
// Package governance ist das Prüfgate für QA-05 (Datenschutz- &
// Package qa05gate ist das Prüfgate für QA-05 (Datenschutz- &
// Governance-Prüfung). Es bringt keine neue Fachlogik ein, sondern führt
// GOV-02/GOV-03/GOV-05 end-to-end gegen eine echte Postgres-Wegwerf-DB bzw.
// einen Mock-Core zusammen, um die drei QA-05-Akzeptanzkriterien in der
// Praxis (nicht nur auf Store-Ebene) nachzuweisen.
package governance
package qa05gate
import (
"context"
@@ -23,8 +23,8 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/openai"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/tenantfeature"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
)
// realPool liefert einen frischen, migrierten Pool auf der Wegwerf-DB aus
@@ -36,7 +36,7 @@ func realPool(t *testing.T) *pgxpool.Pool {
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest übersprungen")
}
pool := testdb.Pool(t, dsn)
migrationsDir, err := filepath.Abs("../../../migrations")
migrationsDir, err := filepath.Abs("../../migrations")
if err != nil {
t.Fatalf("migrationsverzeichnis: %v", err)
}
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
func newPool(t *testing.T) *pgxpool.Pool {
+3 -3
View File
@@ -16,9 +16,9 @@ import (
// eine Zeitueberschreitung real simulieren laesst (respektiert ctx wie ein
// echter HTTP-Client es tun wuerde).
type slowProvider struct {
delay time.Duration
calls atomic.Int64
cancelled atomic.Int64
delay time.Duration
calls atomic.Int64
cancelled atomic.Int64
}
func (s *slowProvider) Chat(ctx context.Context, req provider.ChatRequest) (provider.ChatResponse, error) {
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
func newPool(t *testing.T) *pgxpool.Pool {
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/openai"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
func setup(t *testing.T) *residency.Store {
@@ -13,7 +13,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residencyapi"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
const testToken = "test-service-token"
@@ -8,7 +8,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/historysuggest"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
func newStore(t *testing.T) *Store {
@@ -11,7 +11,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/routingfeedback"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
const testToken = "test-service-token"
@@ -11,8 +11,8 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
)
func newRealPool(t *testing.T) *pgxpool.Pool {
@@ -1,8 +1,7 @@
// Package semanticsearchapi ist die HTTP-Bindung der semantischen Suche
// (SEM-03). Aufrufer ist das anfragende Modul (DMS/Mail), nicht der
// Endnutzer direkt — Service-Token-Pflicht wie moduleadapter (Workflow
// ENG-02), fail closed. Zusaetzlich (Issue #369) Rechtepruefung
// ai.search.read gegen Core fuer den Mandanten aus dem Anfrage-Koerper.
// ENG-02), fail closed.
package semanticsearchapi
import (
@@ -11,30 +10,18 @@ import (
"net/http"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
const permissionSearchRead = "ai.search.read"
type Server struct {
svc *semanticsearch.Service
serviceToken string
policy *policyclient.Client
}
func NewServer(svc *semanticsearch.Service, serviceToken string) *Server {
return &Server{svc: svc, serviceToken: serviceToken}
}
// WithPolicy aktiviert die Rechtepruefung gegen Core (POST /authorize).
// Rolle ist fest "user", weil der Modul-zu-Modul-Aufruf keine Nutzerrolle
// transportiert (Uebergang, gleiches Muster wie connect/internal/statusapi).
func (s *Server) WithPolicy(client *policyclient.Client) *Server {
s.policy = client
return s
}
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("POST /search", s.requireAuth(s.handleSearch))
}
@@ -50,22 +37,6 @@ func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc {
}
}
func (s *Server) permitted(w http.ResponseWriter, r *http.Request, tenantSlug, permission string) bool {
if s.policy == nil {
return true
}
allowed, err := s.policy.Allowed(r.Context(), tenantSlug, "user", permission)
if err != nil {
writeError(w, http.StatusServiceUnavailable, "berechtigungsdienst nicht erreichbar")
return false
}
if !allowed {
writeError(w, http.StatusForbidden, "keine berechtigung")
return false
}
return true
}
type refRequest struct {
SourceType string `json:"source_type"`
SourceID string `json:"source_id"`
@@ -88,9 +59,6 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "ungueltiger anfrage-koerper")
return
}
if !s.permitted(w, r, req.TenantSlug, permissionSearchRead) {
return
}
refs := make([]semanticsearch.Ref, 0, len(req.AllowedRefs))
for _, ref := range req.AllowedRefs {
@@ -1,100 +0,0 @@
package semanticsearchapi_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
)
// Steht fuer Core's POST /authorize-Vertrag ({"allowed": bool}); prueft
// die echte Kette requireAuth -> permitted -> policyclient -> 403/200.
func newFakePolicyAPI(t *testing.T, allow bool, gotBody *map[string]string) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/authorize" || r.Header.Get("X-Service-Token") == "" {
t.Fatalf("unerwartete anfrage: %s %s", r.Method, r.URL.Path)
}
if gotBody != nil {
_ = json.NewDecoder(r.Body).Decode(gotBody)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]bool{"allowed": allow})
}))
}
func searchRequest(t *testing.T) *http.Request {
t.Helper()
body, _ := json.Marshal(map[string]any{
"tenant_slug": "acme",
"query": "test",
"top_k": 5,
"allowed_refs": []map[string]string{{"source_type": "dms_document", "source_id": "doc-1"}},
})
req := httptest.NewRequest(http.MethodPost, "/search", bytes.NewReader(body))
req.Header.Set("X-Service-Token", "geheimes-token")
return req
}
func TestHandleSearch_PolicyDenies_Rejected(t *testing.T) {
var got map[string]string
policyAPI := newFakePolicyAPI(t, false, &got)
defer policyAPI.Close()
mux := http.NewServeMux()
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, searchRequest(t))
if rec.Code != http.StatusForbidden {
t.Fatalf("erwartet 403 ohne berechtigung, bekam %d: %s", rec.Code, rec.Body.String())
}
if got["permission"] != "ai.search.read" || got["tenant"] != "acme" {
t.Fatalf("falsche policy-anfrage: %+v", got)
}
}
func TestHandleSearch_PolicyAllows_Passes(t *testing.T) {
policyAPI := newFakePolicyAPI(t, true, nil)
defer policyAPI.Close()
mux := http.NewServeMux()
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, searchRequest(t))
if rec.Code != http.StatusOK {
t.Fatalf("erwartet 200 mit berechtigung, bekam %d: %s", rec.Code, rec.Body.String())
}
}
func TestHandleSearch_PolicyUnavailable_FailsClosed(t *testing.T) {
policyAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer policyAPI.Close()
mux := http.NewServeMux()
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, searchRequest(t))
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("erwartet 503 bei ausfall des berechtigungsdienstes, bekam %d", rec.Code)
}
}
func TestHandleSearch_PolicyCheckedOnlyAfterServiceToken(t *testing.T) {
policyAPI := newFakePolicyAPI(t, true, nil)
defer policyAPI.Close()
mux := http.NewServeMux()
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
req := searchRequest(t)
req.Header.Set("X-Service-Token", "falsch")
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("erwartet 401 bei falschem service-token, bekam %d", rec.Code)
}
}
+3 -3
View File
@@ -125,10 +125,10 @@ func wordSet(s string) map[string]bool {
// TypeMetrics fasst die Bewertungen aller Stichproben eines
// Zusammenfassungstyps zusammen (AK1).
type TypeMetrics struct {
Type string
Count int
Type string
Count int
AverageGroundedness float64
FlaggedCount int
FlaggedCount int
}
// AggregateByType berechnet je Zusammenfassungstyp die Kennzahl über alle
@@ -27,6 +27,7 @@ func (s *stubProvider) Embed(ctx context.Context, req provider.EmbeddingRequest)
return provider.EmbeddingResponse{}, nil
}
// AK1: Konfidenz wird je Vorschlag mitgeliefert.
func TestSuggest_ReturnsConfidencePerSuggestion(t *testing.T) {
p := &stubProvider{response: `[{"tag":"rechnung","confidence":0.9,"reasoning":"enthaelt rechnungsnummer"},{"tag":"steuer","confidence":0.4,"reasoning":"erwaehnt umsatzsteuer"}]`}
@@ -7,7 +7,7 @@ import (
"testing"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
func newStore(t *testing.T) *Store {
@@ -7,7 +7,6 @@ package testdb
import (
"context"
"fmt"
"os"
"sync/atomic"
"testing"
"time"
@@ -17,21 +16,6 @@ import (
var seq atomic.Int64
// AdminDSN liefert TEST_ADMIN_DSN. Fehlt er, wird uebersprungen; mit
// REQUIRE_DB=1 (CI) zaehlt das Fehlen als Fehler.
func AdminDSN(t testing.TB) string {
t.Helper()
dsn := os.Getenv("TEST_ADMIN_DSN")
if dsn != "" {
return dsn
}
if os.Getenv("REQUIRE_DB") == "1" {
t.Fatal("testdb: TEST_ADMIN_DSN nicht gesetzt, aber REQUIRE_DB=1")
}
t.Skip("testdb: TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen")
return ""
}
// Pool legt ein Schema an und liefert einen darauf ausgerichteten Pool;
// beides wird per t.Cleanup wieder entfernt.
func Pool(t testing.TB, dsn string) *pgxpool.Pool {
@@ -30,9 +30,9 @@ type Thread struct {
// Result ist die Zusammenfassung eines Threads.
type Result struct {
Summary string
MessageCount int
LastMessageID string
Summary string
MessageCount int
LastMessageID string
}
// Options steuert die Zusammenfassung.
+1 -1
View File
@@ -11,8 +11,8 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
)
func newPool(t *testing.T) *pgxpool.Pool {
+1 -1
View File
@@ -15,8 +15,8 @@ import (
"gitea.perlbach24.de/scripte/nexarch/archive/internal/legalholdapi"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/moduleadapter"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/tenantregistryclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
)
func main() {
+1 -1
View File
@@ -30,7 +30,7 @@ Verbindlich für jeden Pull Request, der Dateien in einem der folgenden Bereiche
- **WORM-Durchsetzung** (`archive/internal/wormstore/`) — die Storage-Object-Lock-Garantie selbst.
- **Zugriffsprüfung** (`archive/internal/rbacclient/`, `archive/internal/retentionapi/`) — RBAC-Anbindung
und die APIs, die sie durchsetzen.
- **Tenant-/Schlüssel-Bezug** (`archive/internal/kekclient/`,
- **Tenant-/Schlüssel-Bezug** (`archive/internal/tenantregistryclient/`, `archive/internal/kekclient/`,
`archive/internal/backupcrypto/`) — unabhängige Tenant-Validierung und Verschlüsselungspfade.
Regel: **jede geänderte `.go`-Datei in einem dieser Bereiche muss von einer geänderten oder neuen
+5 -4
View File
@@ -4,10 +4,7 @@ go 1.25.0
toolchain go1.25.13
require (
gitea.perlbach24.de/scripte/nexarch/common v0.1.0
github.com/jackc/pgx/v5 v5.9.2
)
require github.com/jackc/pgx/v5 v5.9.2
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -16,3 +13,7 @@ require (
golang.org/x/sync v0.22.0 // indirect
golang.org/x/text v0.41.0 // indirect
)
require gitea.perlbach24.de/scripte/nexarch/common v0.0.0
replace gitea.perlbach24.de/scripte/nexarch/common => ../common
-2
View File
@@ -1,5 +1,3 @@
gitea.perlbach24.de/scripte/nexarch/common v0.1.0 h1:biNzFr2LOMZC7b1leY3smQtrMmSMp6IW80OvR4LSjLY=
gitea.perlbach24.de/scripte/nexarch/common v0.1.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -1,51 +0,0 @@
package archtest
import (
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
// Architekturtest: im Produktivcode dieses Moduls gibt es keine Aufrufe ueber
// den Standard-Client ohne Zeitlimit.
func TestNoUnboundedDefaultClientInProductionCode(t *testing.T) {
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
forbidden := regexp.MustCompile(`http\.DefaultClient|http\.(Get|Post|Head|PostForm)\(`)
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if n := d.Name(); n == "node_modules" || n == "testdata" || n == ".git" {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
for i, line := range strings.Split(string(data), "\n") {
if t := strings.TrimSpace(line); strings.HasPrefix(t, "//") {
continue
}
if forbidden.MatchString(line) {
rel, _ := filepath.Rel(root, path)
t.Errorf("%s:%d nutzt den Standard-Client ohne Zeitlimit: %s", rel, i+1, strings.TrimSpace(line))
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ func moduleRoot(t *testing.T) string {
// Antwort bzw. der rohe ListenAndServe-Aufruf genau die erlaubte
// Implementierung der Regel selbst ist (API-20/API-21), kein Verstoss.
// Das Archive-Modul hat keine weiteren, physisch verschachtelten Go-Module.
var exempt = []string{}
var exempt = []string{"internal/httpharden"}
// TestNoErrorTextLeaksIntoHTTPResponses ist Akzeptanzkriterium 1: kein
// err.Error() direkt in einer HTTP-Antwort, ausser ueber apierror.
+1
View File
@@ -15,6 +15,7 @@ import (
var sensitivePathPatterns = []*regexp.Regexp{
regexp.MustCompile(`(^|/)archive/internal/wormstore/`),
regexp.MustCompile(`(^|/)archive/internal/rbacclient/`),
regexp.MustCompile(`(^|/)archive/internal/tenantregistryclient/`),
regexp.MustCompile(`(^|/)archive/internal/retentionapi/`),
regexp.MustCompile(`(^|/)archive/internal/kekclient/`),
regexp.MustCompile(`(^|/)archive/internal/backupcrypto/`),
@@ -60,6 +60,7 @@ func TestCheckDiff_CoversAllSensitiveAreas(t *testing.T) {
cases := []string{
"archive/internal/wormstore/wormstore.go",
"archive/internal/rbacclient/rbacclient.go",
"archive/internal/tenantregistryclient/tenantregistryclient.go",
"archive/internal/retentionapi/handler.go",
"archive/internal/kekclient/kekclient.go",
"archive/internal/backupcrypto/reposecret.go",
+1 -1
View File
@@ -1,7 +1,7 @@
// Package retentionverify implementiert VAL-03: Stichprobenvergleich von
// retain_until (Quelle aus TAR-01 vs. nach der Migration im generischen
// Retention-Objektmodell) je Tenant, nach dem Berichtsmuster aus VAL-01
// (internal/migration/verify): Abweichung ist entweder erklaert oder
// (internal/migrationverify): Abweichung ist entweder erklaert oder
// blockierender Befund, nie reine Notiz.
package retentionverify
@@ -0,0 +1,60 @@
// Package tenantregistryclient ist ein schlanker HTTP-Client fuer Core
// TEN-01 (Tenant-Registry). Archive ist ein physisch getrenntes Go-Modul
// und kann Core TEN-01 daher nicht direkt importieren - nur ueber dessen
// HTTP-Schnittstelle abfragen. Gleiches Muster wie archive/internal/
// rbacclient (RET-08) und archive/internal/notifyclient (RET-07).
package tenantregistryclient
import (
"context"
"fmt"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"net/http"
"net/url"
)
// Client fragt Core TEN-01 nach der Gueltigkeit eines Tenant-Kontexts.
type Client struct {
BaseURL string
ServiceToken string
HTTPClient *http.Client
}
func New(baseURL, serviceToken string) *Client {
return &Client{BaseURL: baseURL, ServiceToken: serviceToken}
}
// VerifyTenant prueft, ob tenantSlug in der Core-Tenant-Registry als
// existierender Tenant gefuehrt wird - unabhaengig davon, was das
// aufrufende Modul (DMS/Mail) selbst behauptet. Ein Transport-/
// Serverfehler wird als Fehler zurueckgegeben, NICHT stillschweigend als
// gueltig behandelt - der Aufrufer muss fail-closed (ablehnen) reagieren.
func (c *Client) VerifyTenant(ctx context.Context, tenantSlug string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/tenants/"+url.PathEscape(tenantSlug), nil)
if err != nil {
return false, fmt.Errorf("tenantregistryclient: request bauen: %w", err)
}
req.Header.Set("X-Service-Token", c.ServiceToken)
resp, err := c.httpClient().Do(req)
if err != nil {
return false, fmt.Errorf("tenantregistryclient: aufruf fehlgeschlagen: %w", err)
}
defer func() { _ = resp.Body.Close() }()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusNotFound:
return false, nil
default:
return false, fmt.Errorf("tenantregistryclient: unerwarteter status %d", resp.StatusCode)
}
}
func (c *Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return httpclient.Default()
}
@@ -0,0 +1,59 @@
package tenantregistryclient
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
func TestVerifyTenant_ExistingTenant(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/tenants/mandant-a" {
t.Errorf("pfad = %q, want /tenants/mandant-a", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
c := New(server.URL, "test-token")
ok, err := c.VerifyTenant(context.Background(), "mandant-a")
if err != nil {
t.Fatalf("verifytenant: %v", err)
}
if !ok {
t.Fatal("erwartet gueltigen tenant, habe false")
}
}
func TestVerifyTenant_UnknownTenant(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
c := New(server.URL, "test-token")
ok, err := c.VerifyTenant(context.Background(), "unbekannt")
if err != nil {
t.Fatalf("verifytenant: %v", err)
}
if ok {
t.Fatal("erwartet ungueltigen tenant, habe true")
}
}
func TestVerifyTenant_ServerErrorIsFailClosed(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
c := New(server.URL, "test-token")
ok, err := c.VerifyTenant(context.Background(), "mandant-a")
if err == nil {
t.Fatal("erwartet fehler bei 500-antwort der tenant-registry")
}
if ok {
t.Fatal("erwartet false bei fehler (fail-closed)")
}
}
-49
View File
@@ -1,49 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgconn"
)
const exitPermanentDBError = 2
type pinger interface {
Ping(ctx context.Context) error
}
func isPermanentDBError(err error) bool {
var pgErr *pgconn.PgError
if !errors.As(err, &pgErr) {
return false
}
switch pgErr.Code {
case "28000", "28P01", "3D000":
return true
}
return false
}
func waitForDB(ctx context.Context, p pinger, timeout time.Duration, sleep func(time.Duration)) error {
deadline := time.Now().Add(timeout)
delay := time.Second
for {
err := p.Ping(ctx)
if err == nil {
return nil
}
if isPermanentDBError(err) {
return fmt.Errorf("datenbank lehnt Anmeldung oder Datenbank ab (dauerhaft): %w", err)
}
if time.Now().Add(delay).After(deadline) {
return fmt.Errorf("datenbank nach %s nicht erreichbar: %w", timeout, err)
}
sleep(delay)
if delay < 30*time.Second {
delay *= 2
}
}
}
-66
View File
@@ -1,66 +0,0 @@
package main
import (
"context"
"errors"
"testing"
"time"
"github.com/jackc/pgx/v5/pgconn"
)
type fakePinger struct {
errs []error
calls int
}
func (f *fakePinger) Ping(context.Context) error {
i := f.calls
f.calls++
if i < len(f.errs) {
return f.errs[i]
}
return nil
}
func TestWaitForDB_PermanentAuthErrorStopsImmediately(t *testing.T) {
p := &fakePinger{errs: []error{&pgconn.PgError{Code: "28P01"}, nil}}
sleeps := 0
err := waitForDB(context.Background(), p, time.Minute, func(time.Duration) { sleeps++ })
if err == nil || !isPermanentDBError(err) {
t.Fatalf("erwartet dauerhaften fehler, war %v", err)
}
if p.calls != 1 || sleeps != 0 {
t.Fatalf("kein retry erwartet, calls=%d sleeps=%d", p.calls, sleeps)
}
}
func TestWaitForDB_TransientErrorRetriesWithBackoff(t *testing.T) {
p := &fakePinger{errs: []error{errors.New("connection refused"), errors.New("connection refused")}}
var delays []time.Duration
if err := waitForDB(context.Background(), p, time.Minute, func(d time.Duration) { delays = append(delays, d) }); err != nil {
t.Fatal(err)
}
if p.calls != 3 || len(delays) != 2 || delays[1] <= delays[0] {
t.Fatalf("calls=%d delays=%v", p.calls, delays)
}
}
func TestWaitForDB_GivesUpAfterTimeout(t *testing.T) {
p := &fakePinger{errs: []error{errors.New("x"), errors.New("x"), errors.New("x"), errors.New("x")}}
err := waitForDB(context.Background(), p, 0, func(time.Duration) {})
if err == nil || isPermanentDBError(err) {
t.Fatalf("erwartet transienten timeout-fehler, war %v", err)
}
}
func TestIsPermanentDBError(t *testing.T) {
for code, want := range map[string]bool{"28P01": true, "28000": true, "3D000": true, "57P03": false} {
if got := isPermanentDBError(&pgconn.PgError{Code: code}); got != want {
t.Errorf("%s: got %v", code, got)
}
}
if isPermanentDBError(errors.New("connection refused")) {
t.Error("verbindungsfehler ist transient")
}
}
-11
View File
@@ -82,17 +82,6 @@ func main() {
}
defer pool.Close()
dbWait := time.Duration(envInt("NEXARCH_ACCOUNT_DB_STARTUP_TIMEOUT_SECONDS", 120, 0, 3600)) * time.Second
if err := waitForDB(ctx, pool, dbWait, time.Sleep); err != nil {
log.Printf("account-api: %v — DSN in NEXARCH_ACCOUNT_TENANT_DSN (Rolle, Passwort, Datenbank) pruefen", err)
if isPermanentDBError(err) {
pool.Close()
os.Exit(exitPermanentDBError)
}
pool.Close()
os.Exit(1)
}
totpCipher, err := newTOTPCipher()
if err != nil {
log.Fatalf("totp-verschluesselung: %v", err)
+2 -2
View File
@@ -1,6 +1,6 @@
// cor01-tenant-migrate fuehrt COR-01 aus: Uebernahme der freigegebenen
// tenant_migration_map (MAP-02) in Core TEN-01. Einmalig wiederholbar
// gegen dieselbe Quelle (siehe internal/migration/tenant).
// gegen dieselbe Quelle (siehe internal/migrationtenant).
package main
import (
@@ -9,7 +9,7 @@ import (
"os"
"gitea.perlbach24.de/scripte/nexarch/internal/db"
"gitea.perlbach24.de/scripte/nexarch/internal/migration/tenant"
"gitea.perlbach24.de/scripte/nexarch/internal/migrationtenant"
"gitea.perlbach24.de/scripte/nexarch/internal/tenant"
)
+2 -2
View File
@@ -1,6 +1,6 @@
// cor02-user-migrate fuehrt COR-02 aus: Uebernahme der freigegebenen
// user_migration_map in Core IAM-01, bcrypt-Hashes 1:1 ohne Rehash.
// Einmalig wiederholbar gegen dieselbe Quelle (siehe internal/migration/user).
// Einmalig wiederholbar gegen dieselbe Quelle (siehe internal/migrationuser).
package main
import (
@@ -9,7 +9,7 @@ import (
"os"
"gitea.perlbach24.de/scripte/nexarch/internal/db"
"gitea.perlbach24.de/scripte/nexarch/internal/migration/user"
"gitea.perlbach24.de/scripte/nexarch/internal/migrationuser"
"gitea.perlbach24.de/scripte/nexarch/internal/tenant"
)
+2 -2
View File
@@ -4,7 +4,7 @@
// Migrationsschritt.
//
// Benutzer-Quellzahl kommt aus der freigegebenen user_migration_map
// (Registry-DB, siehe internal/migration/user). Dokument-/Nachrichten-
// (Registry-DB, siehe internal/migrationuser). Dokument-/Nachrichten-
// Quellzahlen kommen aus archivdms/archivmail und liegen in diesem
// Repository nicht als Go-Code vor (nur als Source-Interface in
// dms/internal/migrationdocument bzw. mail/internal/messagemigration,
@@ -22,7 +22,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/internal/migration/verify"
"gitea.perlbach24.de/scripte/nexarch/internal/migrationverify"
"gitea.perlbach24.de/scripte/nexarch/internal/tenant"
)
-1
View File
@@ -1 +0,0 @@
2
-26
View File
@@ -1,26 +0,0 @@
# common
Geteiltes Go-Modul für Pakete, die in mehreren NEXARCH-Modulen identisch gebraucht werden (`internal/` ist modulübergreifend nicht sichtbar).
Pakete: `pflichttestgate` (parametrisierte Testpflicht-Logik), `apierror`, `auditclient`, `bearerauth`, `httpclient`, `httpharden`, `loadtest`, `migrate`, `moduleauth`, `policyclient`, `svcauth`, `tenantregistryclient`, `testdb`.
## Einbinden
```
go env -w GOPRIVATE=gitea.perlbach24.de
go get gitea.perlbach24.de/scripte/nexarch/common@latest
```
Build-Orte brauchen Lesezugriff auf Gitea: Read-only-Token in `~/.netrc` (`machine gitea.perlbach24.de login <user> password <token>`).
## Versionen
Tags `common/v0.X.Y` setzt `.gitea/workflows/common-tag.yml` nach jedem Merge in `main`, der `common/` ändert. Module aktualisieren einzeln (`go get ...common@v0.X.Y`). Lokal mit mehreren Modulen gleichzeitig arbeiten: nicht eingechecktes `go.work`.
## Regeln
- Nur Code, der in mindestens zwei Modulen byte-gleich (ohne Kommentare) gebraucht wird; fachlich verschiedene Pakete bleiben modul-lokal.
- Änderungen abwärtskompatibel halten, solange Module nicht gleichzeitig umgestellt werden.
- Jeder PR an `common/` nennt die betroffenen Module (Auswirkungsliste). Breaking Changes nur mit neuer Minor-Version (`v0.2.0`) und Umstellungs-PRs je Modul; die aktuelle Minor steht in `common/MINOR`, der Tag-Workflow setzt bei einer neuen Minor `v0.<minor>.0`.
- Sicherheitskritische Pakete (`svcauth`, `tenantregistryclient`, `auditclient`, `policyclient`, `apierror`, `httpharden`) unterliegen dem Pflichttest-Gate (`cmd/pflichttestgate`, Workflow `common-pflichttest-gate.yml`): Änderung ohne begleitende `_test.go` im selben Package schlägt fehl.
-71
View File
@@ -1,71 +0,0 @@
// Package bearerauth setzt die Identitaet der Modul-APIs aus einem
// verifizierten Core-Modul-Token. Die Handler lesen weiter X-User-Id und
// X-Tenant-Slug; die Middleware ueberschreibt beide aus den Claims, sodass ein
// Client sie nicht frei setzen kann. X-User-Role kommt nur aus dem Token.
package bearerauth
import (
"encoding/json"
"errors"
"net/http"
"strings"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
)
const (
headerUser = "X-User-Id"
headerTenant = "X-Tenant-Slug"
headerRole = "X-User-Role"
)
// Middleware verlangt fuer Anfragen mit Authorization: Bearer ein gueltiges
// Token. Ohne Bearer werden X-User-Id und X-Tenant-Slug entfernt, ausser
// allowHeaderAuth ist gesetzt (Uebergang, bis alle Clients Bearer senden).
func Middleware(v *moduleauth.Verifier, allowHeaderAuth bool, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Del(headerRole)
token, ok := bearer(r)
if !ok {
if !allowHeaderAuth {
r.Header.Del(headerUser)
r.Header.Del(headerTenant)
}
next.ServeHTTP(w, r)
return
}
if v == nil {
writeError(w, http.StatusUnauthorized, "nicht angemeldet")
return
}
claims, err := v.Verify(r.Context(), token)
if err != nil {
if errors.Is(err, moduleauth.ErrInvalidToken) {
writeError(w, http.StatusUnauthorized, "nicht angemeldet")
} else {
writeError(w, http.StatusServiceUnavailable, "identitaetsdienst nicht erreichbar")
}
return
}
r.Header.Set(headerUser, claims.Subject)
r.Header.Set(headerTenant, claims.Tenant)
if claims.Role != "" {
r.Header.Set(headerRole, claims.Role)
}
next.ServeHTTP(w, r)
})
}
func bearer(r *http.Request) (string, bool) {
h := r.Header.Get("Authorization")
if len(h) > 7 && strings.EqualFold(h[:7], "bearer ") {
return strings.TrimSpace(h[7:]), true
}
return "", false
}
func writeError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}
-75
View File
@@ -1,75 +0,0 @@
package bearerauth
import (
"net/http"
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
)
func echo() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(r.Header.Get("X-User-Id") + "|" + r.Header.Get("X-Tenant-Slug") + "|" + r.Header.Get("X-User-Role")))
})
}
func do(h http.Handler, hdr map[string]string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, "/x", nil)
for k, v := range hdr {
req.Header.Set(k, v)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
func TestBearerOverridesClientSuppliedHeaders(t *testing.T) {
v, iss := moduleauth.NewTestVerifierAndIssuer()
h := Middleware(v, false, echo())
rec := do(h, map[string]string{
"Authorization": "Bearer " + iss.Token("u1", "acme", "user", "", ""),
"X-User-Id": "fremd", "X-Tenant-Slug": "fremd",
})
if rec.Code != 200 || rec.Body.String() != "u1|acme|user" {
t.Fatalf("code=%d body=%q", rec.Code, rec.Body.String())
}
}
func TestInvalidBearerIs401(t *testing.T) {
v, _ := moduleauth.NewTestVerifierAndIssuer()
_, other := moduleauth.NewTestVerifierAndIssuer()
h := Middleware(v, true, echo())
if rec := do(h, map[string]string{"Authorization": "Bearer " + other.Token("u1", "acme", "user", "", ""), "X-User-Id": "evil"}); rec.Code != 401 {
t.Fatalf("code=%d", rec.Code)
}
if rec := do(Middleware(nil, true, echo()), map[string]string{"Authorization": "Bearer x.y.z"}); rec.Code != 401 {
t.Fatalf("ohne Verifier code=%d", rec.Code)
}
}
func TestWithoutBearerHeadersStrippedUnlessAllowed(t *testing.T) {
v, _ := moduleauth.NewTestVerifierAndIssuer()
hdr := map[string]string{"X-User-Id": "u9", "X-Tenant-Slug": "t9"}
if rec := do(Middleware(v, false, echo()), hdr); rec.Body.String() != "||" {
t.Fatalf("Header muessen entfernt werden, bekam %q", rec.Body.String())
}
if rec := do(Middleware(v, true, echo()), hdr); rec.Body.String() != "u9|t9|" {
t.Fatalf("Uebergangsmodus muss Header durchlassen, bekam %q", rec.Body.String())
}
}
func TestRoleOnlyFromToken(t *testing.T) {
v, iss := moduleauth.NewTestVerifierAndIssuer()
h := Middleware(v, true, echo())
rec := do(h, map[string]string{
"Authorization": "Bearer " + iss.Token("u1", "acme", "tenant_admin", "", ""),
"X-User-Role": "tenant_admin_fremd",
})
if rec.Body.String() != "u1|acme|tenant_admin" {
t.Fatalf("Rolle muss aus dem Token kommen, bekam %q", rec.Body.String())
}
if rec := do(h, map[string]string{"X-User-Role": "tenant_admin"}); rec.Body.String() != "||" {
t.Fatalf("Rolle ohne Token muss entfernt werden, bekam %q", rec.Body.String())
}
}
-50
View File
@@ -1,50 +0,0 @@
// Command pflichttestgate ist das CI-Gate für common/.
// Aufruf: pflichttestgate < geänderte-Dateien (eine pro Zeile, via stdin)
// Exit-Code 1 mit Liste der Verstöße, wenn ein sicherheitskritisches Package ohne
// begleitende Testdatei geändert wurde.
package main
import (
"bufio"
"fmt"
"os"
"regexp"
"gitea.perlbach24.de/scripte/nexarch/common/pflichttestgate"
)
var sensitive = []*regexp.Regexp{
regexp.MustCompile(`(^|/)common/svcauth/`),
regexp.MustCompile(`(^|/)common/tenantregistryclient/`),
regexp.MustCompile(`(^|/)common/auditclient/`),
regexp.MustCompile(`(^|/)common/policyclient/`),
regexp.MustCompile(`(^|/)common/apierror/`),
regexp.MustCompile(`(^|/)common/httpharden/`),
regexp.MustCompile(`(^|/)common/bearerauth/`),
}
func main() {
var changedFiles []string
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
if line := scanner.Text(); line != "" {
changedFiles = append(changedFiles, line)
}
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "pflichttestgate: Eingabe konnte nicht gelesen werden: %v\n", err)
os.Exit(2)
}
violations := pflichttestgate.CheckDiff(changedFiles, sensitive)
if len(violations) == 0 {
fmt.Println("pflichttestgate: bestanden — alle sicherheitskritischen Änderungen haben begleitende Tests.")
return
}
fmt.Fprintln(os.Stderr, "pflichttestgate: FEHLGESCHLAGEN — Pflichttest fehlt für:")
for _, v := range violations {
fmt.Fprintf(os.Stderr, " - Package %q (Datei %q hat keine begleitende _test.go-Änderung)\n", v.Package, v.ChangedFile)
}
os.Exit(1)
}
-12
View File
@@ -1,15 +1,3 @@
module gitea.perlbach24.de/scripte/nexarch/common
go 1.25.0
toolchain go1.25.13
require github.com/jackc/pgx/v5 v5.9.2
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
)
-26
View File
@@ -1,26 +0,0 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+6 -5
View File
@@ -57,15 +57,16 @@ func TestClientDoesNotFollowRedirects(t *testing.T) {
}
}
// Architekturtest: im Produktivcode von common gibt es keine Aufrufe ueber den
// Standard-Client ohne Zeitlimit. Die Verbraucher-Module pruefen ihren eigenen
// Code selbst.
// Architekturtest: im Produktivcode der Module, die dieses Paket nutzen, gibt
// es keine Aufrufe ueber den Standard-Client ohne Zeitlimit.
func TestNoUnboundedDefaultClientInProductionCode(t *testing.T) {
root, err := filepath.Abs(filepath.Join(".."))
repoRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
scanNoDefaultClient(t, root, root)
for _, mod := range []string{"common", "dms", "mail", "archive"} {
scanNoDefaultClient(t, repoRoot, filepath.Join(repoRoot, mod))
}
}
func scanNoDefaultClient(t *testing.T, repoRoot, root string) {
-247
View File
@@ -1,247 +0,0 @@
// Package migrate implements versioned, revertible SQL migrations (no ORM).
// Jedes Modul benennt seine Fortschrittstabelle selbst (New), damit sich
// Module in derselben Datenbank nicht gegenseitig den Fortschritt
// ueberschreiben (FDN-13).
package migrate
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
)
// Migration ist eine einzelne versionierte Migrationsdatei.
type Migration struct {
Version string // Dateiname ohne .up.sql/.down.sql, z.B. "0001_documents"
UpSQL string
DownSQL string
}
// Load liest alle *.up.sql/*.down.sql-Paare aus dir, sortiert nach
// Dateiname (Akzeptanzkriterium: Migrationen sind versioniert).
func Load(dir string) ([]Migration, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("migrationsverzeichnis %q lesen: %w", dir, err)
}
var versions []string
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".up.sql") {
continue
}
versions = append(versions, strings.TrimSuffix(e.Name(), ".up.sql"))
}
sort.Strings(versions)
migrations := make([]Migration, 0, len(versions))
for _, v := range versions {
up, err := os.ReadFile(filepath.Join(dir, v+".up.sql"))
if err != nil {
return nil, fmt.Errorf("migration %q: up.sql lesen: %w", v, err)
}
down, err := os.ReadFile(filepath.Join(dir, v+".down.sql"))
if err != nil {
return nil, fmt.Errorf("migration %q: down.sql lesen (jede Migration braucht ein Rollback): %w", v, err)
}
migrations = append(migrations, Migration{Version: v, UpSQL: string(up), DownSQL: string(down)})
}
return migrations, nil
}
var tableNamePattern = regexp.MustCompile(`^[a-z_][a-z0-9_]{0,62}$`)
// Migrator fuehrt Migrationen gegen eine modulspezifisch benannte Tabelle aus.
type Migrator struct {
table string
}
// New liefert einen Migrator fuer die Fortschrittstabelle table (z. B.
// "dms_schema_migrations"). Der Name geht unquotiert in SQL und muss daher
// ^[a-z_][a-z0-9_]{0,62}$ entsprechen.
func New(table string) (*Migrator, error) {
if !tableNamePattern.MatchString(table) {
return nil, fmt.Errorf("migrate: ungueltiger tabellenname %q", table)
}
return &Migrator{table: table}, nil
}
// MustNew wie New, panikt bei ungueltigem Namen (fuer Paketvariablen).
func MustNew(table string) *Migrator {
mg, err := New(table)
if err != nil {
panic(err)
}
return mg
}
// Table liefert den Namen der Fortschrittstabelle.
func (mg *Migrator) Table() string { return mg.table }
const legacyTrackingTable = "schema_migrations"
// ensureTrackingTable legt mg.table an. Beim erstmaligen Anlegen wird
// ein vorhandener Bestand aus der frueheren, unqualifizierten
// schema_migrations uebernommen (nur Versionen, die dieses Modul kennt, und
// nur wenn dessen Basisversion dort steht — eine schema_migrations eines
// anderen Moduls oder eines anderen Schemas wird weder gelesen noch
// veraendert). Die alte Tabelle
// bleibt unangetastet.
func (mg *Migrator) ensureTrackingTable(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) error {
tx, err := pool.Begin(ctx)
if err != nil {
return fmt.Errorf("%s anlegen: %w", mg.table, err)
}
defer func() { _ = tx.Rollback(ctx) }()
// Serialisiert gleichzeitige Erstanlage/Uebernahme (mehrere App-Instanzen).
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtext($1))`, mg.table); err != nil {
return fmt.Errorf("%s anlegen: %w", mg.table, err)
}
var existed bool
if err := tx.QueryRow(ctx, `SELECT to_regclass(current_schema() || '.' || $1::text) IS NOT NULL`, mg.table).Scan(&existed); err != nil {
return fmt.Errorf("%s anlegen: %w", mg.table, err)
}
if existed {
return tx.Commit(ctx)
}
if _, err := tx.Exec(ctx, `
CREATE TABLE `+mg.table+` (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
`); err != nil {
return fmt.Errorf("%s anlegen: %w", mg.table, err)
}
if len(migrations) > 0 {
var legacy bool
if err := tx.QueryRow(ctx, `SELECT to_regclass(current_schema() || '.' || $1::text) IS NOT NULL`, legacyTrackingTable).Scan(&legacy); err != nil {
return fmt.Errorf("%s pruefen: %w", legacyTrackingTable, err)
}
if legacy {
versions := make([]string, len(migrations))
for i, m := range migrations {
versions[i] = m.Version
}
var hasBase bool
if err := tx.QueryRow(ctx,
`SELECT EXISTS (SELECT 1 FROM `+legacyTrackingTable+` WHERE version = $1)`, versions[0],
).Scan(&hasBase); err != nil {
return fmt.Errorf("%s lesen: %w", legacyTrackingTable, err)
}
if hasBase {
if _, err := tx.Exec(ctx, `
INSERT INTO `+mg.table+` (version, applied_at)
SELECT version, applied_at FROM `+legacyTrackingTable+` WHERE version = ANY($1)
`, versions); err != nil {
return fmt.Errorf("bestand aus %s uebernehmen: %w", legacyTrackingTable, err)
}
}
}
}
return tx.Commit(ctx)
}
func (mg *Migrator) appliedVersions(ctx context.Context, pool *pgxpool.Pool) (map[string]bool, error) {
rows, err := pool.Query(ctx, `SELECT version FROM `+mg.table)
if err != nil {
return nil, fmt.Errorf("angewendete migrationen lesen: %w", err)
}
defer rows.Close()
applied := map[string]bool{}
for rows.Next() {
var v string
if err := rows.Scan(&v); err != nil {
return nil, fmt.Errorf("migrationsversion lesen: %w", err)
}
applied[v] = true
}
return applied, rows.Err()
}
// Up wendet alle noch nicht angewendeten Migrationen in Reihenfolge an
// (Akzeptanzkriterium 2: vorwaerts ausfuehrbar) — bereits angewendete
// werden uebersprungen, damit Up auf einer leeren UND auf einer bestehenden
// DB funktioniert (Pruefung 1).
func (mg *Migrator) Up(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) (applied []string, err error) {
if err := mg.ensureTrackingTable(ctx, pool, migrations); err != nil {
return nil, err
}
already, err := mg.appliedVersions(ctx, pool)
if err != nil {
return nil, err
}
for _, m := range migrations {
if already[m.Version] {
continue
}
tx, err := pool.Begin(ctx)
if err != nil {
return applied, fmt.Errorf("transaktion fuer %q starten: %w", m.Version, err)
}
if _, err := tx.Exec(ctx, m.UpSQL); err != nil {
_ = tx.Rollback(ctx)
return applied, fmt.Errorf("migration %q anwenden: %w", m.Version, err)
}
if _, err := tx.Exec(ctx, `INSERT INTO `+mg.table+` (version) VALUES ($1)`, m.Version); err != nil {
_ = tx.Rollback(ctx)
return applied, fmt.Errorf("migration %q als angewendet markieren: %w", m.Version, err)
}
if err := tx.Commit(ctx); err != nil {
return applied, fmt.Errorf("migration %q committen: %w", m.Version, err)
}
applied = append(applied, m.Version)
}
return applied, nil
}
// DownOne macht die zuletzt angewendete Migration rueckgaengig
// (Akzeptanzkriterium 2: rueckwaerts ausfuehrbar) und liefert deren Version,
// oder "" falls keine Migration angewendet war.
func (mg *Migrator) DownOne(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) (version string, err error) {
if err := mg.ensureTrackingTable(ctx, pool, migrations); err != nil {
return "", err
}
already, err := mg.appliedVersions(ctx, pool)
if err != nil {
return "", err
}
var last *Migration
for i := len(migrations) - 1; i >= 0; i-- {
if already[migrations[i].Version] {
last = &migrations[i]
break
}
}
if last == nil {
return "", nil
}
tx, err := pool.Begin(ctx)
if err != nil {
return "", fmt.Errorf("transaktion fuer rollback von %q starten: %w", last.Version, err)
}
if _, err := tx.Exec(ctx, last.DownSQL); err != nil {
_ = tx.Rollback(ctx)
return "", fmt.Errorf("migration %q zurueckrollen: %w", last.Version, err)
}
if _, err := tx.Exec(ctx, `DELETE FROM `+mg.table+` WHERE version = $1`, last.Version); err != nil {
_ = tx.Rollback(ctx)
return "", fmt.Errorf("migration %q aus %s entfernen: %w", last.Version, mg.table, err)
}
if err := tx.Commit(ctx); err != nil {
return "", fmt.Errorf("rollback von %q committen: %w", last.Version, err)
}
return last.Version, nil
}
-98
View File
@@ -1,98 +0,0 @@
package migrate
import (
"context"
"os"
"path/filepath"
"testing"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
)
func TestNewValidatesTableName(t *testing.T) {
for _, ok := range []string{"dms_schema_migrations", "_x", "a1"} {
if _, err := New(ok); err != nil {
t.Errorf("New(%q): %v", ok, err)
}
}
for _, bad := range []string{"", "1abc", "Dms", "a-b", "a;drop table x", "a b", "schema.table"} {
if _, err := New(bad); err == nil {
t.Errorf("New(%q) sollte scheitern", bad)
}
}
}
func writeMigrations(t *testing.T, files map[string]string) []Migration {
t.Helper()
dir := t.TempDir()
for name, body := range files {
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
migs, err := Load(dir)
if err != nil {
t.Fatal(err)
}
return migs
}
func TestUpDownAndSeparateTrackingTables(t *testing.T) {
ctx := context.Background()
pool := testdb.Pool(t, testdb.AdminDSN(t))
migs := writeMigrations(t, map[string]string{
"0001_a.up.sql": "CREATE TABLE mig_a (id int)",
"0001_a.down.sql": "DROP TABLE mig_a",
"0002_b.up.sql": "CREATE TABLE mig_b (id int)",
"0002_b.down.sql": "DROP TABLE mig_b",
})
one := MustNew("one_schema_migrations")
two := MustNew("two_schema_migrations")
applied, err := one.Up(ctx, pool, migs)
if err != nil || len(applied) != 2 {
t.Fatalf("Up: applied=%v err=%v", applied, err)
}
again, err := one.Up(ctx, pool, migs)
if err != nil || len(again) != 0 {
t.Fatalf("zweites Up soll nichts anwenden: applied=%v err=%v", again, err)
}
var n int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM two_schema_migrations`).Scan(&n); err == nil {
t.Fatal("two_schema_migrations darf vor dem ersten Lauf von two nicht existieren")
}
if _, err := two.DownOne(ctx, pool, migs); err != nil {
t.Fatalf("two.DownOne: %v", err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM one_schema_migrations`).Scan(&n); err != nil || n != 2 {
t.Fatalf("Fortschritt von one muss unberuehrt sein: n=%d err=%v", n, err)
}
v, err := one.DownOne(ctx, pool, migs)
if err != nil || v != "0002_b" {
t.Fatalf("DownOne: v=%q err=%v", v, err)
}
}
func TestLegacyTrackingTableIsAdopted(t *testing.T) {
ctx := context.Background()
pool := testdb.Pool(t, testdb.AdminDSN(t))
migs := writeMigrations(t, map[string]string{
"0001_a.up.sql": "CREATE TABLE mig_leg (id int)",
"0001_a.down.sql": "DROP TABLE mig_leg",
})
for _, q := range []string{
`CREATE TABLE schema_migrations (version TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT now())`,
`INSERT INTO schema_migrations (version) VALUES ('0001_a')`,
`CREATE TABLE mig_leg (id int)`,
} {
if _, err := pool.Exec(ctx, q); err != nil {
t.Fatal(err)
}
}
applied, err := MustNew("leg_schema_migrations").Up(ctx, pool, migs)
if err != nil || len(applied) != 0 {
t.Fatalf("uebernommene Version darf nicht erneut laufen: applied=%v err=%v", applied, err)
}
}
-53
View File
@@ -1,53 +0,0 @@
// Package pflichttestgate erzwingt: jede geänderte Go-Datei in einem sicherheitskritischen
// Bereich muss von einer geänderten oder neuen _test.go-Datei im selben Package begleitet sein.
// Die Bereiche (Pfadmuster) liefert der Aufrufer, die Logik ist für alle Module dieselbe.
package pflichttestgate
import (
"path"
"regexp"
"strings"
)
type Violation struct {
Package string
ChangedFile string
}
func IsSensitive(file string, patterns []*regexp.Regexp) bool {
if !strings.HasSuffix(file, ".go") || strings.HasSuffix(file, "_test.go") {
return false
}
for _, re := range patterns {
if re.MatchString(file) {
return true
}
}
return false
}
func CheckDiff(changedFiles []string, patterns []*regexp.Regexp) []Violation {
sensitiveByPkg := map[string]string{}
testTouchedPkgs := map[string]bool{}
for _, f := range changedFiles {
pkg := path.Dir(f)
if strings.HasSuffix(f, "_test.go") {
testTouchedPkgs[pkg] = true
continue
}
if IsSensitive(f, patterns) {
if _, seen := sensitiveByPkg[pkg]; !seen {
sensitiveByPkg[pkg] = f
}
}
}
var violations []Violation
for pkg, file := range sensitiveByPkg {
if !testTouchedPkgs[pkg] {
violations = append(violations, Violation{Package: pkg, ChangedFile: file})
}
}
return violations
}
-54
View File
@@ -1,54 +0,0 @@
package pflichttestgate
import (
"regexp"
"testing"
)
var testPatterns = []*regexp.Regexp{
regexp.MustCompile(`(^|/)common/svcauth/`),
regexp.MustCompile(`(^|/)common/tenantregistryclient/`),
}
func TestCheckDiff_BlocksSensitiveChangeWithoutTest(t *testing.T) {
changed := []string{"common/svcauth/svcauth.go", "common/loadtest/loadtest.go"}
violations := CheckDiff(changed, testPatterns)
if len(violations) != 1 {
t.Fatalf("erwartet 1 Verstoß, bekommen %d: %+v", len(violations), violations)
}
if violations[0].Package != "common/svcauth" || violations[0].ChangedFile != "common/svcauth/svcauth.go" {
t.Errorf("unerwarteter Verstoß: %+v", violations[0])
}
}
func TestCheckDiff_PassesWhenTestFileAccompaniesChange(t *testing.T) {
changed := []string{"common/svcauth/svcauth.go", "common/svcauth/svcauth_test.go"}
if v := CheckDiff(changed, testPatterns); len(v) != 0 {
t.Fatalf("erwartet keinen Verstoß, bekommen %+v", v)
}
}
func TestCheckDiff_TestInOtherPackageDoesNotCount(t *testing.T) {
changed := []string{"common/svcauth/svcauth.go", "common/tenantregistryclient/tenantregistryclient_test.go"}
if v := CheckDiff(changed, testPatterns); len(v) != 1 {
t.Fatalf("erwartet 1 Verstoß, bekommen %+v", v)
}
}
func TestCheckDiff_IgnoresNonGoAndUnrelated(t *testing.T) {
changed := []string{"common/svcauth/README.md", "common/httpclient/httpclient.go"}
if v := CheckDiff(changed, testPatterns); len(v) != 0 {
t.Fatalf("erwartet keinen Verstoß, bekommen %+v", v)
}
}
func TestCheckDiff_NoPatternsMeansNoSensitiveAreas(t *testing.T) {
if v := CheckDiff([]string{"common/svcauth/svcauth.go"}, nil); len(v) != 0 {
t.Fatalf("ohne Muster darf nichts als sensibel gelten, bekommen %+v", v)
}
}
-11
View File
@@ -1,11 +0,0 @@
package testdb
import "testing"
func TestAdminDSNGesetzt(t *testing.T) {
t.Setenv("TEST_ADMIN_DSN", "postgres://x")
t.Setenv("REQUIRE_DB", "1")
if got := AdminDSN(t); got != "postgres://x" {
t.Fatalf("dsn = %q", got)
}
}
-13
View File
@@ -1,13 +0,0 @@
# connect-kanban – Dev Log
## 2026-09-12 21:36 – 21:39 (2m)
**Beschreibung:** Claude Code Session
**Projekt:** flutter_app
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
Keine Änderungen ermittelbar.
---
-93
View File
@@ -1,93 +0,0 @@
# Connect-Kanban – lokal abarbeiten
32 Arbeitskacheln · 7 Epics · 85 Personentage · 7 Wellen (inkl. Welle 7: Threat-Model-Nachtrag 2026-09-12 — ENG-07, ENG-08).
Hinweis: Dieses Board baut vollständig auf NEXARCH Core auf (REST-Grundgerüst, Rate-Limiting, OpenAPI, Webhook-Zustellung, Token-Verwaltung, Audit) — keine eigene API-Infrastruktur. LDAP/SSO sind Core-Sache (IAM-05/IAM-06), Scanner-Anbindung ist DMS/Mail-Sache, hier ausschließlich externe Drittsystem-Konnektoren.
Hinweis: Alle Oberflächen-Tickets in diesem Board bauen auf der zentralen UI-Shell & Design-System aus NEXARCH Core auf (siehe ../core-kanban/, `SHL-01`) — kein eigenes Design-System in diesem Board.
Stand: 2026-08-26
## Weg 1 – Sofort loslegen (empfohlen)
`kanban.html` per Doppelklick im Browser öffnen. Kein Server, kein Internet, keine Installation.
- Kacheln per Drag & Drop zwischen Backlog → Bereit → In Arbeit → Prüfung → Fertig ziehen
- Der Stand wird im Browser gespeichert (localStorage) und bleibt nach dem Schließen erhalten
- Filter nach Epic, Rolle, Welle, Volltext sowie „Nur startbare“ (alle Vorgänger auf Fertig)
- Klick auf eine Kachel öffnet Details inkl. vollem Bau-Prompt und „Prompt kopieren“
- „Stand sichern“ exportiert den aktuellen Status als CSV
Hinweis: Der Stand hängt an Browser und Profil. Für Teamarbeit Weg 2 oder 3 nutzen.
## Weg 2 – Import in ein vorhandenes Werkzeug
`board.csv` (UTF-8 mit BOM, Komma-getrennt) importieren:
| Werkzeug | Weg |
| --- | --- |
| Trello | Board → Menü → Mehr → CSV importieren (Power-Up) |
| Jira | Einstellungen → System → Externes Importieren → CSV, Spalte „Status“ auf den Workflow mappen |
| GitHub Projects | Projekt → … → Import, oder per `gh` CLI aus `board.json` |
| Notion | Datenbank → … → Merge with CSV |
| Linear / Asana / ClickUp | jeweils CSV-Import, Spalten wie oben zuordnen |
Spalten: Code, Titel, Epic, Rolle, Komplexitaet, Aufwand_PT, Welle, Status, Technik,
Beschreibung, Haengt_ab_von, Schaltet_frei, Akzeptanzkriterien, Pruefungen.
## Weg 3 – Dateibasiert in der Entwicklungsumgebung
Der Ordner `tickets/` enthält für jede Kachel eine Markdown-Datei (`ENG-01.md` …).
Jede Datei ist ein vollständiger Bau-Prompt – Auftrag, Produkt-DNA, Ausgangslage,
Akzeptanzkriterien, Prüfungen und Abschlussregeln.
- **Obsidian**: Ordner als Vault öffnen, Community-Plugin „Kanban“ installieren, Spalten anlegen und die Notizen einsortieren.
- **VS Code**: Ordner öffnen, Erweiterung „Todo Tree“ oder „Markdown Kanban“ nutzen.
- **Coding-Assistent**: Dateiöffnen, Inhalt kopieren, als Auftrag übergeben. Ein Prompt = eine Kachel = ein Branch.
## Weg 4 – Eigenes Board im Netzwerk (Team)
Selbst gehostet, jeweils mit CSV-Import:
```bash
# Focalboard
docker run -d -p 8000:8000 --name focalboard mattermost/focalboard
# Planka
docker run -d -p 3010:1337 --name planka ghcr.io/plankanban/planka:latest
# Vikunja
docker run -d -p 3456:3456 --name vikunja vikunja/vikunja
```
Danach `board.csv` importieren.
## Reihenfolge der Arbeit
Die Wellen geben an, was parallel laufen darf. Eine Kachel ist startbar,
sobald alle Kacheln unter „Hängt ab von“ auf Fertig stehen.
- **Welle 1** (2 Kacheln): ENG-01, QA-01
- **Welle 2** (10 Kacheln): ENG-02, ENG-03, ENG-04, ENG-06, ERP-01, ERP-02, DAT-01, DMS2-01, DMS2-03, API-03
- **Welle 3** (10 Kacheln): ENG-05, ERP-03, ERP-04, DAT-02, M365-01, DMS2-02, API-01, QA-02, QA-04, QA-06
- **Welle 4** (4 Kacheln): DAT-03, M365-02, M365-03, API-02
- **Welle 5** (1 Kachel): QA-03
- **Welle 6** (1 Kachel): QA-05
Kritischer Pfad (16 PT): ENG-01 → DAT-01 → DAT-02 → DAT-03 → QA-03 → QA-05
## Produkt-DNA
### Übernehmen
- **Zapier/n8n**: Connector-Plugin-Modell mit austauschbaren Adaptern statt fest verdrahteter Drittsystem-Logik – Kernidee dieses Boards, bewusst schlanker ohne visuellen Workflow-Builder.
- **Airbyte**: Sync-Job-Konzept mit Retry und Fehlerbehandlung als Vorbild für robuste, wiederholbare Imports/Exports.
### Vermeiden
- **Proprietäre, geschlossene iPaaS-Plattformen mit Vendor-Lock.**: Jeder Connector ist offen dokumentiert und ohne Herstellerwerkzeug ersetzbar – was die Oberfläche kann, kann auch die offene Schnittstelle.
- **Zapier/n8n – voller grafischer Workflow-Builder.**: Nur die Kernidee austauschbarer Adapter, kein eigenes Prozessmodellierungswerkzeug – das übernimmt bei Bedarf NEXARCH Workflow.
## Arbeitsregeln
Je Kachel ein eigener Branch, schrittweise committen, Branch pushen, dann anhalten.
Kein Merge und kein Deploy ohne ausdrückliche Freigabe.
Code-Struktur: Hauptdateien/Entry-Points klein halten, Fachlogik in eigene Module/Dateien auslagern. Vor dem Anlegen einer neuen Funktions-/Modul-Datei prüfen, ob dieselbe Funktionalität nicht schon in einem anderen Modul (auch modulübergreifend, z. B. Core) existiert — keine doppelten Funktionsdateien, ein Modul/Board referenziert das andere extern statt Code zu kopieren (gleiches Prinzip wie bei den Konsolidierungen IAM/Archive/Workflow/AI/API/UI-Shell, siehe nexarch-state.json).
-203
View File
@@ -1,203 +0,0 @@
Code,Titel,Epic,Rolle,Komplexitaet,Aufwand_PT,Welle,Status,Technik,Beschreibung,Haengt_ab_von,Schaltet_frei,Akzeptanzkriterien,Pruefungen
ENG-01,Connector-Plugin-Modell,🔌 Connector-Engine & Grundgerüst,Backend,Mittel,3,1,Fertig,"Plugin-Schnittstelle, Go-Interface, Registrierung","Plugin-Schnittstelle, über die einzelne Drittsystem-Connectoren (ERP, CRM, DATEV, M365, andere DMS, Backup-Ziele) registriert werden, ohne dass die Engine connectorspezifisches Wissen braucht.",,ENG-02 ENG-03 ENG-04 ENG-06 ERP-01 ERP-02 DAT-01 DMS2-01 DMS2-03 API-03,"Neuer Connector lässt sich ohne Codeänderung an der Engine registrieren
Fehlende Registrierung führt zu klarer Fehlermeldung statt Absturz
Connector-Metadaten (Name, Version, unterstützte Operationen) sind abrufbar","Zwei Testconnectoren parallel registriert und einzeln ansprechbar
Deregistrierung entfernt Connector vollständig
Fehlerhafter Connector wird beim Laden abgewiesen, Engine bleibt lauffähig"
ENG-02,"Verbindungs-Credential-Verwaltung (OAuth/API-Keys, verschlüsselt)",🔌 Connector-Engine & Grundgerüst,Backend,Mittel,3,2,Fertig,"Verschlüsselte Speicherung, OAuth2-Client, PostgreSQL","Verwaltung von Zugangsdaten für Drittsysteme (OAuth-Tokens, API-Keys), verschlüsselt gespeichert und je Connector-Instanz zugeordnet.",ENG-01,M365-01 API-01 QA-04 QA-07 QA-02 ENG-07,"Zugangsdaten sind ausschließlich verschlüsselt gespeichert
OAuth-Token-Erneuerung läuft automatisiert ohne Nutzereingriff
Widerruf einer Verbindung entfernt zugehörige Zugangsdaten vollständig","Datenbankinhalt der Zugangsdaten-Tabelle ist ohne Schlüssel nicht lesbar
Ablaufender OAuth-Token wird vor Ablauf erneuert
Widerruf im Test entfernt Zugangsdaten nachweislich"
ENG-03,Sync-Job-Verwaltung (periodische Imports/Exports),🔌 Connector-Engine & Grundgerüst,Backend,Mittel,3,2,Fertig,"Postgres-Jobqueue, periodische Ausführung","Verwaltung periodisch ausgeführter Sync-Jobs (Import/Export) je Connector-Instanz: Zeitplan, letzter Lauf, nächster Lauf.",ENG-01,ENG-05 QA-02 ENG-08,"Sync-Job ist mit Zeitplan konfigurierbar
Laufender Job überlebt einen Dienst-Neustart
Manuelles Anstoßen eines Jobs außerhalb des Zeitplans möglich","Zeitplan im Test korrekt eingehalten
Neustart des Dienstes mit laufendem Job geprüft
Manueller Anstoß erzeugt genau einen zusätzlichen Lauf"
ENG-04,Feld-Mapping-Konfiguration (NEXARCH ↔ Drittsystem),🔌 Connector-Engine & Grundgerüst,Backend,Mittel,3,2,Fertig,"Mapping-Konfiguration, Schema-Validierung",Konfigurierbares Feld-Mapping zwischen NEXARCH-Datenmodell und Drittsystem-Feldern je Connector-Instanz.,ENG-01,API-01 QA-02,"Feld-Mapping ist ohne Codeänderung konfigurierbar
Ungültiges Mapping wird beim Speichern abgewiesen
Pflichtfeld ohne Zuordnung wird vor Ausführung gemeldet","Ungültiges Mapping im Test abgewiesen
Fehlendes Pflichtfeld-Mapping verhindert Sync-Start
Geändertes Mapping wirkt erst auf nachfolgende Läufe"
ENG-05,Fehlerbehandlung & Retry bei Drittsystem-Ausfällen,🔌 Connector-Engine & Grundgerüst,Backend,Mittel,3,3,Fertig,"Retry mit Backoff, Fehlerprotokoll, Postgres-Jobqueue","Fehlerbehandlung für Sync-Jobs bei Drittsystem-Ausfällen: Retry mit Backoff, Fehlerprotokoll, Abbruchgrenze.",ENG-03,API-02 QA-02 ENG-08,"Fehlgeschlagener Sync-Job wird mit Backoff automatisch wiederholt
Dauerhafter Ausfall führt nach Grenzwert zu klarem Fehlerstatus statt Endlosschleife
Fehlerprotokoll je Lauf ist einsehbar","Simulierter Drittsystem-Ausfall löst Retry mit Backoff aus
Abbruchgrenze im Test erreicht und Status korrekt gesetzt
Fehlerprotokoll enthält Ursache und Zeitpunkt"
ENG-06,Connector-Audit-Kopplung (Core AUD),🔌 Connector-Engine & Grundgerüst,Backend,Einfach,2,2,Fertig,Audit-Ereignis-Kopplung,"Kopplung aller Connector-Aktionen (Verbindung angelegt, Sync ausgeführt, Zugangsdaten geändert) an das zentrale Core-Audit-Log.",ENG-01,QA-02,"Jede sicherheitsrelevante Connector-Aktion erzeugt einen Audit-Eintrag im Core-Audit-Log
Audit-Eintrag enthält Person, Zeitpunkt und betroffenen Connector
Fehlende Audit-Kopplung verhindert die Aktion nicht, wird aber protokolliert","Verbindung anlegen erzeugt nachweisbaren Audit-Eintrag
Zugangsdaten ändern erzeugt nachweisbaren Audit-Eintrag
Ausfall des Audit-Dienstes blockiert Connector-Betrieb nicht"
ERP-01,ERP-Connector-Grundgerüst,📈 ERP/CRM-Anbindung,Backend,Mittel,3,2,Fertig,"ERP-Connector, REST/SOAP-Client","Connector-Grundgerüst für die Anbindung gängiger ERP-Systeme: Verbindungsaufbau, Basisoperationen (Lesen/Schreiben von Stammdaten und Belegen).",ENG-01,ERP-03 ERP-04,"Verbindung zu einem Test-ERP-System lässt sich aufbauen und prüfen
Basisoperationen (Lesen/Schreiben) sind über das Connector-Plugin-Modell nutzbar
Verbindungsfehler wird klar gemeldet statt stillem Abbruch","Verbindungsaufbau gegen Test-ERP-System erfolgreich geprüft
Lese- und Schreiboperation im Test erfolgreich
Simulierter Verbindungsfehler wird korrekt gemeldet"
ERP-02,CRM-Connector-Grundgerüst,📈 ERP/CRM-Anbindung,Backend,Mittel,3,2,Fertig,"CRM-Connector, REST-Client","Connector-Grundgerüst für die Anbindung gängiger CRM-Systeme: Verbindungsaufbau, Basisoperationen für Kontakte/Kunden.",ENG-01,ERP-03 QA-03,"Verbindung zu einem Test-CRM-System lässt sich aufbauen und prüfen
Basisoperationen (Lesen/Schreiben) sind über das Connector-Plugin-Modell nutzbar
Verbindungsfehler wird klar gemeldet statt stillem Abbruch","Verbindungsaufbau gegen Test-CRM-System erfolgreich geprüft
Lese- und Schreiboperation im Test erfolgreich
Simulierter Verbindungsfehler wird korrekt gemeldet"
ERP-03,Stammdaten-Abgleich (Kunden/Lieferanten),📈 ERP/CRM-Anbindung,Backend,Mittel,3,3,Fertig,"Sync-Logik, Konfliktbehandlung",Automatisierter Abgleich von Kunden- und Lieferantenstammdaten zwischen NEXARCH und angebundenem ERP/CRM.,ERP-01 ERP-02,QA-03,"Neuer Stammdatensatz im Drittsystem erscheint nach Sync in NEXARCH
Änderungskonflikt wird erkannt und nicht stillschweigend überschrieben
Abgleich läuft nach Zeitplan über die Sync-Job-Verwaltung","Neuanlage im Test-ERP nach Sync in NEXARCH sichtbar
Gleichzeitige Änderung auf beiden Seiten erzeugt erkennbaren Konflikt statt Datenverlust
Zeitplan-gesteuerter Lauf im Test erfolgreich"
ERP-04,Beleg-/Dokumentexport an ERP,📈 ERP/CRM-Anbindung,Backend,Mittel,3,3,Fertig,"Dokumentexport, Feld-Mapping",Export von Belegen/Dokumenten aus NEXARCH an das angebundene ERP-System inklusive Metadaten.,ERP-01,QA-03,"Beleg lässt sich mit zugeordneten Metadaten an das ERP exportieren
Fehlgeschlagener Export wird über die Fehlerbehandlung erneut versucht
Bereits exportierter Beleg wird nicht doppelt übertragen","Testbeleg im ERP nach Export mit korrekten Metadaten auffindbar
Simulierter Exportfehler löst Retry aus
Doppelversand im Test ausgeschlossen"
DAT-01,DATEV-Format-Export (Belege/Buchungsstapel),🧾 DATEV-Anbindung,Backend,Mittel,3,2,Fertig,"DATEV-Exportformat, Buchungsstapel-Struktur",Export von Belegen und Buchungsstapeln im DATEV-Format aus NEXARCH-Dokumenten und -Metadaten.,ENG-01 RET-05,DAT-02 DAT-03,"Export erzeugt gültige DATEV-Exportdatei aus Testbelegen
Pflichtfelder des DATEV-Formats werden vollständig befüllt oder der Export klar abgewiesen
Exportierter Buchungsstapel ist eindeutig einem Zeitraum zugeordnet
Exportiertes Objekt (Buchungsstapel) wird bei Archive RET-05 mit der GoBD-Aufbewahrungsklasse registriert, statt unregistriert im Dateisystem zu liegen","Erzeugte Exportdatei besteht Formatprüfung gegen DATEV-Spezifikation
Fehlendes Pflichtfeld führt zu klarer Abweisung statt fehlerhaftem Export
Zeitraumzuordnung im Test korrekt
Export eines Testbuchungsstapels erzeugt nachweisbar eine Registrierung bei Archive RET-05"
DAT-02,DATEV-Konnektor (Schnittstellen-Protokoll),🧾 DATEV-Anbindung,Backend,Komplex,4,3,Fertig,"DATEV-Schnittstellenprotokoll, Übertragungslogik","Konnektor, der den Export im DATEV-Format über das offizielle Schnittstellenprotokoll an DATEV-Systeme überträgt.",DAT-01,DAT-03,"Übertragung an ein Test-DATEV-Zielsystem gelingt protokollkonform
Fehlgeschlagene Übertragung wird über die Fehlerbehandlung erneut versucht
Übertragungsstatus ist je Lauf nachvollziehbar","Protokollkonforme Übertragung im Test gegen Referenzsystem geprüft
Simulierter Übertragungsfehler löst Retry aus
Übertragungsstatus im Test korrekt und nachvollziehbar"
DAT-03,DATEV-Export-Oberfläche & Protokoll,🧾 DATEV-Anbindung,Frontend,Einfach,2,4,Fertig,"React/Next.js-Oberfläche, Exportprotokoll-Ansicht",Oberfläche zum Anstoßen von DATEV-Exporten sowie Einsicht in Exportprotokolle und Übertragungsstatus.,DAT-01 DAT-02 SHL-01,QA-03,"Export lässt sich über die Oberfläche mit Zeitraum-Auswahl anstoßen
Exportprotokoll ist je Lauf einsehbar
Fehlgeschlagener Export ist in der Oberfläche klar erkennbar","Export-Anstoß im Test aus der Oberfläche erfolgreich
Protokollansicht zeigt reale Testläufe korrekt an
Fehlgeschlagener Testlauf ist visuell klar unterscheidbar"
M365-01,Microsoft-Graph-API-Anbindung (Auth/Basis),☁️ Microsoft 365-Anbindung,Backend,Mittel,3,3,Fertig,"Microsoft Graph API, OAuth2",Basis-Anbindung an Microsoft Graph: Authentifizierung über OAuth2 und grundlegende API-Aufrufe als Voraussetzung für SharePoint/OneDrive- und Outlook-Anbindung.,ENG-02,M365-02 M365-03,"Authentifizierung gegen Microsoft Graph im Test erfolgreich
Zugangsdaten werden über die Connect-Credential-Verwaltung gespeichert
Abgelaufene Berechtigung wird erkannt und klar gemeldet","OAuth2-Anmeldung im Test gegen Microsoft-Testmandant erfolgreich
Testaufruf einer Graph-API-Basisoperation erfolgreich
Simulierter Berechtigungsablauf wird korrekt erkannt und gemeldet"
M365-02,SharePoint/OneDrive-Dokumentabgleich,☁️ Microsoft 365-Anbindung,Backend,Mittel,3,4,Fertig,"SharePoint/OneDrive-API, Sync-Logik",Abgleich von Dokumenten zwischen NEXARCH und SharePoint/OneDrive über die Microsoft-Graph-Anbindung.,M365-01,QA-03,"Dokument aus SharePoint/OneDrive erscheint nach Sync in NEXARCH mit Metadaten
Änderungskonflikt wird erkannt und nicht stillschweigend überschrieben
Abgleich läuft nach Zeitplan über die Sync-Job-Verwaltung","Testdokument aus SharePoint nach Sync in NEXARCH korrekt vorhanden
Gleichzeitige Änderung auf beiden Seiten erzeugt erkennbaren Konflikt
Zeitplan-gesteuerter Lauf im Test erfolgreich"
M365-03,Outlook-Kalender-Kopplung (Fristen/Wiedervorlage),☁️ Microsoft 365-Anbindung,Backend,Mittel,3,4,Fertig,"Outlook-Kalender-API, Fristen-Kopplung",Kopplung von NEXARCH-Fristen und Wiedervorlagen mit dem Outlook-Kalender über die Microsoft-Graph-Anbindung.,M365-01,QA-03,"Frist/Wiedervorlage erscheint als Kalendereintrag in Outlook
Änderung der Frist in NEXARCH aktualisiert den Kalendereintrag
Gelöschte Frist entfernt den zugehörigen Kalendereintrag","Testfrist im Test-Outlook-Kalender korrekt sichtbar
Friständerung im Test korrekt im Kalender nachgezogen
Löschung im Test entfernt Kalendereintrag vollständig"
DMS2-01,"Standardisiertes Export-/Import-Format (offen, dokumentiert)",🔄 Andere-DMS-Interoperabilität,Backend,Mittel,3,2,Fertig,"Offenes Austauschformat, Schema-Dokumentation",Offen dokumentiertes Export-/Import-Format für Dokumente und Metadaten zur Interoperabilität mit anderen DMS-Systemen.,ENG-01,DMS2-02 QA-03,"Format ist vollständig dokumentiert und ohne NEXARCH-Werkzeug lesbar
Export enthält Dokument, Metadaten und Versionsbezug
Format-Validierung weist fehlerhafte Dateien ab","Exportierte Testdatei extern ohne NEXARCH-Werkzeug lesbar geprüft
Enthaltene Metadaten im Test vollständig und korrekt
Fehlerhafte Testdatei wird bei Validierung abgewiesen"
DMS2-02,Import-Assistent für Drittsystem-Exporte,🔄 Andere-DMS-Interoperabilität,Fullstack,Mittel,3,3,Fertig,"Import-Assistent mit Mapping-Vorschau-Oberfläche (Next.js/React), Backend-Parser für Drittsystem-Exportformate","Assistent, der Exporte aus gängigen Drittsystem-DMS einliest, Feld-Mapping vorschlägt und den Import durchführt.",DMS2-01 SHL-01,QA-03,"Testexport eines gängigen Drittsystems lässt sich einlesen
Feld-Mapping wird vorgeschlagen und ist vor Import änderbar
Import-Ergebnis mit Erfolgs-/Fehlerzahl ist einsehbar
Mapping-Vorschau ist eine bedienbare Oberfläche (nicht nur eine API-Antwort), in der Feldzuordnungen vor dem Import angepasst werden können","Testexport eines Drittsystems im Test erfolgreich eingelesen
Manuelle Mapping-Korrektur im Test übernommen
Import-Protokoll zeigt korrekte Erfolgs-/Fehlerzahl
Mapping-Vorschau im Browser bedient: Zuordnung geändert, Import mit geänderter Zuordnung ausgeführt"
DMS2-03,Externe-Backup-Ziel-Anbindung,🔄 Andere-DMS-Interoperabilität,Backend,Mittel,3,2,Fertig,"Backup-Ziel-Connector, verschlüsselte Übertragung",Anbindung externer Backup-Ziele (z. B. S3-kompatibler Speicher) für den verschlüsselten Export von Dokumentbeständen.,ENG-01,QA-03,"Backup-Ziel lässt sich über das Connector-Plugin-Modell einrichten
Export an das Backup-Ziel erfolgt verschlüsselt
Fehlgeschlagener Backup-Export wird über die Fehlerbehandlung erneut versucht","Verbindungsaufbau zu Test-Backup-Ziel im Test erfolgreich
Übertragung im Test nachweislich verschlüsselt
Simulierter Übertragungsfehler löst Retry aus"
API-01,Connect-Konfigurationsoberfläche (Connectoren einrichten),🖥️ Connect-Oberfläche,Frontend,Mittel,3,3,Fertig,"Next.js/React, Connector-Einrichtungsformulare","Oberfläche zum Einrichten und Konfigurieren von Connectoren: Verbindung anlegen, Zugangsdaten hinterlegen, Feld-Mapping bearbeiten.",ENG-02 ENG-04 SHL-01,,"Connector lässt sich vollständig über die Oberfläche einrichten, ohne Konfigurationsdatei
Zugangsdaten werden nur maskiert angezeigt, nie im Klartext
Feld-Mapping ist über die Oberfläche bearbeitbar","Vollständige Einrichtung eines Testconnectors über die Oberfläche erfolgreich
Zugangsdaten in der Oberfläche im Test nachweislich maskiert
Mapping-Änderung über die Oberfläche im Test korrekt übernommen"
API-02,Verbindungsstatus-Übersicht & Diagnose,🖥️ Connect-Oberfläche,Frontend,Einfach,2,4,Fertig,"Next.js/React, Statusanzeige","Übersicht über alle eingerichteten Verbindungen mit Status, letztem Sync-Lauf und Diagnoseinformationen bei Fehlern.",ENG-05 SHL-01,,"Verbindungsstatus je Connector ist auf einen Blick erkennbar
Letzter Sync-Lauf mit Ergebnis ist einsehbar
Fehlerdiagnose zeigt die Ursache aus dem Fehlerprotokoll","Statusanzeige im Test korrekt für erfolgreichen und fehlgeschlagenen Lauf
Diagnoseansicht zeigt reale Testfehler nachvollziehbar an
Aktualisierung nach neuem Sync-Lauf im Test korrekt"
API-03,Externe Webhook-Abonnements (nutzt Core API-07),🖥️ Connect-Oberfläche,Backend,Mittel,3,2,Fertig,Core-Webhook-Registry-Anbindung,"Abonnieren externer Systemereignisse über Webhooks, die die zentrale Core-Webhook-Registry zustellt – Connect registriert lediglich Abonnements, betreibt keine eigene Zustellung.",ENG-01,,"Connector kann ein Webhook-Ereignis über die Core-Webhook-Registry abonnieren
Zugestelltes Ereignis löst die zugeordnete Connector-Aktion aus
Abbestellen entfernt das Abonnement vollständig","Testabonnement über Core-Webhook-Registry im Test erfolgreich registriert
Testereignis löst im Test die erwartete Connector-Aktion aus
Abbestellung im Test vollständig entfernt"
QA-01,Teststrategie Connect,✅ Prüfungen & Qualitätssicherung,Backend,Einfach,2,1,Fertig,"Testkorpus, Teststrategie-Dokument","Teststrategie für Connect: Testkorpus aus realen Drittsystem-Antworten (ERP, CRM, DATEV, Microsoft Graph), Testarten je Ausbaustufe, Freigabekriterien.",,,"Teststrategie ist dokumentiert und für alle Epics anwendbar
Testkorpus enthält reale Beispieldaten je angebundenem Drittsystemtyp
Freigabekriterien je Prüfgate sind eindeutig benannt
Pflichttests für sicherheitsrelevanten Code (Tenant-Scoping, Zugriffsprüfung, Auth-nahe Logik) sind als verbindliches Merge-Gate verankert – kein Merge auf solchen Code ohne Test, analog Core QA-01","Teststrategie-Dokument liegt vollständig vor und wurde geprüft
Testkorpus im Test auf Vollständigkeit je Systemtyp geprüft
Freigabekriterien im Test auf Eindeutigkeit geprüft"
QA-02,Prüfgate Connector-Engine,✅ Prüfungen & Qualitätssicherung,Backend,Einfach,2,3,Fertig,"Prüfprotokoll, automatisierte Tests","Verbindliches Prüfgate für die Connector-Engine (Plugin-Modell, Credential-Verwaltung, Sync-Jobs, Mapping, Fehlerbehandlung, Audit-Kopplung).",ENG-06 ENG-02 ENG-03 ENG-04 ENG-05,QA-05,"Alle Akzeptanzkriterien der ENG-Kacheln sind geprüft und protokolliert
Automatisierte Tests der ENG-Kacheln laufen fehlerfrei
Prüfprotokoll ist abgelegt und nachlesbar","Vollständiger Testlauf über alle ENG-Kacheln durchgeführt und protokolliert
Fehlerinjektion (Drittsystem-Ausfall) im Test korrekt behandelt
Audit-Kopplung im Test nachweislich vollständig"
QA-03,Prüfgate ERP/DATEV/M365,✅ Prüfungen & Qualitätssicherung,Backend,Einfach,2,5,Fertig,"Prüfprotokoll, automatisierte Tests","Verbindliches Prüfgate für ERP/CRM-, DATEV- und Microsoft-365-Anbindung.",ERP-04 DAT-03 M365-03 ERP-02 ERP-03 M365-02 DMS2-01 DMS2-02 DMS2-03,QA-05,"Alle Akzeptanzkriterien der ERP-, DAT- und M365-Kacheln sind geprüft und protokolliert
Automatisierte Tests laufen fehlerfrei gegen Testsysteme
Prüfprotokoll ist abgelegt und nachlesbar
Alle Akzeptanzkriterien der DMS2-Kacheln (Interoperabilität mit Drittsystemen) sind ebenfalls geprüft und protokolliert","Vollständiger Testlauf über ERP-, DAT- und M365-Kacheln durchgeführt und protokolliert
Mindestens ein echter Testlauf je Drittsystemtyp durchgeführt
Fehlerbehandlung bei simuliertem Ausfall je Systemtyp geprüft"
QA-04,Sicherheitsprüfung Drittsystem-Credentials,✅ Prüfungen & Qualitätssicherung,Backend,Mittel,3,3,Fertig,"Sicherheitsprüfung, Penetrationstest-Checkliste","Gezielte Sicherheitsprüfung der Drittsystem-Credential-Verwaltung: Verschlüsselung, Zugriffskontrolle, Schlüsselverwaltung.",ENG-02,QA-05,"Verschlüsselung der Zugangsdaten besteht die Sicherheitsprüfung
Zugriff auf Zugangsdaten ist auf berechtigte Rollen beschränkt
Schlüsselverwaltung folgt dokumentierten Sicherheitsvorgaben","Verschlüsselungsprüfung gegen Datenbankinhalt durchgeführt und protokolliert
Zugriffsprüfung mit unberechtigter Rolle im Test abgewiesen
Schlüsselrotation im Test ohne Datenverlust durchgeführt"
QA-05,Abnahme-Prüfung Connect,✅ Prüfungen & Qualitätssicherung,Backend,Einfach,2,6,Fertig,Abnahmeprotokoll,"Abschließende Abnahme-Prüfung von NEXARCH Connect über alle Epics hinweg, Freigabe zur Nutzung durch andere Module.",QA-02 QA-03 QA-04 QA-06 QA-07 QA-08,,"Alle vorgelagerten Prüfgates sind abgeschlossen und protokolliert
Zusammenspiel aller Connectoren im Gesamttest geprüft
Abnahmeprotokoll ist abgelegt und nachlesbar","Gesamttest über alle Connectoren durchgeführt und protokolliert
Zusammenspiel mit Core-Webhook-Registry im Test geprüft
Abnahmeprotokoll vollständig und nachvollziehbar abgelegt"
QA-06,Schnittstellen-Vertragstests,✅ Prüfungen & Qualitätssicherung,Backend,Mittel,3,3,Fertig,"Vertragstests (Consumer-Driven Contracts), CI-Pipeline","Vertragstests für externe Webhook-Abonnements (API-03) – anders als bei internen Modul-zu-Modul-Schnittstellen sind hier externe Drittsysteme die Konsumenten, ein Breaking Change trifft Kunden direkt, nicht nur ein anderes NEXARCH-Modul.",API-03,QA-05,"Vertragstests decken das Webhook-Abonnement-Format vollständig ab
Absichtlich brechender Schema-Wechsel lässt Tests fehlschlagen
Tests laufen automatisiert in der CI","Testlauf gegen aktuellen Stand grün
Simulierte Breaking Change erkannt
CI-Integration nachgewiesen"
QA-07,Last- & Leistungstest,✅ Prüfungen & Qualitätssicherung,Backend,Mittel,3,6,Fertig,"Lastgenerator, Messprotokoll","Lasttest für Connect unter realistischer Last aus mehreren gleichzeitig synchronisierenden Connector-Instanzen (ERP, DATEV, M365) – insbesondere Credential-Verwaltung (ENG-02) und Sync-Job-Queue unter Last.",ENG-02,QA-05,"Definierte Ziel-Latenz für Credential-Abruf unter Last mehrerer gleichzeitiger Connector-Instanzen eingehalten
Sync-Job-Queue verarbeitet viele gleichzeitig eingereihte Jobs ohne Verklemmung oder Doppelverarbeitung
Rate-Limiting gegenüber Drittsystemen bleibt unter Mehrinstanz-Last korrekt eingehalten","Lasttest mit simulierten parallelen Sync-Jobs mehrerer Connector-Typen durchgeführt, Ergebnis dokumentiert
Test auf Doppelverarbeitung/Verklemmung der Job-Queue unter Last
Rate-Limit-Verhalten gegenüber Drittsystemen bei mehreren gleichzeitigen Connector-Instanzen geprüft"
QA-08,Barrierefreiheits-Audit,✅ Prüfungen & Qualitätssicherung,QA,Mittel,3,5,Fertig,"Prüfwerkzeuge, Bildschirmleser","Unabhängige Prüfung aller Connect-Oberflächen (DATEV-Export-Oberfläche, Import-Assistent, Connect-Konfigurationsoberfläche, Verbindungsstatus-Übersicht) gegen WCAG 2.1 AA mit Werkzeugen und manuellem Durchlauf, gegen die von Core SHL-01 geerbten Basis-Komponenten sowie modulspezifische Ergänzungen.",DAT-03 DMS2-02 API-01 API-02,QA-05,"Keine kritischen Verstöße offen
Manueller Bildschirmleser-Durchlauf dokumentiert
Restbefunde sind mit Frist eingeplant","Automatisierte Prüfung über alle Hauptseiten
Vollständiger Ablauf ohne Maus durchgeführt
Auditbericht abgelegt"
ENG-07,Drittsystem-Zugangsdaten über Core-Tenant-KEK-Hierarchie verschlüsseln,🔌 Connector-Engine & Grundgerüst,Backend,Mittel,2,7,Fertig,"Envelope-Encryption (Tenant-KEK aus Core API-10), PostgreSQL, Go","ENG-02 speichert Zugangsdaten für Drittsysteme (OAuth-Tokens, API-Keys) verschlüsselt, ohne festzulegen, mit welchem Schlüssel. Analog zu DMS `IAM-19` müssen diese Zugangsdaten über den Tenant-KEK aus der Core-Schlüsselhierarchie (API-10) verschlüsselt werden statt über ein separates, eigenes Verschlüsselungsschema – sonst entsteht neben der zentralen Schlüsselverwaltung ein zweiter, unabhängig zu härtender Verschlüsselungspfad für hochsensible Zugangsdaten.",ENG-02,,"Drittsystem-Zugangsdaten (OAuth-Tokens, API-Keys) sind in der Datenbank ausschließlich verschlüsselt über den Tenant-KEK des jeweiligen Mandanten gespeichert
Ein direkter Lesezugriff auf die Zugangsdaten-Tabelle liefert keinen verwertbaren Klartext
Bestehende Connector-Authentifizierung aus ENG-02 funktioniert nach der Umstellung unverändert","Test: Zugangsdaten in der Datenbank direkt auslesen, prüfen dass nur Chiffrat sichtbar ist
Regressionstest: Connector-Verbindungsaufbau (ERP/DATEV/M365/DMS2) funktioniert nach Umstellung auf Tenant-KEK-Verschlüsselung weiterhin
Test: Entschlüsselung schlägt fehl, wenn der falsche Tenant-KEK verwendet wird (kein Cross-Tenant-Zugriff auf Drittsystem-Credentials)"
ENG-08,Ressourcenlimits & Rate-Limiting für Drittsystem-Sync-Jobs,🔌 Connector-Engine & Grundgerüst,Backend,Mittel,2,7,Fertig,"Postgres-Jobqueue, Rate-Limiting, Ressourcenlimits (Zeit/Datenmenge)","ENG-03 verwaltet periodische Sync-Jobs, ENG-05 behandelt Fehler bei Drittsystem-Ausfällen mit Retry/Backoff, ohne Obergrenzen für Laufzeit oder Datenmenge je Sync-Lauf festzulegen. Ein fehlerhaft konfigurierter oder kompromittierter Connector (z. B. ein Drittsystem, das ungewöhnlich große Antworten liefert oder in einer Retry-Schleife hängen bleibt) kann sonst Ressourcen auf Kosten anderer Mandanten/Connectoren binden. Harte Obergrenzen je Sync-Lauf definieren (max. Laufzeit, max. übertragene Datenmenge, max. Anzahl Retry-Versuche als Abbruchgrenze aus ENG-05) sowie Rate-Limiting für ausgehende Anfragen an ein einzelnes Drittsystem.",ENG-03 ENG-05,,"Jeder Sync-Job hat konfigurierbare Obergrenzen für Laufzeit und übertragene Datenmenge
Die Abbruchgrenze für Retry-Versuche aus ENG-05 wird durchgesetzt, ein Job endet nach Erreichen der Grenze kontrolliert statt endlos zu wiederholen
Ausgehende Anfragen an ein einzelnes Drittsystem sind ratenbegrenzt, sodass ein einzelner Connector nicht das gesamte Sync-System für andere Connectoren/Mandanten blockiert","Test: Sync-Job, der die Laufzeit-/Datenmengenobergrenze überschreitet, wird kontrolliert abgebrochen
Test: Retry-Abbruchgrenze aus ENG-05 wird nach der konfigurierten Anzahl Versuche tatsächlich durchgesetzt
Lasttest: ein Connector mit Drittsystem-Fehlverhalten blockiert nicht die Sync-Jobs anderer Connectoren/Mandanten"
ENG-10,Modul-weiter Kill-Switch und Ausfallisolation für Connect,🔌 Connector-Engine & Grundgerüst,Backend,Mittel,2,8,Fertig,,,,,Kill-Switch per Umgebungsvariable; Totalausfall ohne Auswirkung auf andere Module; Einzel-Connector-Ausfall blockiert nicht den Rest,Kill-Switch-Test; Smoke-Test anderer Module nach hartem Prozess-Kill; Ausfall-Connector-Test
SEC-01,Echte Authentifizierung und verifizierter Tenant-Kontext für Connect-APIs,🔒 Sicherheit,Backend,Hoch,3,8,Backlog,"JWT-Verifikation (Core-Token), Go net/http Middleware, RBAC-Policy-Prüfung gegen Core","Security-Review-Finding K1 (kritisch): Connect-APIs prüfen nur `X-User-Id != """"`, Tenant kommt unverifiziert aus `X-Tenant-Slug`. `statusapi.WithPolicy()` existiert, wird aber nicht verdrahtet. Fundstellen: `statusapi.go:57-70`, `dms2importapi.go:41-54`, `datevapi.go:30-43`, `main.go:48-49`. Echte AuthN analog DMS-Vorbild (`moduleauth/middleware.go:72`) einführen.",,,"Jeder Connect-API-Aufruf verlangt ein gültiges, kryptografisch verifiziertes Core-Token; ein Request ohne gültiges Token wird mit 401 abgewiesen
Tenant- und User-Kontext werden ausschließlich aus dem verifizierten Token abgeleitet; X-User-Id/X-Tenant-Slug haben keinen Einfluss mehr, wenn sie vom Tokeninhalt abweichen
statusapi.WithPolicy() ist vor jeder Fach-API verdrahtet; unzureichende RBAC-Berechtigung führt zu 403
Angriff mit gefälschtem X-Tenant-Slug-Header auf fremde Mandantendaten ist nach der Umstellung nachweislich blockiert","Angriff real nachstellen: curl mit gefälschtem X-Tenant-Slug liefert vor dem Fix fremde Mandantendaten über /connectors/status
Gleicher Angriff gegen POST /dms2/import/execute legt vor dem Fix Import-Lauf im Namen des fremden Mandanten an
Nach dem Fix: gleicher Request ohne gültiges Token bzw. mit gefälschtem Slug wird mit 401/403 abgewiesen
Regressionstest: legitimer Request mit gültigem Token funktioniert unverändert
Test: RBAC-Policy-Verweigerung löst 403 aus"
SEC-02,Entpack-Limit gegen Zip-Bomben im DMS2-Import,🔒 Sicherheit,Backend,Mittel,1,8,Backlog,"Go archive/zip, Streaming-Lese-Limits","Security-Review-Finding H12 (hoch): `io.ReadAll(rc)` ohne Limit in `readZipEntry` (`dms2import/parse.go:219-225`) und analog in `dmsexchange.go:1966-1973`. Das 200-MiB-Limit in `dms2importapi.go:26` gilt nur für die komprimierte Größe, `UncompressedSize64` wird nie geprüft (Zip-Bomb-Risiko).",,,"readZipEntry prüft UncompressedSize64 je Eintrag und kumulativ gegen feste Obergrenzen
Gleiches Limit gilt im analogen Entpack-Muster in dmsexchange.go
Überschreitung bricht den Import mit klarer Fehlermeldung ab statt abzustürzen
Limits greifen streaming während des Lesens, nicht erst nach vollständigem Entpacken","Angriff real nachstellen: Test-Zip (~10 MiB komprimiert) entpackt vor dem Fix unbegrenzt auf mehrere GiB
Nach dem Fix: gleicher Testfall bricht vor übermäßigem Speicherverbrauch mit Fehlermeldung ab
Regressionstest: normales Import-Zip innerhalb der Limits wird weiterhin korrekt eingelesen
Grenzfalltest: Zip knapp unter/über dem Limit liefert erwartetes Verhalten"
SEC-03,Append-only-Schutz für Connect-Protokolltabellen,🔒 Sicherheit,Backend,Mittel,2,8,Backlog,"PostgreSQL REVOKE/GRANT, Trigger-Funktion","Security-Review-Finding H15 (hoch): keine der 12 Migrationen unter `connect/migrations/` erzwingt Append-only auf Protokolltabellen (`datev_exports`, `dms2_import_runs`, `connect_audit_outbox`); Code macht UPDATE/DELETE (`syncjob.go:258`, `audittrail.go:1120`, `credential.go:793`). Etabliertes Muster aus Core AUD-07/DMS VER-08/Workflow ENG-07 übernehmen.",,,"REVOKE UPDATE/DELETE von der Anwendungsrolle auf datev_exports, dms2_import_runs, connect_audit_outbox
Trigger weist versuchten UPDATE/DELETE ab und protokolliert den Versuch
INSERT funktioniert unverändert, inkl. Sequence-GRANT bei BIGSERIAL
Betroffene Codepfade (syncjob.go, audittrail.go, credential.go) funktionieren ohne Schreibversuch auf geschützte Felder","Vor der Migration: direktes UPDATE/DELETE mit Anwendungsrolle gelingt nachweislich
Nach der Migration: gleicher Versuch wird abgewiesen, Trigger protokolliert die Warnung
Regressionstest: INSERT in alle drei Tabellen funktioniert unverändert inkl. Sequence-Vergabe
Regressionstest: angepasste Codepfade (syncjob, audittrail, credential) funktionieren fachlich weiterhin korrekt"
SEC-04,Fremdsystem-Antwortkörper vor Speicherung in last_error filtern,🔒 Sicherheit,Backend,Niedrig,1,8,Backlog,"Go, Muster-/Regex-Filterung","Security-Review-Finding M16 (mittel): bis zu 1 MiB Fremdsystem-Antwortkörper ungefiltert in `last_error`, lesbar über `statusapi.go:102`. Fundstellen: `datevtransfer.go:676-682`, `m365connector.go:112`, `backupconnector.go:101,139`. Kann Secrets enthalten (z. B. AWS StringToSign/AccessKeyID).",,,"Fremdsystem-Antwortkörper wird vor Speicherung in last_error auf sicherheitsrelevante Muster geprüft und diese werden entfernt/redigiert
Nur diagnostisch relevante Kurzfassung wird persistiert, keine vollständige Rohantwort
Kurzfassung bleibt für Fehlerdiagnose über die Status-API nachvollziehbar","Vor dem Fix: simulierter Fehlerfall mit AWS-artiger Antwort landet ungefiltert in last_error und ist über Status-API lesbar
Nach dem Fix: sicherheitsrelevante Angaben sind in der Kurzfassung nicht mehr enthalten
Regressionstest: normaler Fehlerfall liefert weiterhin diagnostisch brauchbare Kurzfassung
Test für alle drei Fundstellen einzeln (DATEV-Transfer, M365, Backup-Connector)"
SEC-05,Tenant-Bindung der Connect-Store-Methoden (IDOR-Härtung),🔒 Sicherheit,Backend,Mittel,2,8,Backlog,"Go, pgx/v5, PostgreSQL, Signaturänderung","Security-Review-Finding M17 (mittel): tenant-ungebundene Store-Methoden mit `WHERE id=$1` ohne Tenant-Filter, latentes IDOR-Risiko. Fundstellen: `syncjob.go:109-122,191-269`, `datevtransfer/store.go:935-1001`, `erpexport/store.go:1424-1490`.",,,"Betroffene Store-Methoden nehmen Tenant-Parameter als zwingenden Bestandteil der Signatur entgegen
WHERE-Klausel ist um AND tenant_slug=$2 ergänzt
Alle Aufrufer sind angepasst, Projekt compiliert fehlerfrei
Zugriff mit id eines fremden Mandanten liefert kein Ergebnis","Vor dem Fix: Store-Methode mit id von Mandant A und Tenant-Kontext von Mandant B liefert fremden Datensatz
Nach dem Fix: gleicher Aufruf liefert kein Ergebnis bei nicht passendem tenant_slug
Regressionstest: Aufruf mit korrekt zueinander passendem id/tenant_slug liefert erwarteten Datensatz, für alle drei Stores
Build-Test: Projekt compiliert fehlerfrei, kein Aufrufer ohne Tenant-Parameter"
1 Code Titel Epic Rolle Komplexitaet Aufwand_PT Welle Status Technik Beschreibung Haengt_ab_von Schaltet_frei Akzeptanzkriterien Pruefungen
2 ENG-01 Connector-Plugin-Modell 🔌 Connector-Engine & Grundgerüst Backend Mittel 3 1 Fertig Plugin-Schnittstelle, Go-Interface, Registrierung Plugin-Schnittstelle, über die einzelne Drittsystem-Connectoren (ERP, CRM, DATEV, M365, andere DMS, Backup-Ziele) registriert werden, ohne dass die Engine connectorspezifisches Wissen braucht. ENG-02 ENG-03 ENG-04 ENG-06 ERP-01 ERP-02 DAT-01 DMS2-01 DMS2-03 API-03 Neuer Connector lässt sich ohne Codeänderung an der Engine registrieren Fehlende Registrierung führt zu klarer Fehlermeldung statt Absturz Connector-Metadaten (Name, Version, unterstützte Operationen) sind abrufbar Zwei Testconnectoren parallel registriert und einzeln ansprechbar Deregistrierung entfernt Connector vollständig Fehlerhafter Connector wird beim Laden abgewiesen, Engine bleibt lauffähig
3 ENG-02 Verbindungs-Credential-Verwaltung (OAuth/API-Keys, verschlüsselt) 🔌 Connector-Engine & Grundgerüst Backend Mittel 3 2 Fertig Verschlüsselte Speicherung, OAuth2-Client, PostgreSQL Verwaltung von Zugangsdaten für Drittsysteme (OAuth-Tokens, API-Keys), verschlüsselt gespeichert und je Connector-Instanz zugeordnet. ENG-01 M365-01 API-01 QA-04 QA-07 QA-02 ENG-07 Zugangsdaten sind ausschließlich verschlüsselt gespeichert OAuth-Token-Erneuerung läuft automatisiert ohne Nutzereingriff Widerruf einer Verbindung entfernt zugehörige Zugangsdaten vollständig Datenbankinhalt der Zugangsdaten-Tabelle ist ohne Schlüssel nicht lesbar Ablaufender OAuth-Token wird vor Ablauf erneuert Widerruf im Test entfernt Zugangsdaten nachweislich
4 ENG-03 Sync-Job-Verwaltung (periodische Imports/Exports) 🔌 Connector-Engine & Grundgerüst Backend Mittel 3 2 Fertig Postgres-Jobqueue, periodische Ausführung Verwaltung periodisch ausgeführter Sync-Jobs (Import/Export) je Connector-Instanz: Zeitplan, letzter Lauf, nächster Lauf. ENG-01 ENG-05 QA-02 ENG-08 Sync-Job ist mit Zeitplan konfigurierbar Laufender Job überlebt einen Dienst-Neustart Manuelles Anstoßen eines Jobs außerhalb des Zeitplans möglich Zeitplan im Test korrekt eingehalten Neustart des Dienstes mit laufendem Job geprüft Manueller Anstoß erzeugt genau einen zusätzlichen Lauf
5 ENG-04 Feld-Mapping-Konfiguration (NEXARCH ↔ Drittsystem) 🔌 Connector-Engine & Grundgerüst Backend Mittel 3 2 Fertig Mapping-Konfiguration, Schema-Validierung Konfigurierbares Feld-Mapping zwischen NEXARCH-Datenmodell und Drittsystem-Feldern je Connector-Instanz. ENG-01 API-01 QA-02 Feld-Mapping ist ohne Codeänderung konfigurierbar Ungültiges Mapping wird beim Speichern abgewiesen Pflichtfeld ohne Zuordnung wird vor Ausführung gemeldet Ungültiges Mapping im Test abgewiesen Fehlendes Pflichtfeld-Mapping verhindert Sync-Start Geändertes Mapping wirkt erst auf nachfolgende Läufe
6 ENG-05 Fehlerbehandlung & Retry bei Drittsystem-Ausfällen 🔌 Connector-Engine & Grundgerüst Backend Mittel 3 3 Fertig Retry mit Backoff, Fehlerprotokoll, Postgres-Jobqueue Fehlerbehandlung für Sync-Jobs bei Drittsystem-Ausfällen: Retry mit Backoff, Fehlerprotokoll, Abbruchgrenze. ENG-03 API-02 QA-02 ENG-08 Fehlgeschlagener Sync-Job wird mit Backoff automatisch wiederholt Dauerhafter Ausfall führt nach Grenzwert zu klarem Fehlerstatus statt Endlosschleife Fehlerprotokoll je Lauf ist einsehbar Simulierter Drittsystem-Ausfall löst Retry mit Backoff aus Abbruchgrenze im Test erreicht und Status korrekt gesetzt Fehlerprotokoll enthält Ursache und Zeitpunkt
7 ENG-06 Connector-Audit-Kopplung (Core AUD) 🔌 Connector-Engine & Grundgerüst Backend Einfach 2 2 Fertig Audit-Ereignis-Kopplung Kopplung aller Connector-Aktionen (Verbindung angelegt, Sync ausgeführt, Zugangsdaten geändert) an das zentrale Core-Audit-Log. ENG-01 QA-02 Jede sicherheitsrelevante Connector-Aktion erzeugt einen Audit-Eintrag im Core-Audit-Log Audit-Eintrag enthält Person, Zeitpunkt und betroffenen Connector Fehlende Audit-Kopplung verhindert die Aktion nicht, wird aber protokolliert Verbindung anlegen erzeugt nachweisbaren Audit-Eintrag Zugangsdaten ändern erzeugt nachweisbaren Audit-Eintrag Ausfall des Audit-Dienstes blockiert Connector-Betrieb nicht
8 ERP-01 ERP-Connector-Grundgerüst 📈 ERP/CRM-Anbindung Backend Mittel 3 2 Fertig ERP-Connector, REST/SOAP-Client Connector-Grundgerüst für die Anbindung gängiger ERP-Systeme: Verbindungsaufbau, Basisoperationen (Lesen/Schreiben von Stammdaten und Belegen). ENG-01 ERP-03 ERP-04 Verbindung zu einem Test-ERP-System lässt sich aufbauen und prüfen Basisoperationen (Lesen/Schreiben) sind über das Connector-Plugin-Modell nutzbar Verbindungsfehler wird klar gemeldet statt stillem Abbruch Verbindungsaufbau gegen Test-ERP-System erfolgreich geprüft Lese- und Schreiboperation im Test erfolgreich Simulierter Verbindungsfehler wird korrekt gemeldet
9 ERP-02 CRM-Connector-Grundgerüst 📈 ERP/CRM-Anbindung Backend Mittel 3 2 Fertig CRM-Connector, REST-Client Connector-Grundgerüst für die Anbindung gängiger CRM-Systeme: Verbindungsaufbau, Basisoperationen für Kontakte/Kunden. ENG-01 ERP-03 QA-03 Verbindung zu einem Test-CRM-System lässt sich aufbauen und prüfen Basisoperationen (Lesen/Schreiben) sind über das Connector-Plugin-Modell nutzbar Verbindungsfehler wird klar gemeldet statt stillem Abbruch Verbindungsaufbau gegen Test-CRM-System erfolgreich geprüft Lese- und Schreiboperation im Test erfolgreich Simulierter Verbindungsfehler wird korrekt gemeldet
10 ERP-03 Stammdaten-Abgleich (Kunden/Lieferanten) 📈 ERP/CRM-Anbindung Backend Mittel 3 3 Fertig Sync-Logik, Konfliktbehandlung Automatisierter Abgleich von Kunden- und Lieferantenstammdaten zwischen NEXARCH und angebundenem ERP/CRM. ERP-01 ERP-02 QA-03 Neuer Stammdatensatz im Drittsystem erscheint nach Sync in NEXARCH Änderungskonflikt wird erkannt und nicht stillschweigend überschrieben Abgleich läuft nach Zeitplan über die Sync-Job-Verwaltung Neuanlage im Test-ERP nach Sync in NEXARCH sichtbar Gleichzeitige Änderung auf beiden Seiten erzeugt erkennbaren Konflikt statt Datenverlust Zeitplan-gesteuerter Lauf im Test erfolgreich
11 ERP-04 Beleg-/Dokumentexport an ERP 📈 ERP/CRM-Anbindung Backend Mittel 3 3 Fertig Dokumentexport, Feld-Mapping Export von Belegen/Dokumenten aus NEXARCH an das angebundene ERP-System inklusive Metadaten. ERP-01 QA-03 Beleg lässt sich mit zugeordneten Metadaten an das ERP exportieren Fehlgeschlagener Export wird über die Fehlerbehandlung erneut versucht Bereits exportierter Beleg wird nicht doppelt übertragen Testbeleg im ERP nach Export mit korrekten Metadaten auffindbar Simulierter Exportfehler löst Retry aus Doppelversand im Test ausgeschlossen
12 DAT-01 DATEV-Format-Export (Belege/Buchungsstapel) 🧾 DATEV-Anbindung Backend Mittel 3 2 Fertig DATEV-Exportformat, Buchungsstapel-Struktur Export von Belegen und Buchungsstapeln im DATEV-Format aus NEXARCH-Dokumenten und -Metadaten. ENG-01 RET-05 DAT-02 DAT-03 Export erzeugt gültige DATEV-Exportdatei aus Testbelegen Pflichtfelder des DATEV-Formats werden vollständig befüllt oder der Export klar abgewiesen Exportierter Buchungsstapel ist eindeutig einem Zeitraum zugeordnet Exportiertes Objekt (Buchungsstapel) wird bei Archive RET-05 mit der GoBD-Aufbewahrungsklasse registriert, statt unregistriert im Dateisystem zu liegen Erzeugte Exportdatei besteht Formatprüfung gegen DATEV-Spezifikation Fehlendes Pflichtfeld führt zu klarer Abweisung statt fehlerhaftem Export Zeitraumzuordnung im Test korrekt Export eines Testbuchungsstapels erzeugt nachweisbar eine Registrierung bei Archive RET-05
13 DAT-02 DATEV-Konnektor (Schnittstellen-Protokoll) 🧾 DATEV-Anbindung Backend Komplex 4 3 Fertig DATEV-Schnittstellenprotokoll, Übertragungslogik Konnektor, der den Export im DATEV-Format über das offizielle Schnittstellenprotokoll an DATEV-Systeme überträgt. DAT-01 DAT-03 Übertragung an ein Test-DATEV-Zielsystem gelingt protokollkonform Fehlgeschlagene Übertragung wird über die Fehlerbehandlung erneut versucht Übertragungsstatus ist je Lauf nachvollziehbar Protokollkonforme Übertragung im Test gegen Referenzsystem geprüft Simulierter Übertragungsfehler löst Retry aus Übertragungsstatus im Test korrekt und nachvollziehbar
14 DAT-03 DATEV-Export-Oberfläche & Protokoll 🧾 DATEV-Anbindung Frontend Einfach 2 4 Fertig React/Next.js-Oberfläche, Exportprotokoll-Ansicht Oberfläche zum Anstoßen von DATEV-Exporten sowie Einsicht in Exportprotokolle und Übertragungsstatus. DAT-01 DAT-02 SHL-01 QA-03 Export lässt sich über die Oberfläche mit Zeitraum-Auswahl anstoßen Exportprotokoll ist je Lauf einsehbar Fehlgeschlagener Export ist in der Oberfläche klar erkennbar Export-Anstoß im Test aus der Oberfläche erfolgreich Protokollansicht zeigt reale Testläufe korrekt an Fehlgeschlagener Testlauf ist visuell klar unterscheidbar
15 M365-01 Microsoft-Graph-API-Anbindung (Auth/Basis) ☁️ Microsoft 365-Anbindung Backend Mittel 3 3 Fertig Microsoft Graph API, OAuth2 Basis-Anbindung an Microsoft Graph: Authentifizierung über OAuth2 und grundlegende API-Aufrufe als Voraussetzung für SharePoint/OneDrive- und Outlook-Anbindung. ENG-02 M365-02 M365-03 Authentifizierung gegen Microsoft Graph im Test erfolgreich Zugangsdaten werden über die Connect-Credential-Verwaltung gespeichert Abgelaufene Berechtigung wird erkannt und klar gemeldet OAuth2-Anmeldung im Test gegen Microsoft-Testmandant erfolgreich Testaufruf einer Graph-API-Basisoperation erfolgreich Simulierter Berechtigungsablauf wird korrekt erkannt und gemeldet
16 M365-02 SharePoint/OneDrive-Dokumentabgleich ☁️ Microsoft 365-Anbindung Backend Mittel 3 4 Fertig SharePoint/OneDrive-API, Sync-Logik Abgleich von Dokumenten zwischen NEXARCH und SharePoint/OneDrive über die Microsoft-Graph-Anbindung. M365-01 QA-03 Dokument aus SharePoint/OneDrive erscheint nach Sync in NEXARCH mit Metadaten Änderungskonflikt wird erkannt und nicht stillschweigend überschrieben Abgleich läuft nach Zeitplan über die Sync-Job-Verwaltung Testdokument aus SharePoint nach Sync in NEXARCH korrekt vorhanden Gleichzeitige Änderung auf beiden Seiten erzeugt erkennbaren Konflikt Zeitplan-gesteuerter Lauf im Test erfolgreich
17 M365-03 Outlook-Kalender-Kopplung (Fristen/Wiedervorlage) ☁️ Microsoft 365-Anbindung Backend Mittel 3 4 Fertig Outlook-Kalender-API, Fristen-Kopplung Kopplung von NEXARCH-Fristen und Wiedervorlagen mit dem Outlook-Kalender über die Microsoft-Graph-Anbindung. M365-01 QA-03 Frist/Wiedervorlage erscheint als Kalendereintrag in Outlook Änderung der Frist in NEXARCH aktualisiert den Kalendereintrag Gelöschte Frist entfernt den zugehörigen Kalendereintrag Testfrist im Test-Outlook-Kalender korrekt sichtbar Friständerung im Test korrekt im Kalender nachgezogen Löschung im Test entfernt Kalendereintrag vollständig
18 DMS2-01 Standardisiertes Export-/Import-Format (offen, dokumentiert) 🔄 Andere-DMS-Interoperabilität Backend Mittel 3 2 Fertig Offenes Austauschformat, Schema-Dokumentation Offen dokumentiertes Export-/Import-Format für Dokumente und Metadaten zur Interoperabilität mit anderen DMS-Systemen. ENG-01 DMS2-02 QA-03 Format ist vollständig dokumentiert und ohne NEXARCH-Werkzeug lesbar Export enthält Dokument, Metadaten und Versionsbezug Format-Validierung weist fehlerhafte Dateien ab Exportierte Testdatei extern ohne NEXARCH-Werkzeug lesbar geprüft Enthaltene Metadaten im Test vollständig und korrekt Fehlerhafte Testdatei wird bei Validierung abgewiesen
19 DMS2-02 Import-Assistent für Drittsystem-Exporte 🔄 Andere-DMS-Interoperabilität Fullstack Mittel 3 3 Fertig Import-Assistent mit Mapping-Vorschau-Oberfläche (Next.js/React), Backend-Parser für Drittsystem-Exportformate Assistent, der Exporte aus gängigen Drittsystem-DMS einliest, Feld-Mapping vorschlägt und den Import durchführt. DMS2-01 SHL-01 QA-03 Testexport eines gängigen Drittsystems lässt sich einlesen Feld-Mapping wird vorgeschlagen und ist vor Import änderbar Import-Ergebnis mit Erfolgs-/Fehlerzahl ist einsehbar Mapping-Vorschau ist eine bedienbare Oberfläche (nicht nur eine API-Antwort), in der Feldzuordnungen vor dem Import angepasst werden können Testexport eines Drittsystems im Test erfolgreich eingelesen Manuelle Mapping-Korrektur im Test übernommen Import-Protokoll zeigt korrekte Erfolgs-/Fehlerzahl Mapping-Vorschau im Browser bedient: Zuordnung geändert, Import mit geänderter Zuordnung ausgeführt
20 DMS2-03 Externe-Backup-Ziel-Anbindung 🔄 Andere-DMS-Interoperabilität Backend Mittel 3 2 Fertig Backup-Ziel-Connector, verschlüsselte Übertragung Anbindung externer Backup-Ziele (z. B. S3-kompatibler Speicher) für den verschlüsselten Export von Dokumentbeständen. ENG-01 QA-03 Backup-Ziel lässt sich über das Connector-Plugin-Modell einrichten Export an das Backup-Ziel erfolgt verschlüsselt Fehlgeschlagener Backup-Export wird über die Fehlerbehandlung erneut versucht Verbindungsaufbau zu Test-Backup-Ziel im Test erfolgreich Übertragung im Test nachweislich verschlüsselt Simulierter Übertragungsfehler löst Retry aus
21 API-01 Connect-Konfigurationsoberfläche (Connectoren einrichten) 🖥️ Connect-Oberfläche Frontend Mittel 3 3 Fertig Next.js/React, Connector-Einrichtungsformulare Oberfläche zum Einrichten und Konfigurieren von Connectoren: Verbindung anlegen, Zugangsdaten hinterlegen, Feld-Mapping bearbeiten. ENG-02 ENG-04 SHL-01 Connector lässt sich vollständig über die Oberfläche einrichten, ohne Konfigurationsdatei Zugangsdaten werden nur maskiert angezeigt, nie im Klartext Feld-Mapping ist über die Oberfläche bearbeitbar Vollständige Einrichtung eines Testconnectors über die Oberfläche erfolgreich Zugangsdaten in der Oberfläche im Test nachweislich maskiert Mapping-Änderung über die Oberfläche im Test korrekt übernommen
22 API-02 Verbindungsstatus-Übersicht & Diagnose 🖥️ Connect-Oberfläche Frontend Einfach 2 4 Fertig Next.js/React, Statusanzeige Übersicht über alle eingerichteten Verbindungen mit Status, letztem Sync-Lauf und Diagnoseinformationen bei Fehlern. ENG-05 SHL-01 Verbindungsstatus je Connector ist auf einen Blick erkennbar Letzter Sync-Lauf mit Ergebnis ist einsehbar Fehlerdiagnose zeigt die Ursache aus dem Fehlerprotokoll Statusanzeige im Test korrekt für erfolgreichen und fehlgeschlagenen Lauf Diagnoseansicht zeigt reale Testfehler nachvollziehbar an Aktualisierung nach neuem Sync-Lauf im Test korrekt
23 API-03 Externe Webhook-Abonnements (nutzt Core API-07) 🖥️ Connect-Oberfläche Backend Mittel 3 2 Fertig Core-Webhook-Registry-Anbindung Abonnieren externer Systemereignisse über Webhooks, die die zentrale Core-Webhook-Registry zustellt – Connect registriert lediglich Abonnements, betreibt keine eigene Zustellung. ENG-01 Connector kann ein Webhook-Ereignis über die Core-Webhook-Registry abonnieren Zugestelltes Ereignis löst die zugeordnete Connector-Aktion aus Abbestellen entfernt das Abonnement vollständig Testabonnement über Core-Webhook-Registry im Test erfolgreich registriert Testereignis löst im Test die erwartete Connector-Aktion aus Abbestellung im Test vollständig entfernt
24 QA-01 Teststrategie Connect ✅ Prüfungen & Qualitätssicherung Backend Einfach 2 1 Fertig Testkorpus, Teststrategie-Dokument Teststrategie für Connect: Testkorpus aus realen Drittsystem-Antworten (ERP, CRM, DATEV, Microsoft Graph), Testarten je Ausbaustufe, Freigabekriterien. Teststrategie ist dokumentiert und für alle Epics anwendbar Testkorpus enthält reale Beispieldaten je angebundenem Drittsystemtyp Freigabekriterien je Prüfgate sind eindeutig benannt Pflichttests für sicherheitsrelevanten Code (Tenant-Scoping, Zugriffsprüfung, Auth-nahe Logik) sind als verbindliches Merge-Gate verankert – kein Merge auf solchen Code ohne Test, analog Core QA-01 Teststrategie-Dokument liegt vollständig vor und wurde geprüft Testkorpus im Test auf Vollständigkeit je Systemtyp geprüft Freigabekriterien im Test auf Eindeutigkeit geprüft
25 QA-02 Prüfgate Connector-Engine ✅ Prüfungen & Qualitätssicherung Backend Einfach 2 3 Fertig Prüfprotokoll, automatisierte Tests Verbindliches Prüfgate für die Connector-Engine (Plugin-Modell, Credential-Verwaltung, Sync-Jobs, Mapping, Fehlerbehandlung, Audit-Kopplung). ENG-06 ENG-02 ENG-03 ENG-04 ENG-05 QA-05 Alle Akzeptanzkriterien der ENG-Kacheln sind geprüft und protokolliert Automatisierte Tests der ENG-Kacheln laufen fehlerfrei Prüfprotokoll ist abgelegt und nachlesbar Vollständiger Testlauf über alle ENG-Kacheln durchgeführt und protokolliert Fehlerinjektion (Drittsystem-Ausfall) im Test korrekt behandelt Audit-Kopplung im Test nachweislich vollständig
26 QA-03 Prüfgate ERP/DATEV/M365 ✅ Prüfungen & Qualitätssicherung Backend Einfach 2 5 Fertig Prüfprotokoll, automatisierte Tests Verbindliches Prüfgate für ERP/CRM-, DATEV- und Microsoft-365-Anbindung. ERP-04 DAT-03 M365-03 ERP-02 ERP-03 M365-02 DMS2-01 DMS2-02 DMS2-03 QA-05 Alle Akzeptanzkriterien der ERP-, DAT- und M365-Kacheln sind geprüft und protokolliert Automatisierte Tests laufen fehlerfrei gegen Testsysteme Prüfprotokoll ist abgelegt und nachlesbar Alle Akzeptanzkriterien der DMS2-Kacheln (Interoperabilität mit Drittsystemen) sind ebenfalls geprüft und protokolliert Vollständiger Testlauf über ERP-, DAT- und M365-Kacheln durchgeführt und protokolliert Mindestens ein echter Testlauf je Drittsystemtyp durchgeführt Fehlerbehandlung bei simuliertem Ausfall je Systemtyp geprüft
27 QA-04 Sicherheitsprüfung Drittsystem-Credentials ✅ Prüfungen & Qualitätssicherung Backend Mittel 3 3 Fertig Sicherheitsprüfung, Penetrationstest-Checkliste Gezielte Sicherheitsprüfung der Drittsystem-Credential-Verwaltung: Verschlüsselung, Zugriffskontrolle, Schlüsselverwaltung. ENG-02 QA-05 Verschlüsselung der Zugangsdaten besteht die Sicherheitsprüfung Zugriff auf Zugangsdaten ist auf berechtigte Rollen beschränkt Schlüsselverwaltung folgt dokumentierten Sicherheitsvorgaben Verschlüsselungsprüfung gegen Datenbankinhalt durchgeführt und protokolliert Zugriffsprüfung mit unberechtigter Rolle im Test abgewiesen Schlüsselrotation im Test ohne Datenverlust durchgeführt
28 QA-05 Abnahme-Prüfung Connect ✅ Prüfungen & Qualitätssicherung Backend Einfach 2 6 Fertig Abnahmeprotokoll Abschließende Abnahme-Prüfung von NEXARCH Connect über alle Epics hinweg, Freigabe zur Nutzung durch andere Module. QA-02 QA-03 QA-04 QA-06 QA-07 QA-08 Alle vorgelagerten Prüfgates sind abgeschlossen und protokolliert Zusammenspiel aller Connectoren im Gesamttest geprüft Abnahmeprotokoll ist abgelegt und nachlesbar Gesamttest über alle Connectoren durchgeführt und protokolliert Zusammenspiel mit Core-Webhook-Registry im Test geprüft Abnahmeprotokoll vollständig und nachvollziehbar abgelegt
29 QA-06 Schnittstellen-Vertragstests ✅ Prüfungen & Qualitätssicherung Backend Mittel 3 3 Fertig Vertragstests (Consumer-Driven Contracts), CI-Pipeline Vertragstests für externe Webhook-Abonnements (API-03) – anders als bei internen Modul-zu-Modul-Schnittstellen sind hier externe Drittsysteme die Konsumenten, ein Breaking Change trifft Kunden direkt, nicht nur ein anderes NEXARCH-Modul. API-03 QA-05 Vertragstests decken das Webhook-Abonnement-Format vollständig ab Absichtlich brechender Schema-Wechsel lässt Tests fehlschlagen Tests laufen automatisiert in der CI Testlauf gegen aktuellen Stand grün Simulierte Breaking Change erkannt CI-Integration nachgewiesen
30 QA-07 Last- & Leistungstest ✅ Prüfungen & Qualitätssicherung Backend Mittel 3 6 Fertig Lastgenerator, Messprotokoll Lasttest für Connect unter realistischer Last aus mehreren gleichzeitig synchronisierenden Connector-Instanzen (ERP, DATEV, M365) – insbesondere Credential-Verwaltung (ENG-02) und Sync-Job-Queue unter Last. ENG-02 QA-05 Definierte Ziel-Latenz für Credential-Abruf unter Last mehrerer gleichzeitiger Connector-Instanzen eingehalten Sync-Job-Queue verarbeitet viele gleichzeitig eingereihte Jobs ohne Verklemmung oder Doppelverarbeitung Rate-Limiting gegenüber Drittsystemen bleibt unter Mehrinstanz-Last korrekt eingehalten Lasttest mit simulierten parallelen Sync-Jobs mehrerer Connector-Typen durchgeführt, Ergebnis dokumentiert Test auf Doppelverarbeitung/Verklemmung der Job-Queue unter Last Rate-Limit-Verhalten gegenüber Drittsystemen bei mehreren gleichzeitigen Connector-Instanzen geprüft
31 QA-08 Barrierefreiheits-Audit ✅ Prüfungen & Qualitätssicherung QA Mittel 3 5 Fertig Prüfwerkzeuge, Bildschirmleser Unabhängige Prüfung aller Connect-Oberflächen (DATEV-Export-Oberfläche, Import-Assistent, Connect-Konfigurationsoberfläche, Verbindungsstatus-Übersicht) gegen WCAG 2.1 AA mit Werkzeugen und manuellem Durchlauf, gegen die von Core SHL-01 geerbten Basis-Komponenten sowie modulspezifische Ergänzungen. DAT-03 DMS2-02 API-01 API-02 QA-05 Keine kritischen Verstöße offen Manueller Bildschirmleser-Durchlauf dokumentiert Restbefunde sind mit Frist eingeplant Automatisierte Prüfung über alle Hauptseiten Vollständiger Ablauf ohne Maus durchgeführt Auditbericht abgelegt
32 ENG-07 Drittsystem-Zugangsdaten über Core-Tenant-KEK-Hierarchie verschlüsseln 🔌 Connector-Engine & Grundgerüst Backend Mittel 2 7 Fertig Envelope-Encryption (Tenant-KEK aus Core API-10), PostgreSQL, Go ENG-02 speichert Zugangsdaten für Drittsysteme (OAuth-Tokens, API-Keys) verschlüsselt, ohne festzulegen, mit welchem Schlüssel. Analog zu DMS `IAM-19` müssen diese Zugangsdaten über den Tenant-KEK aus der Core-Schlüsselhierarchie (API-10) verschlüsselt werden statt über ein separates, eigenes Verschlüsselungsschema – sonst entsteht neben der zentralen Schlüsselverwaltung ein zweiter, unabhängig zu härtender Verschlüsselungspfad für hochsensible Zugangsdaten. ENG-02 Drittsystem-Zugangsdaten (OAuth-Tokens, API-Keys) sind in der Datenbank ausschließlich verschlüsselt über den Tenant-KEK des jeweiligen Mandanten gespeichert Ein direkter Lesezugriff auf die Zugangsdaten-Tabelle liefert keinen verwertbaren Klartext Bestehende Connector-Authentifizierung aus ENG-02 funktioniert nach der Umstellung unverändert Test: Zugangsdaten in der Datenbank direkt auslesen, prüfen dass nur Chiffrat sichtbar ist Regressionstest: Connector-Verbindungsaufbau (ERP/DATEV/M365/DMS2) funktioniert nach Umstellung auf Tenant-KEK-Verschlüsselung weiterhin Test: Entschlüsselung schlägt fehl, wenn der falsche Tenant-KEK verwendet wird (kein Cross-Tenant-Zugriff auf Drittsystem-Credentials)
33 ENG-08 Ressourcenlimits & Rate-Limiting für Drittsystem-Sync-Jobs 🔌 Connector-Engine & Grundgerüst Backend Mittel 2 7 Fertig Postgres-Jobqueue, Rate-Limiting, Ressourcenlimits (Zeit/Datenmenge) ENG-03 verwaltet periodische Sync-Jobs, ENG-05 behandelt Fehler bei Drittsystem-Ausfällen mit Retry/Backoff, ohne Obergrenzen für Laufzeit oder Datenmenge je Sync-Lauf festzulegen. Ein fehlerhaft konfigurierter oder kompromittierter Connector (z. B. ein Drittsystem, das ungewöhnlich große Antworten liefert oder in einer Retry-Schleife hängen bleibt) kann sonst Ressourcen auf Kosten anderer Mandanten/Connectoren binden. Harte Obergrenzen je Sync-Lauf definieren (max. Laufzeit, max. übertragene Datenmenge, max. Anzahl Retry-Versuche als Abbruchgrenze aus ENG-05) sowie Rate-Limiting für ausgehende Anfragen an ein einzelnes Drittsystem. ENG-03 ENG-05 Jeder Sync-Job hat konfigurierbare Obergrenzen für Laufzeit und übertragene Datenmenge Die Abbruchgrenze für Retry-Versuche aus ENG-05 wird durchgesetzt, ein Job endet nach Erreichen der Grenze kontrolliert statt endlos zu wiederholen Ausgehende Anfragen an ein einzelnes Drittsystem sind ratenbegrenzt, sodass ein einzelner Connector nicht das gesamte Sync-System für andere Connectoren/Mandanten blockiert Test: Sync-Job, der die Laufzeit-/Datenmengenobergrenze überschreitet, wird kontrolliert abgebrochen Test: Retry-Abbruchgrenze aus ENG-05 wird nach der konfigurierten Anzahl Versuche tatsächlich durchgesetzt Lasttest: ein Connector mit Drittsystem-Fehlverhalten blockiert nicht die Sync-Jobs anderer Connectoren/Mandanten
34 ENG-10 Modul-weiter Kill-Switch und Ausfallisolation für Connect 🔌 Connector-Engine & Grundgerüst Backend Mittel 2 8 Fertig Kill-Switch per Umgebungsvariable; Totalausfall ohne Auswirkung auf andere Module; Einzel-Connector-Ausfall blockiert nicht den Rest Kill-Switch-Test; Smoke-Test anderer Module nach hartem Prozess-Kill; Ausfall-Connector-Test
35 SEC-01 Echte Authentifizierung und verifizierter Tenant-Kontext für Connect-APIs 🔒 Sicherheit Backend Hoch 3 8 Backlog JWT-Verifikation (Core-Token), Go net/http Middleware, RBAC-Policy-Prüfung gegen Core Security-Review-Finding K1 (kritisch): Connect-APIs prüfen nur `X-User-Id != ""`, Tenant kommt unverifiziert aus `X-Tenant-Slug`. `statusapi.WithPolicy()` existiert, wird aber nicht verdrahtet. Fundstellen: `statusapi.go:57-70`, `dms2importapi.go:41-54`, `datevapi.go:30-43`, `main.go:48-49`. Echte AuthN analog DMS-Vorbild (`moduleauth/middleware.go:72`) einführen. Jeder Connect-API-Aufruf verlangt ein gültiges, kryptografisch verifiziertes Core-Token; ein Request ohne gültiges Token wird mit 401 abgewiesen Tenant- und User-Kontext werden ausschließlich aus dem verifizierten Token abgeleitet; X-User-Id/X-Tenant-Slug haben keinen Einfluss mehr, wenn sie vom Tokeninhalt abweichen statusapi.WithPolicy() ist vor jeder Fach-API verdrahtet; unzureichende RBAC-Berechtigung führt zu 403 Angriff mit gefälschtem X-Tenant-Slug-Header auf fremde Mandantendaten ist nach der Umstellung nachweislich blockiert Angriff real nachstellen: curl mit gefälschtem X-Tenant-Slug liefert vor dem Fix fremde Mandantendaten über /connectors/status Gleicher Angriff gegen POST /dms2/import/execute legt vor dem Fix Import-Lauf im Namen des fremden Mandanten an Nach dem Fix: gleicher Request ohne gültiges Token bzw. mit gefälschtem Slug wird mit 401/403 abgewiesen Regressionstest: legitimer Request mit gültigem Token funktioniert unverändert Test: RBAC-Policy-Verweigerung löst 403 aus
36 SEC-02 Entpack-Limit gegen Zip-Bomben im DMS2-Import 🔒 Sicherheit Backend Mittel 1 8 Backlog Go archive/zip, Streaming-Lese-Limits Security-Review-Finding H12 (hoch): `io.ReadAll(rc)` ohne Limit in `readZipEntry` (`dms2import/parse.go:219-225`) und analog in `dmsexchange.go:1966-1973`. Das 200-MiB-Limit in `dms2importapi.go:26` gilt nur für die komprimierte Größe, `UncompressedSize64` wird nie geprüft (Zip-Bomb-Risiko). readZipEntry prüft UncompressedSize64 je Eintrag und kumulativ gegen feste Obergrenzen Gleiches Limit gilt im analogen Entpack-Muster in dmsexchange.go Überschreitung bricht den Import mit klarer Fehlermeldung ab statt abzustürzen Limits greifen streaming während des Lesens, nicht erst nach vollständigem Entpacken Angriff real nachstellen: Test-Zip (~10 MiB komprimiert) entpackt vor dem Fix unbegrenzt auf mehrere GiB Nach dem Fix: gleicher Testfall bricht vor übermäßigem Speicherverbrauch mit Fehlermeldung ab Regressionstest: normales Import-Zip innerhalb der Limits wird weiterhin korrekt eingelesen Grenzfalltest: Zip knapp unter/über dem Limit liefert erwartetes Verhalten
37 SEC-03 Append-only-Schutz für Connect-Protokolltabellen 🔒 Sicherheit Backend Mittel 2 8 Backlog PostgreSQL REVOKE/GRANT, Trigger-Funktion Security-Review-Finding H15 (hoch): keine der 12 Migrationen unter `connect/migrations/` erzwingt Append-only auf Protokolltabellen (`datev_exports`, `dms2_import_runs`, `connect_audit_outbox`); Code macht UPDATE/DELETE (`syncjob.go:258`, `audittrail.go:1120`, `credential.go:793`). Etabliertes Muster aus Core AUD-07/DMS VER-08/Workflow ENG-07 übernehmen. REVOKE UPDATE/DELETE von der Anwendungsrolle auf datev_exports, dms2_import_runs, connect_audit_outbox Trigger weist versuchten UPDATE/DELETE ab und protokolliert den Versuch INSERT funktioniert unverändert, inkl. Sequence-GRANT bei BIGSERIAL Betroffene Codepfade (syncjob.go, audittrail.go, credential.go) funktionieren ohne Schreibversuch auf geschützte Felder Vor der Migration: direktes UPDATE/DELETE mit Anwendungsrolle gelingt nachweislich Nach der Migration: gleicher Versuch wird abgewiesen, Trigger protokolliert die Warnung Regressionstest: INSERT in alle drei Tabellen funktioniert unverändert inkl. Sequence-Vergabe Regressionstest: angepasste Codepfade (syncjob, audittrail, credential) funktionieren fachlich weiterhin korrekt
38 SEC-04 Fremdsystem-Antwortkörper vor Speicherung in last_error filtern 🔒 Sicherheit Backend Niedrig 1 8 Backlog Go, Muster-/Regex-Filterung Security-Review-Finding M16 (mittel): bis zu 1 MiB Fremdsystem-Antwortkörper ungefiltert in `last_error`, lesbar über `statusapi.go:102`. Fundstellen: `datevtransfer.go:676-682`, `m365connector.go:112`, `backupconnector.go:101,139`. Kann Secrets enthalten (z. B. AWS StringToSign/AccessKeyID). Fremdsystem-Antwortkörper wird vor Speicherung in last_error auf sicherheitsrelevante Muster geprüft und diese werden entfernt/redigiert Nur diagnostisch relevante Kurzfassung wird persistiert, keine vollständige Rohantwort Kurzfassung bleibt für Fehlerdiagnose über die Status-API nachvollziehbar Vor dem Fix: simulierter Fehlerfall mit AWS-artiger Antwort landet ungefiltert in last_error und ist über Status-API lesbar Nach dem Fix: sicherheitsrelevante Angaben sind in der Kurzfassung nicht mehr enthalten Regressionstest: normaler Fehlerfall liefert weiterhin diagnostisch brauchbare Kurzfassung Test für alle drei Fundstellen einzeln (DATEV-Transfer, M365, Backup-Connector)
39 SEC-05 Tenant-Bindung der Connect-Store-Methoden (IDOR-Härtung) 🔒 Sicherheit Backend Mittel 2 8 Backlog Go, pgx/v5, PostgreSQL, Signaturänderung Security-Review-Finding M17 (mittel): tenant-ungebundene Store-Methoden mit `WHERE id=$1` ohne Tenant-Filter, latentes IDOR-Risiko. Fundstellen: `syncjob.go:109-122,191-269`, `datevtransfer/store.go:935-1001`, `erpexport/store.go:1424-1490`. Betroffene Store-Methoden nehmen Tenant-Parameter als zwingenden Bestandteil der Signatur entgegen WHERE-Klausel ist um AND tenant_slug=$2 ergänzt Alle Aufrufer sind angepasst, Projekt compiliert fehlerfrei Zugriff mit id eines fremden Mandanten liefert kein Ergebnis Vor dem Fix: Store-Methode mit id von Mandant A und Tenant-Kontext von Mandant B liefert fremden Datensatz Nach dem Fix: gleicher Aufruf liefert kein Ergebnis bei nicht passendem tenant_slug Regressionstest: Aufruf mit korrekt zueinander passendem id/tenant_slug liefert erwarteten Datensatz, für alle drei Stores Build-Test: Projekt compiliert fehlerfrei, kein Aufrufer ohne Tenant-Parameter
File diff suppressed because it is too large. Load diff
-50
View File
@@ -1,50 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Connect Kanban – lokal</title>
<link rel="stylesheet" href="../_shared/kanban.css">
</head>
<body>
<header>
<div class="row">
<h1>Connect Kanban <span class="sub">– lokal, offline, ohne Server</span></h1>
<input id="q" placeholder="Suche: Code, Titel, Technik …" style="min-width:220px" />
<select id="fEpic"><option value="">Alle Epics</option></select>
<select id="fRole"><option value="">Alle Rollen</option></select>
<select id="fWave"><option value="">Alle Wellen</option></select>
<button id="onlyReady">Nur startbare</button>
<button id="exportBtn">Stand sichern</button>
<button id="resetBtn">Zurücksetzen</button>
</div>
<div class="stats" id="stats"></div>
</header>
<div class="board" id="board"></div>
<dialog id="dlg">
<div class="dlg-head">
<div>
<div class="code" id="dCode" style="font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--muted)"></div>
<div id="dTitle" style="font-size:18px;font-weight:600;margin-top:4px"></div>
<div id="dMeta" style="margin-top:8px"></div>
</div>
<div class="row">
<button class="primary" id="copyBtn">Prompt kopieren</button>
<button id="closeBtn">Schließen</button>
</div>
</div>
<div class="dlg-body">
<p id="dDesc" style="font-size:13px;color:#C7D3E5;line-height:1.6;margin-top:0"></p>
<h3>Baut auf</h3><div id="dDeps"></div>
<h3>Schaltet frei</h3><div id="dNext"></div>
<h3>Akzeptanzkriterien</h3><ul id="dAcc"></ul>
<h3>Prüfungen vor der Abnahme</h3><ul id="dChk"></ul>
<h3>Bau-Prompt</h3><pre id="dPrompt"></pre>
</div>
</dialog>
<script id="data" type="application/json">{"erzeugtAm": "2026-08-26", "totalDays": 85, "epics": [{"key": "ENG", "name": "Connector-Engine & Grundgerüst", "emoji": "🔌", "color": "#0369A1", "description": "Connector-Plugin-Modell, Credential-Verwaltung, Sync-Job-Verwaltung, Feld-Mapping, Fehlerbehandlung/Retry und Audit-Kopplung."}, {"key": "ERP", "name": "ERP/CRM-Anbindung", "emoji": "📈", "color": "#B45309", "description": "ERP- und CRM-Connector-Grundgerüst, Stammdaten-Abgleich und Beleg-/Dokumentexport."}, {"key": "DAT", "name": "DATEV-Anbindung", "emoji": "🧾", "color": "#7C3AED", "description": "DATEV-Format-Export, Konnektor-Protokoll und Export-Oberfläche."}, {"key": "M365", "name": "Microsoft 365-Anbindung", "emoji": "☁️", "color": "#14B8A6", "description": "Microsoft-Graph-Anbindung, SharePoint/OneDrive-Dokumentabgleich und Outlook-Kalender-Kopplung."}, {"key": "DMS2", "name": "Andere-DMS-Interoperabilität", "emoji": "🔄", "color": "#92400E", "description": "Offenes Export-/Import-Format, Import-Assistent für Drittsystem-Exporte und externe Backup-Ziel-Anbindung."}, {"key": "API", "name": "Connect-Oberfläche", "emoji": "🖥️", "color": "#4338CA", "description": "Konfigurationsoberfläche für Connectoren, Verbindungsstatus-Diagnose und externe Webhook-Abonnements."}, {"key": "QA", "name": "Prüfungen & Qualitätssicherung", "emoji": "✅", "color": "#16A34A", "description": "Teststrategie und verbindliche Prüfgates je Ausbaustufe. Ein Gate schließt eine Ausbaustufe ab."}], "waves": [{"welle": 1, "codes": ["ENG-01", "QA-01"]}, {"welle": 2, "codes": ["ENG-02", "ENG-03", "ENG-04", "ENG-06", "ERP-01", "ERP-02", "DAT-01", "DMS2-01", "DMS2-03", "API-03"]}, {"welle": 3, "codes": ["ENG-05", "ERP-03", "ERP-04", "DAT-02", "M365-01", "DMS2-02", "API-01", "QA-02", "QA-04", "QA-06"]}, {"welle": 4, "codes": ["DAT-03", "M365-02", "M365-03", "API-02"]}, {"welle": 5, "codes": ["QA-03"]}, {"welle": 6, "codes": ["QA-05", "QA-07"]}, {"welle": 7, "codes": ["ENG-07", "ENG-08"]}], "kritischerPfad": {"path": ["ENG-01", "DAT-01", "DAT-02", "DAT-03", "QA-03", "QA-05"], "days": 16}, "produktDna": {"uebernehmen": [{"produkt": "Zapier/n8n", "staerke": "Connector-Plugin-Modell mit austauschbaren Adaptern statt fest verdrahteter Drittsystem-Logik – Kernidee dieses Boards, bewusst schlanker ohne visuellen Workflow-Builder."}, {"produkt": "Airbyte", "staerke": "Sync-Job-Konzept mit Retry und Fehlerbehandlung als Vorbild für robuste, wiederholbare Imports/Exports."}], "vermeiden": [{"produkt": "Proprietäre, geschlossene iPaaS-Plattformen mit Vendor-Lock.", "ansatz": "Jeder Connector ist offen dokumentiert und ohne Herstellerwerkzeug ersetzbar – was die Oberfläche kann, kann auch die offene Schnittstelle."}, {"produkt": "Zapier/n8n – voller grafischer Workflow-Builder.", "ansatz": "Nur die Kernidee austauschbarer Adapter, kein eigenes Prozessmodellierungswerkzeug – das übernimmt bei Bedarf NEXARCH Workflow."}]}, "epicDna": {"ENG": {"leitsatz": "Ein Connector-Plugin-Modell wie bei Zapier/n8n, bewusst schlanker – kein visueller Workflow-Builder, nur austauschbare Adapter.", "uebernehmen": ["Zapier/n8n: das Konzept austauschbarer Connector-Plugins mit klar definierter Schnittstelle statt fest verdrahteter Drittsystem-Logik.", "Airbyte: Sync-Job mit Retry und Fehlerbehandlung als Vorbild für robuste periodische Imports/Exports.", "Jeder Connector offen dokumentiert und ohne Herstellerwerkzeug ersetzbar – Linie aus der DMS-Produkt-DNA fortgeführt."], "vermeiden": ["Zapier/n8n – kein grafischer Workflow-Builder mit eigener Ausführungs-Engine als Voraussetzung. Die Kernidee austauschbarer Adapter genügt.", "Proprietäre, geschlossene iPaaS-Plattformen mit Vendor-Lock – jeder Connector bleibt offen dokumentiert und ersetzbar."]}, "ERP": {"leitsatz": "Stammdaten- und Belegaustausch mit ERP/CRM wie bei Airbyte-Sync-Jobs – nachvollziehbar, wiederholbar, mit Fehlerprotokoll.", "uebernehmen": ["Airbyte: Sync-Job-Konzept mit Retry und Fehlerbehandlung für Stammdaten- und Belegabgleich.", "Zapier/n8n: austauschbare Connector-Plugins je Zielsystem statt einer monolithischen ERP-Integration."], "vermeiden": ["Kein proprietäres, geschlossenes Austauschformat je ERP-Hersteller – offen dokumentierte Feld-Mappings.", "Keine stille Datenüberschreibung bei Konflikten – Abgleich bleibt nachvollziehbar und protokolliert."]}, "DAT": {"leitsatz": "DATEV-Export so verlässlich wie ein Airbyte-Sync-Job, mit offen dokumentiertem Format statt Blackbox-Konnektor.", "uebernehmen": ["Airbyte: Retry und Fehlerbehandlung bei Exportläufen, damit ein fehlgeschlagener Export nicht stillschweigend verloren geht.", "Vollständiges Exportprotokoll je Lauf, damit jeder DATEV-Export nachvollziehbar bleibt."], "vermeiden": ["Kein Vendor-Lock durch proprietäres DATEV-Werkzeug – das Exportformat bleibt offen dokumentiert und ohne Herstellerwerkzeug nachvollziehbar.", "Keine stille Fehlklassifizierung von Belegen – AbLine truncated
<script src="../_shared/kanban.js"></script>
</body>
</html>
-64
View File
@@ -1,64 +0,0 @@
# API-01 – Connect-Konfigurationsoberfläche (Connectoren einrichten)
**Epic:** 🖥️ Connect-Oberfläche | **Rolle:** Frontend | **Komplexität:** Mittel | **Aufwand:** 3 Personentage | **Welle:** 3
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Frontend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Oberfläche und Bedienbarkeit. Keine Geschäftslogik, die serverseitig gehört.
## Auftrag
Oberfläche zum Einrichten und Konfigurieren von Connectoren: Verbindung anlegen, Zugangsdaten hinterlegen, Feld-Mapping bearbeiten.
**Technische Grundlage:** Next.js/React, Connector-Einrichtungsformulare
## Produkt-DNA für diesen Bereich
> Eine schlanke Konfigurationsoberfläche für Connectoren wie bei Zapier/n8n, kein visueller Workflow-Builder.
**Übernehmen:**
- Zapier/n8n: übersichtliche Verbindungs-Einrichtung und Statusanzeige je Connector, ohne grafischen Workflow-Builder.
- Airbyte: klare Diagnoseansicht bei fehlgeschlagenen Sync-Läufen.
**Bewusst vermeiden:**
- Kein eigener Workflow-Builder – die Oberfläche konfiguriert Connectoren, sie modelliert keine Prozesse.
- Keine eigene Webhook-Infrastruktur – die Oberfläche nutzt ausschließlich die zentrale Core-Webhook-Registry (API-07).
**Für genau diese Kachel:** Oberfläche zum Einrichten und Konfigurieren von Connectoren: Verbindung anlegen, Zugangsdaten hinterlegen, Feld-Mapping bearbeiten.
## Ausgangslage
Die folgenden Arbeitspakete sind bereits fertiggestellt und dürfen vorausgesetzt werden. Baue auf ihnen auf, statt Funktionalität zu duplizieren:
- `ENG-02` Verbindungs-Credential-Verwaltung (OAuth/API-Keys, verschlüsselt) – Verwaltung von Zugangsdaten für Drittsysteme (OAuth-Tokens, API-Keys), verschlüsselt gespeichert und je Connector-Instanz zugeordnet.
- `ENG-04` Feld-Mapping-Konfiguration (NEXARCH ↔ Drittsystem) – Konfigurierbares Feld-Mapping zwischen NEXARCH-Datenmodell und Drittsystem-Feldern je Connector-Instanz.
- Core `SHL-01` UI-Shell & Design-System (zentral) – zentrales Design-System mit WCAG-AA, i18n-Rahmen und Theming; Modul-Oberflächen erben diese Eigenschaften strukturell statt sie einzeln zu bauen.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Connector lässt sich vollständig über die Oberfläche einrichten, ohne Konfigurationsdatei
2. Zugangsdaten werden nur maskiert angezeigt, nie im Klartext
3. Feld-Mapping ist über die Oberfläche bearbeitbar
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Vollständige Einrichtung eines Testconnectors über die Oberfläche erfolgreich
2. Zugangsdaten in der Oberfläche im Test nachweislich maskiert
3. Mapping-Änderung über die Oberfläche im Test korrekt übernommen
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `API-02` Verbindungsstatus-Übersicht & Diagnose
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `API-01: 01 connect konfigurationsoberflaeche connectoren einrichten`
- Branch: `feature/api-01-01-connect-konfigurationsoberflaeche-connectoren-einrichten`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
-63
View File
@@ -1,63 +0,0 @@
# API-02 – Verbindungsstatus-Übersicht & Diagnose
**Epic:** 🖥️ Connect-Oberfläche | **Rolle:** Frontend | **Komplexität:** Einfach | **Aufwand:** 2 Personentage | **Welle:** 4
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Frontend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Oberfläche und Bedienbarkeit. Keine Geschäftslogik, die serverseitig gehört.
## Auftrag
Übersicht über alle eingerichteten Verbindungen mit Status, letztem Sync-Lauf und Diagnoseinformationen bei Fehlern.
**Technische Grundlage:** Next.js/React, Statusanzeige
## Produkt-DNA für diesen Bereich
> Eine schlanke Konfigurationsoberfläche für Connectoren wie bei Zapier/n8n, kein visueller Workflow-Builder.
**Übernehmen:**
- Zapier/n8n: übersichtliche Verbindungs-Einrichtung und Statusanzeige je Connector, ohne grafischen Workflow-Builder.
- Airbyte: klare Diagnoseansicht bei fehlgeschlagenen Sync-Läufen.
**Bewusst vermeiden:**
- Kein eigener Workflow-Builder – die Oberfläche konfiguriert Connectoren, sie modelliert keine Prozesse.
- Keine eigene Webhook-Infrastruktur – die Oberfläche nutzt ausschließlich die zentrale Core-Webhook-Registry (API-07).
**Für genau diese Kachel:** Übersicht über alle eingerichteten Verbindungen mit Status, letztem Sync-Lauf und Diagnoseinformationen bei Fehlern.
## Ausgangslage
Die folgenden Arbeitspakete sind bereits fertiggestellt und dürfen vorausgesetzt werden. Baue auf ihnen auf, statt Funktionalität zu duplizieren:
- `ENG-05` Fehlerbehandlung & Retry bei Drittsystem-Ausfällen – Fehlerbehandlung für Sync-Jobs bei Drittsystem-Ausfällen: Retry mit Backoff, Fehlerprotokoll, Abbruchgrenze.
- Core `SHL-01` UI-Shell & Design-System (zentral) – zentrales Design-System mit WCAG-AA, i18n-Rahmen und Theming; Modul-Oberflächen erben diese Eigenschaften strukturell statt sie einzeln zu bauen.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Verbindungsstatus je Connector ist auf einen Blick erkennbar
2. Letzter Sync-Lauf mit Ergebnis ist einsehbar
3. Fehlerdiagnose zeigt die Ursache aus dem Fehlerprotokoll
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Statusanzeige im Test korrekt für erfolgreichen und fehlgeschlagenen Lauf
2. Diagnoseansicht zeigt reale Testfehler nachvollziehbar an
3. Aktualisierung nach neuem Sync-Lauf im Test korrekt
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `QA-02` Prüfgate Connector-Engine
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `API-02: 02 verbindungsstatus uebersicht diagnose`
- Branch: `feature/api-02-02-verbindungsstatus-uebersicht-diagnose`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
-63
View File
@@ -1,63 +0,0 @@
# API-03 – Externe Webhook-Abonnements (nutzt Core API-07)
**Epic:** 🖥️ Connect-Oberfläche | **Rolle:** Backend | **Komplexität:** Mittel | **Aufwand:** 3 Personentage | **Welle:** 2
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Serverseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche.
## Auftrag
Abonnieren externer Systemereignisse über Webhooks, die die zentrale Core-Webhook-Registry zustellt – Connect registriert lediglich Abonnements, betreibt keine eigene Zustellung.
**Technische Grundlage:** Core-Webhook-Registry-Anbindung
## Produkt-DNA für diesen Bereich
> Eine schlanke Konfigurationsoberfläche für Connectoren wie bei Zapier/n8n, kein visueller Workflow-Builder.
**Übernehmen:**
- Zapier/n8n: übersichtliche Verbindungs-Einrichtung und Statusanzeige je Connector, ohne grafischen Workflow-Builder.
- Airbyte: klare Diagnoseansicht bei fehlgeschlagenen Sync-Läufen.
**Bewusst vermeiden:**
- Kein eigener Workflow-Builder – die Oberfläche konfiguriert Connectoren, sie modelliert keine Prozesse.
- Keine eigene Webhook-Infrastruktur – die Oberfläche nutzt ausschließlich die zentrale Core-Webhook-Registry (API-07).
**Für genau diese Kachel:** Abonnieren externer Systemereignisse über Webhooks, die die zentrale Core-Webhook-Registry zustellt – Connect registriert lediglich Abonnements, betreibt keine eigene Zustellung.
## Ausgangslage
Die folgenden Arbeitspakete sind bereits fertiggestellt und dürfen vorausgesetzt werden. Baue auf ihnen auf, statt Funktionalität zu duplizieren:
- Setzt voraus (externes Core-Board, siehe /home/sysops/Dokumente/Scripte/nexarch/core-kanban/): `API-07` Zentrale Webhook-Registry & Zustellung
- `ENG-01` Connector-Plugin-Modell – Plugin-Schnittstelle, über die einzelne Drittsystem-Connectoren (ERP, CRM, DATEV, M365, andere DMS, Backup-Ziele) registriert werden, ohne dass die Engine connectorspezifisches Wissen braucht.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Connector kann ein Webhook-Ereignis über die Core-Webhook-Registry abonnieren
2. Zugestelltes Ereignis löst die zugeordnete Connector-Aktion aus
3. Abbestellen entfernt das Abonnement vollständig
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Testabonnement über Core-Webhook-Registry im Test erfolgreich registriert
2. Testereignis löst im Test die erwartete Connector-Aktion aus
3. Abbestellung im Test vollständig entfernt
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `QA-02` Prüfgate Connector-Engine
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `API-03: 03 externe webhook abonnements nutzt core api 07`
- Branch: `feature/api-03-03-externe-webhook-abonnements-nutzt-core-api-07`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
-65
View File
@@ -1,65 +0,0 @@
# DAT-01 – DATEV-Format-Export (Belege/Buchungsstapel)
**Epic:** 🧾 DATEV-Anbindung | **Rolle:** Backend | **Komplexität:** Mittel | **Aufwand:** 3 Personentage | **Welle:** 2
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Serverseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche.
## Auftrag
Export von Belegen und Buchungsstapeln im DATEV-Format aus NEXARCH-Dokumenten und -Metadaten.
**Technische Grundlage:** DATEV-Exportformat, Buchungsstapel-Struktur
## Produkt-DNA für diesen Bereich
> DATEV-Export so verlässlich wie ein Airbyte-Sync-Job, mit offen dokumentiertem Format statt Blackbox-Konnektor.
**Übernehmen:**
- Airbyte: Retry und Fehlerbehandlung bei Exportläufen, damit ein fehlgeschlagener Export nicht stillschweigend verloren geht.
- Vollständiges Exportprotokoll je Lauf, damit jeder DATEV-Export nachvollziehbar bleibt.
**Bewusst vermeiden:**
- Kein Vendor-Lock durch proprietäres DATEV-Werkzeug – das Exportformat bleibt offen dokumentiert und ohne Herstellerwerkzeug nachvollziehbar.
- Keine stille Fehlklassifizierung von Belegen – Abweichungen werden im Protokoll sichtbar.
**Für genau diese Kachel:** Export von Belegen und Buchungsstapeln im DATEV-Format aus NEXARCH-Dokumenten und -Metadaten.
## Ausgangslage
Die folgenden Arbeitspakete sind bereits fertiggestellt und dürfen vorausgesetzt werden. Baue auf ihnen auf, statt Funktionalität zu duplizieren:
- `ENG-01` Connector-Plugin-Modell – Plugin-Schnittstelle, über die einzelne Drittsystem-Connectoren (ERP, CRM, DATEV, M365, andere DMS, Backup-Ziele) registriert werden, ohne dass die Engine connectorspezifisches Wissen braucht.
- Setzt voraus (externes Archive-Board, siehe /home/sysops/Dokumente/Scripte/nexarch/archive-kanban/): `RET-05` Modul-Adapter-Schnittstelle – exportierte GoBD-relevante Buchungsstapel werden hier mit Aufbewahrungsklasse registriert, statt unregistriert im Dateisystem zu liegen.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Export erzeugt gültige DATEV-Exportdatei aus Testbelegen
2. Pflichtfelder des DATEV-Formats werden vollständig befüllt oder der Export klar abgewiesen
3. Exportierter Buchungsstapel ist eindeutig einem Zeitraum zugeordnet
4. Exportiertes Objekt (Buchungsstapel) wird bei Archive RET-05 mit der GoBD-Aufbewahrungsklasse registriert, statt unregistriert im Dateisystem zu liegen
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Erzeugte Exportdatei besteht Formatprüfung gegen DATEV-Spezifikation
2. Fehlendes Pflichtfeld führt zu klarer Abweisung statt fehlerhaftem Export
3. Zeitraumzuordnung im Test korrekt
4. Export eines Testbuchungsstapels erzeugt nachweisbar eine Registrierung bei Archive RET-05
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `DAT-02` DATEV-Konnektor (Schnittstellen-Protokoll)
- `DAT-03` DATEV-Export-Oberfläche & Protokoll
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `DAT-01: 01 datev format export belege buchungsstapel`
- Branch: `feature/dat-01-01-datev-format-export-belege-buchungsstapel`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
-62
View File
@@ -1,62 +0,0 @@
# DAT-02 – DATEV-Konnektor (Schnittstellen-Protokoll)
**Epic:** 🧾 DATEV-Anbindung | **Rolle:** Backend | **Komplexität:** Komplex | **Aufwand:** 4 Personentage | **Welle:** 3
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Serverseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche.
## Auftrag
Konnektor, der den Export im DATEV-Format über das offizielle Schnittstellenprotokoll an DATEV-Systeme überträgt.
**Technische Grundlage:** DATEV-Schnittstellenprotokoll, Übertragungslogik
## Produkt-DNA für diesen Bereich
> DATEV-Export so verlässlich wie ein Airbyte-Sync-Job, mit offen dokumentiertem Format statt Blackbox-Konnektor.
**Übernehmen:**
- Airbyte: Retry und Fehlerbehandlung bei Exportläufen, damit ein fehlgeschlagener Export nicht stillschweigend verloren geht.
- Vollständiges Exportprotokoll je Lauf, damit jeder DATEV-Export nachvollziehbar bleibt.
**Bewusst vermeiden:**
- Kein Vendor-Lock durch proprietäres DATEV-Werkzeug – das Exportformat bleibt offen dokumentiert und ohne Herstellerwerkzeug nachvollziehbar.
- Keine stille Fehlklassifizierung von Belegen – Abweichungen werden im Protokoll sichtbar.
**Für genau diese Kachel:** Konnektor, der den Export im DATEV-Format über das offizielle Schnittstellenprotokoll an DATEV-Systeme überträgt.
## Ausgangslage
Die folgenden Arbeitspakete sind bereits fertiggestellt und dürfen vorausgesetzt werden. Baue auf ihnen auf, statt Funktionalität zu duplizieren:
- `DAT-01` DATEV-Format-Export (Belege/Buchungsstapel) – Export von Belegen und Buchungsstapeln im DATEV-Format aus NEXARCH-Dokumenten und -Metadaten.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Übertragung an ein Test-DATEV-Zielsystem gelingt protokollkonform
2. Fehlgeschlagene Übertragung wird über die Fehlerbehandlung erneut versucht
3. Übertragungsstatus ist je Lauf nachvollziehbar
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Protokollkonforme Übertragung im Test gegen Referenzsystem geprüft
2. Simulierter Übertragungsfehler löst Retry aus
3. Übertragungsstatus im Test korrekt und nachvollziehbar
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `DAT-03` DATEV-Export-Oberfläche & Protokoll
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `DAT-02: 02 datev konnektor schnittstellen protokoll`
- Branch: `feature/dat-02-02-datev-konnektor-schnittstellen-protokoll`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
-64
View File
@@ -1,64 +0,0 @@
# DAT-03 – DATEV-Export-Oberfläche & Protokoll
**Epic:** 🧾 DATEV-Anbindung | **Rolle:** Frontend | **Komplexität:** Einfach | **Aufwand:** 2 Personentage | **Welle:** 4
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Frontend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Oberfläche und Bedienbarkeit. Keine Geschäftslogik, die serverseitig gehört.
## Auftrag
Oberfläche zum Anstoßen von DATEV-Exporten sowie Einsicht in Exportprotokolle und Übertragungsstatus.
**Technische Grundlage:** React/Next.js-Oberfläche, Exportprotokoll-Ansicht
## Produkt-DNA für diesen Bereich
> DATEV-Export so verlässlich wie ein Airbyte-Sync-Job, mit offen dokumentiertem Format statt Blackbox-Konnektor.
**Übernehmen:**
- Airbyte: Retry und Fehlerbehandlung bei Exportläufen, damit ein fehlgeschlagener Export nicht stillschweigend verloren geht.
- Vollständiges Exportprotokoll je Lauf, damit jeder DATEV-Export nachvollziehbar bleibt.
**Bewusst vermeiden:**
- Kein Vendor-Lock durch proprietäres DATEV-Werkzeug – das Exportformat bleibt offen dokumentiert und ohne Herstellerwerkzeug nachvollziehbar.
- Keine stille Fehlklassifizierung von Belegen – Abweichungen werden im Protokoll sichtbar.
**Für genau diese Kachel:** Oberfläche zum Anstoßen von DATEV-Exporten sowie Einsicht in Exportprotokolle und Übertragungsstatus.
## Ausgangslage
Die folgenden Arbeitspakete sind bereits fertiggestellt und dürfen vorausgesetzt werden. Baue auf ihnen auf, statt Funktionalität zu duplizieren:
- `DAT-01` DATEV-Format-Export (Belege/Buchungsstapel) – Export von Belegen und Buchungsstapeln im DATEV-Format aus NEXARCH-Dokumenten und -Metadaten.
- `DAT-02` DATEV-Konnektor (Schnittstellen-Protokoll) – Konnektor, der den Export im DATEV-Format über das offizielle Schnittstellenprotokoll an DATEV-Systeme überträgt.
- Core `SHL-01` UI-Shell & Design-System (zentral) – zentrales Design-System mit WCAG-AA, i18n-Rahmen und Theming; Modul-Oberflächen erben diese Eigenschaften strukturell statt sie einzeln zu bauen.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Export lässt sich über die Oberfläche mit Zeitraum-Auswahl anstoßen
2. Exportprotokoll ist je Lauf einsehbar
3. Fehlgeschlagener Export ist in der Oberfläche klar erkennbar
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Export-Anstoß im Test aus der Oberfläche erfolgreich
2. Protokollansicht zeigt reale Testläufe korrekt an
3. Fehlgeschlagener Testlauf ist visuell klar unterscheidbar
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `QA-03` Prüfgate ERP/DATEV/M365
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `DAT-03: 03 datev export oberflaeche protokoll`
- Branch: `feature/dat-03-03-datev-export-oberflaeche-protokoll`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
-62
View File
@@ -1,62 +0,0 @@
# DMS2-01 – Standardisiertes Export-/Import-Format (offen, dokumentiert)
**Epic:** 🔄 Andere-DMS-Interoperabilität | **Rolle:** Backend | **Komplexität:** Mittel | **Aufwand:** 3 Personentage | **Welle:** 2
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Serverseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche.
## Auftrag
Offen dokumentiertes Export-/Import-Format für Dokumente und Metadaten zur Interoperabilität mit anderen DMS-Systemen.
**Technische Grundlage:** Offenes Austauschformat, Schema-Dokumentation
## Produkt-DNA für diesen Bereich
> Offener Austausch mit anderen DMS-Systemen wie bei einem dokumentierten Airbyte-Konnektor, kein geschlossenes Migrationswerkzeug.
**Übernehmen:**
- Airbyte: offen dokumentiertes Austauschformat als Vorbild für Export/Import zwischen DMS-Systemen.
- Zapier/n8n: der Import-Assistent bleibt ein austauschbarer Adapter, kein Einwegwerkzeug.
**Bewusst vermeiden:**
- Kein proprietäres, geschlossenes Migrationsformat – offen dokumentiert und ohne Herstellerwerkzeug lesbar.
- Keine Backup-Ziel-Anbindung mit Herstellerbindung – Zugangsdaten und Protokoll bleiben austauschbar.
**Für genau diese Kachel:** Offen dokumentiertes Export-/Import-Format für Dokumente und Metadaten zur Interoperabilität mit anderen DMS-Systemen.
## Ausgangslage
Die folgenden Arbeitspakete sind bereits fertiggestellt und dürfen vorausgesetzt werden. Baue auf ihnen auf, statt Funktionalität zu duplizieren:
- `ENG-01` Connector-Plugin-Modell – Plugin-Schnittstelle, über die einzelne Drittsystem-Connectoren (ERP, CRM, DATEV, M365, andere DMS, Backup-Ziele) registriert werden, ohne dass die Engine connectorspezifisches Wissen braucht.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Format ist vollständig dokumentiert und ohne NEXARCH-Werkzeug lesbar
2. Export enthält Dokument, Metadaten und Versionsbezug
3. Format-Validierung weist fehlerhafte Dateien ab
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Exportierte Testdatei extern ohne NEXARCH-Werkzeug lesbar geprüft
2. Enthaltene Metadaten im Test vollständig und korrekt
3. Fehlerhafte Testdatei wird bei Validierung abgewiesen
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `DMS2-02` Import-Assistent für Drittsystem-Exporte
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `DMS2-01: 01 standardisiertes export import format offen dokumentiert`
- Branch: `feature/dms2-01-01-standardisiertes-export-import-format-offen-dokumentiert`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
-65
View File
@@ -1,65 +0,0 @@
# DMS2-02 – Import-Assistent für Drittsystem-Exporte
**Epic:** 🔄 Andere-DMS-Interoperabilität | **Rolle:** Fullstack | **Komplexität:** Mittel | **Aufwand:** 3 Personentage | **Welle:** 3
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Serverseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche.
## Auftrag
Assistent, der Exporte aus gängigen Drittsystem-DMS einliest, Feld-Mapping vorschlägt und den Import durchführt.
**Technische Grundlage:** Import-Assistent mit Mapping-Vorschau-Oberfläche (Next.js/React), Backend-Parser für Drittsystem-Exportformate
## Produkt-DNA für diesen Bereich
> Offener Austausch mit anderen DMS-Systemen wie bei einem dokumentierten Airbyte-Konnektor, kein geschlossenes Migrationswerkzeug.
**Übernehmen:**
- Airbyte: offen dokumentiertes Austauschformat als Vorbild für Export/Import zwischen DMS-Systemen.
- Zapier/n8n: der Import-Assistent bleibt ein austauschbarer Adapter, kein Einwegwerkzeug.
**Bewusst vermeiden:**
- Kein proprietäres, geschlossenes Migrationsformat – offen dokumentiert und ohne Herstellerwerkzeug lesbar.
- Keine Backup-Ziel-Anbindung mit Herstellerbindung – Zugangsdaten und Protokoll bleiben austauschbar.
**Für genau diese Kachel:** Assistent, der Exporte aus gängigen Drittsystem-DMS einliest, Feld-Mapping vorschlägt und den Import durchführt.
## Ausgangslage
Die folgenden Arbeitspakete sind bereits fertiggestellt und dürfen vorausgesetzt werden. Baue auf ihnen auf, statt Funktionalität zu duplizieren:
- `DMS2-01` Standardisiertes Export-/Import-Format (offen, dokumentiert) – Offen dokumentiertes Export-/Import-Format für Dokumente und Metadaten zur Interoperabilität mit anderen DMS-Systemen.
- Core `SHL-01` UI-Shell & Design-System (zentral) – zentrales Design-System mit WCAG-AA, i18n-Rahmen und Theming; Modul-Oberflächen erben diese Eigenschaften strukturell statt sie einzeln zu bauen.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Testexport eines gängigen Drittsystems lässt sich einlesen
2. Feld-Mapping wird vorgeschlagen und ist vor Import änderbar
3. Import-Ergebnis mit Erfolgs-/Fehlerzahl ist einsehbar
4. Mapping-Vorschau ist eine bedienbare Oberfläche (nicht nur eine API-Antwort), in der Feldzuordnungen vor dem Import angepasst werden können
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Testexport eines Drittsystems im Test erfolgreich eingelesen
2. Manuelle Mapping-Korrektur im Test übernommen
3. Import-Protokoll zeigt korrekte Erfolgs-/Fehlerzahl
4. Mapping-Vorschau im Browser bedient: Zuordnung geändert, Import mit geänderter Zuordnung ausgeführt
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `QA-02` Prüfgate Connector-Engine
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `DMS2-02: 02 import assistent fuer drittsystem exporte`
- Branch: `feature/dms2-02-02-import-assistent-fuer-drittsystem-exporte`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
-62
View File
@@ -1,62 +0,0 @@
# DMS2-03 – Externe-Backup-Ziel-Anbindung
**Epic:** 🔄 Andere-DMS-Interoperabilität | **Rolle:** Backend | **Komplexität:** Mittel | **Aufwand:** 3 Personentage | **Welle:** 2
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Serverseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche.
## Auftrag
Anbindung externer Backup-Ziele (z. B. S3-kompatibler Speicher) für den verschlüsselten Export von Dokumentbeständen.
**Technische Grundlage:** Backup-Ziel-Connector, verschlüsselte Übertragung
## Produkt-DNA für diesen Bereich
> Offener Austausch mit anderen DMS-Systemen wie bei einem dokumentierten Airbyte-Konnektor, kein geschlossenes Migrationswerkzeug.
**Übernehmen:**
- Airbyte: offen dokumentiertes Austauschformat als Vorbild für Export/Import zwischen DMS-Systemen.
- Zapier/n8n: der Import-Assistent bleibt ein austauschbarer Adapter, kein Einwegwerkzeug.
**Bewusst vermeiden:**
- Kein proprietäres, geschlossenes Migrationsformat – offen dokumentiert und ohne Herstellerwerkzeug lesbar.
- Keine Backup-Ziel-Anbindung mit Herstellerbindung – Zugangsdaten und Protokoll bleiben austauschbar.
**Für genau diese Kachel:** Anbindung externer Backup-Ziele (z. B. S3-kompatibler Speicher) über das bestehende Connector-Plugin-Modell, statt einer festverdrahteten Backup-Lösung – ein neues Ziel lässt sich als weiterer Connector ergänzen, ohne den Kern anzufassen.
## Ausgangslage
Die folgenden Arbeitspakete sind bereits fertiggestellt und dürfen vorausgesetzt werden. Baue auf ihnen auf, statt Funktionalität zu duplizieren:
- `ENG-01` Connector-Plugin-Modell – Plugin-Schnittstelle, über die einzelne Drittsystem-Connectoren (ERP, CRM, DATEV, M365, andere DMS, Backup-Ziele) registriert werden, ohne dass die Engine connectorspezifisches Wissen braucht.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Backup-Ziel lässt sich über das Connector-Plugin-Modell einrichten
2. Export an das Backup-Ziel erfolgt verschlüsselt
3. Fehlgeschlagener Backup-Export wird über die Fehlerbehandlung erneut versucht
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Verbindungsaufbau zu Test-Backup-Ziel im Test erfolgreich
2. Übertragung im Test nachweislich verschlüsselt
3. Simulierter Übertragungsfehler löst Retry aus
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `QA-04` Sicherheitsprüfung Drittsystem-Credentials
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `DMS2-03: 03 externe backup ziel anbindung`
- Branch: `feature/dms2-03-03-externe-backup-ziel-anbindung`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
-69
View File
@@ -1,69 +0,0 @@
# ENG-01 – Connector-Plugin-Modell
**Epic:** 🔌 Connector-Engine & Grundgerüst | **Rolle:** Backend | **Komplexität:** Mittel | **Aufwand:** 3 Personentage | **Welle:** 1
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Serverseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche.
## Auftrag
Plugin-Schnittstelle, über die einzelne Drittsystem-Connectoren (ERP, CRM, DATEV, M365, andere DMS, Backup-Ziele) registriert werden, ohne dass die Engine connectorspezifisches Wissen braucht.
**Technische Grundlage:** Plugin-Schnittstelle, Go-Interface, Registrierung
## Produkt-DNA für diesen Bereich
> Ein Connector-Plugin-Modell wie bei Zapier/n8n, bewusst schlanker – kein visueller Workflow-Builder, nur austauschbare Adapter.
**Übernehmen:**
- Zapier/n8n: das Konzept austauschbarer Connector-Plugins mit klar definierter Schnittstelle statt fest verdrahteter Drittsystem-Logik.
- Airbyte: Sync-Job mit Retry und Fehlerbehandlung als Vorbild für robuste periodische Imports/Exports.
- Jeder Connector offen dokumentiert und ohne Herstellerwerkzeug ersetzbar – Linie aus der DMS-Produkt-DNA fortgeführt.
**Bewusst vermeiden:**
- Zapier/n8n – kein grafischer Workflow-Builder mit eigener Ausführungs-Engine als Voraussetzung. Die Kernidee austauschbarer Adapter genügt.
- Proprietäre, geschlossene iPaaS-Plattformen mit Vendor-Lock – jeder Connector bleibt offen dokumentiert und ersetzbar.
**Für genau diese Kachel:** Plugin-Schnittstelle, über die einzelne Drittsystem-Connectoren (ERP, CRM, DATEV, M365, andere DMS, Backup-Ziele) registriert werden, ohne dass die Engine connectorspezifisches Wissen braucht.
## Ausgangslage
Diese Kachel hat keine Vorbedingungen und ist sofort startbar.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Neuer Connector lässt sich ohne Codeänderung an der Engine registrieren
2. Fehlende Registrierung führt zu klarer Fehlermeldung statt Absturz
3. Connector-Metadaten (Name, Version, unterstützte Operationen) sind abrufbar
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Zwei Testconnectoren parallel registriert und einzeln ansprechbar
2. Deregistrierung entfernt Connector vollständig
3. Fehlerhafter Connector wird beim Laden abgewiesen, Engine bleibt lauffähig
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `ENG-02` Verbindungs-Credential-Verwaltung
- `ENG-03` Sync-Job-Verwaltung
- `ENG-04` Feld-Mapping-Konfiguration
- `ENG-06` Connector-Audit-Kopplung
- `ERP-01` ERP-Connector-Grundgerüst
- `DAT-01` DATEV-Format-Export
- `DMS2-01` Standardisiertes Export-/Import-Format
- `API-03` Externe Webhook-Abonnements
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `ENG-01: 01 connector plugin modell`
- Branch: `feature/eng-01-01-connector-plugin-modell`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
-66
View File
@@ -1,66 +0,0 @@
# ENG-02 – Verbindungs-Credential-Verwaltung (OAuth/API-Keys, verschlüsselt)
**Epic:** 🔌 Connector-Engine & Grundgerüst | **Rolle:** Backend | **Komplexität:** Mittel | **Aufwand:** 3 Personentage | **Welle:** 2
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Serverseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche.
## Auftrag
Verwaltung von Zugangsdaten für Drittsysteme (OAuth-Tokens, API-Keys), verschlüsselt gespeichert und je Connector-Instanz zugeordnet.
**Technische Grundlage:** Verschlüsselte Speicherung, OAuth2-Client, PostgreSQL
## Produkt-DNA für diesen Bereich
> Ein Connector-Plugin-Modell wie bei Zapier/n8n, bewusst schlanker – kein visueller Workflow-Builder, nur austauschbare Adapter.
**Übernehmen:**
- Zapier/n8n: das Konzept austauschbarer Connector-Plugins mit klar definierter Schnittstelle statt fest verdrahteter Drittsystem-Logik.
- Airbyte: Sync-Job mit Retry und Fehlerbehandlung als Vorbild für robuste periodische Imports/Exports.
- Jeder Connector offen dokumentiert und ohne Herstellerwerkzeug ersetzbar – Linie aus der DMS-Produkt-DNA fortgeführt.
**Bewusst vermeiden:**
- Zapier/n8n – kein grafischer Workflow-Builder mit eigener Ausführungs-Engine als Voraussetzung. Die Kernidee austauschbarer Adapter genügt.
- Proprietäre, geschlossene iPaaS-Plattformen mit Vendor-Lock – jeder Connector bleibt offen dokumentiert und ersetzbar.
**Für genau diese Kachel:** Verwaltung von Zugangsdaten für Drittsysteme (OAuth-Tokens, API-Keys), verschlüsselt gespeichert und je Connector-Instanz zugeordnet.
## Ausgangslage
Die folgenden Arbeitspakete sind bereits fertiggestellt und dürfen vorausgesetzt werden. Baue auf ihnen auf, statt Funktionalität zu duplizieren:
- Setzt voraus (externes Core-Board, siehe /home/sysops/Dokumente/Scripte/nexarch/core-kanban/): `IAM-09` API-Token & Service-Accounts
- `ENG-01` Connector-Plugin-Modell – Plugin-Schnittstelle, über die einzelne Drittsystem-Connectoren (ERP, CRM, DATEV, M365, andere DMS, Backup-Ziele) registriert werden, ohne dass die Engine connectorspezifisches Wissen braucht.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Zugangsdaten sind ausschließlich verschlüsselt gespeichert
2. OAuth-Token-Erneuerung läuft automatisiert ohne Nutzereingriff
3. Widerruf einer Verbindung entfernt zugehörige Zugangsdaten vollständig
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Datenbankinhalt der Zugangsdaten-Tabelle ist ohne Schlüssel nicht lesbar
2. Ablaufender OAuth-Token wird vor Ablauf erneuert
3. Widerruf im Test entfernt Zugangsdaten nachweislich
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `M365-01` Microsoft-Graph-API-Anbindung (nutzt diese Verwaltung)
- `API-01` Connect-Konfigurationsoberfläche
- `QA-04` Sicherheitsprüfung Drittsystem-Credentials
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `ENG-02: 02 verbindungs credential verwaltung oauth api keys verschluesselt`
- Branch: `feature/eng-02-02-verbindungs-credential-verwaltung-oauth-api-keys-verschluesselt`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
-64
View File
@@ -1,64 +0,0 @@
# ENG-03 – Sync-Job-Verwaltung (periodische Imports/Exports)
**Epic:** 🔌 Connector-Engine & Grundgerüst | **Rolle:** Backend | **Komplexität:** Mittel | **Aufwand:** 3 Personentage | **Welle:** 2
---
## Rolle
Du bist erfahrene Entwicklerin im Bereich Backend und baust NEXARCH Connect (Modul für externe Systemanbindungen: ERP/CRM, DATEV, Microsoft 365, Interoperabilität mit anderen DMS-Systemen und externe Backup-Ziele), das vollständig auf der REST-API-Infrastruktur, dem Rate-Limiting, der Webhook-Zustellung, der Token-Verwaltung und dem Audit-Log von NEXARCH Core aufbaut, statt eigene Infrastruktur dafür zu bauen.
Go-Backend (net/http, pgx/v5, PostgreSQL, kein ORM), bcrypt, Postgres-Jobqueue statt Redis, Next.js/React/TypeScript-Frontend, natives systemd-Deployment. Multi-Tenancy Modell A.
Serverseitige Logik, Datenzugriff und Schnittstelle. Keine Geschäftslogik in der Oberfläche.
## Auftrag
Verwaltung periodisch ausgeführter Sync-Jobs (Import/Export) je Connector-Instanz: Zeitplan, letzter Lauf, nächster Lauf.
**Technische Grundlage:** Postgres-Jobqueue, periodische Ausführung
## Produkt-DNA für diesen Bereich
> Ein Connector-Plugin-Modell wie bei Zapier/n8n, bewusst schlanker – kein visueller Workflow-Builder, nur austauschbare Adapter.
**Übernehmen:**
- Zapier/n8n: das Konzept austauschbarer Connector-Plugins mit klar definierter Schnittstelle statt fest verdrahteter Drittsystem-Logik.
- Airbyte: Sync-Job mit Retry und Fehlerbehandlung als Vorbild für robuste periodische Imports/Exports.
- Jeder Connector offen dokumentiert und ohne Herstellerwerkzeug ersetzbar – Linie aus der DMS-Produkt-DNA fortgeführt.
**Bewusst vermeiden:**
- Zapier/n8n – kein grafischer Workflow-Builder mit eigener Ausführungs-Engine als Voraussetzung. Die Kernidee austauschbarer Adapter genügt.
- Proprietäre, geschlossene iPaaS-Plattformen mit Vendor-Lock – jeder Connector bleibt offen dokumentiert und ersetzbar.
**Für genau diese Kachel:** Verwaltung periodisch ausgeführter Sync-Jobs (Import/Export) je Connector-Instanz: Zeitplan, letzter Lauf, nächster Lauf.
## Ausgangslage
Die folgenden Arbeitspakete sind bereits fertiggestellt und dürfen vorausgesetzt werden. Baue auf ihnen auf, statt Funktionalität zu duplizieren:
- `ENG-01` Connector-Plugin-Modell – Plugin-Schnittstelle, über die einzelne Drittsystem-Connectoren (ERP, CRM, DATEV, M365, andere DMS, Backup-Ziele) registriert werden, ohne dass die Engine connectorspezifisches Wissen braucht.
## Akzeptanzkriterien
Die Kachel gilt erst als umgesetzt, wenn alle Punkte erfüllt sind:
1. Sync-Job ist mit Zeitplan konfigurierbar
2. Laufender Job überlebt einen Dienst-Neustart
3. Manuelles Anstoßen eines Jobs außerhalb des Zeitplans möglich
## Prüfungen vor der Abnahme
Führe jede Prüfung tatsächlich durch und halte das Ergebnis schriftlich fest. Nicht durchgeführte Prüfungen zählen als offen:
1. Zeitplan im Test korrekt eingehalten
2. Neustart des Dienstes mit laufendem Job geprüft
3. Manueller Anstoß erzeugt genau einen zusätzlichen Lauf
## Nicht Bestandteil dieser Kachel
Diese Punkte gehören ausdrücklich in andere Kacheln. Bereite höchstens die Schnittstelle dafür vor:
- `ENG-05` Fehlerbehandlung & Retry bei Drittsystem-Ausfällen
- `API-02` Verbindungsstatus-Übersicht & Diagnose
## Arbeitsweise
1. Lies zuerst den bestehenden Code der Vorbedingungen und halte dich an die dort etablierten Muster.
2. Setze die kleinste Lösung um, die alle Akzeptanzkriterien erfüllt. Kein Umbau angrenzender Bereiche.
3. Schreibe zu jedem Akzeptanzkriterium mindestens einen automatisierten Test.
4. Führe alle Prüfungen aus und dokumentiere Zielwert und Ergebnis.
5. Deutschsprachige Oberflächentexte, englischsprachige Bezeichner im Code.
6. Keine Zugangsdaten, Schlüssel oder Verbindungszeichenfolgen im Code – ausschließlich über Umgebungsvariablen.
## Abschluss
- Commit-Nachricht: `ENG-03: 03 sync job verwaltung periodische imports exports`
- Branch: `feature/eng-03-03-sync-job-verwaltung-periodische-imports-exports`
- Schrittweise committen, Branch pushen, dann anhalten. Kein Merge, kein Deploy.
- In der Beschreibung des Pull Request die durchgeführten Prüfungen mit Ergebnis auflisten.
Loaded 100 of 480 files, more files were not shown because too many files have changed in this diff. Show more