Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7515b8db4d | ||
|
|
cd5ca8da53 |
No files matched your search
@@ -1,2 +0,0 @@
|
||||
# common/ ist modulübergreifend: Änderungen brauchen Review (Auswirkungsliste im PR)
|
||||
/common/ @patrick
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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; }
|
||||
@@ -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 { '&': '&', '<': '<', '>': '>' }[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
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
@@ -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=
|
||||
|
||||
@@ -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) ---
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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/`),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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
-1
@@ -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
|
||||
+4
-4
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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)")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
2
|
||||
@@ -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.
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
@@ -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).
|
||||
@@ -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"
|
||||
|
File diff suppressed because it is too large.
Load diff
@@ -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>
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
Reference in new issue
Block a user