feat: Archiv-Übersicht und Beteiligten-CRUD im Web-Layer

Bisher gab es nur Prüfen -> Archivieren, keine Möglichkeit, bereits
geprüfte Beiträge wieder anzusehen oder die Verantwortungsmatrix
(Beteiligte: wer hat vorgegeben, wer freigegeben) tatsächlich zu
pflegen — nur der Store-Layer dafür existierte schon.

Neu:
- GET /beitraege: Liste aller Beiträge des angemeldeten Mandanten
  (Plattform, Status, höchste Finding-Schwere) via
  ListSubmissionsForAccount.
- GET /beitraege/{id}: Detailseite mit Fakten, Findings, Archivieren-
  Aktion und Verantwortungsmatrix.
- POST .../beteiligte, .../beteiligte/{pid}/aktualisieren,
  .../beteiligte/{pid}/loeschen: echtes CRUD statt nur Ansicht, per
  htmx ohne Seiten-Reload.

Mandantentrennung wie beim bestehenden Archiv-Download: fremde
Beiträge und fremde Beteiligte (auch über eine erratene participant_id)
liefern 404, nicht 403 — sonst würde eine 403 die Existenz der Ressource
bei einem anderen Mandanten bestätigen.

Web-Store-Interface um die bereits fertigen Store-Methoden erweitert,
fakeStore in server_test.go entsprechend nachgezogen. Volle Testsuite
inkl. echter Postgres-Tests (./scripts/test.sh) grün.
This commit is contained in:
noroot
2026-08-27 17:54:33 +02:00
parent 6df1d2961b
commit c9d71b0d54
9 changed files with 799 additions and 0 deletions

View File

@@ -66,6 +66,7 @@ type fakeStore struct {
extractions map[string]store.Extraction
findings map[string][]store.Finding
evidencePkgs map[string]store.EvidencePackage
participants map[string]store.Participant
}
func newFakeStore() *fakeStore {
@@ -78,6 +79,7 @@ func newFakeStore() *fakeStore {
extractions: map[string]store.Extraction{},
findings: map[string][]store.Finding{},
evidencePkgs: map[string]store.EvidencePackage{},
participants: map[string]store.Participant{},
}
}
@@ -247,6 +249,106 @@ func (f *fakeStore) GetLatestEvidencePackage(ctx context.Context, submissionID s
return pkg, nil
}
func (f *fakeStore) ListSubmissionsForAccount(ctx context.Context, accountID string) ([]store.SubmissionSummary, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []store.SubmissionSummary
for _, sub := range f.submissions {
if sub.AccountID != accountID {
continue
}
sum := store.SubmissionSummary{Submission: sub}
rank := 0
for _, finding := range f.findings[sub.ID] {
sum.FindingCount++
r := severityRank(finding.Severity)
if r > rank {
rank = r
sum.HighestSeverity = finding.Severity
}
}
out = append(out, sum)
}
return out, nil
}
func severityRank(severity string) int {
switch severity {
case "hoch":
return 3
case "mittel":
return 2
case "niedrig":
return 1
default:
return 0
}
}
func (f *fakeStore) CreateParticipant(ctx context.Context, submissionID, role, name string, vorgegeben, freigegeben bool) (store.Participant, error) {
f.mu.Lock()
defer f.mu.Unlock()
p := store.Participant{
ID: f.newID(), SubmissionID: submissionID, Role: role, Name: name,
Vorgegeben: vorgegeben, Freigegeben: freigegeben, CreatedAt: time.Now(),
}
if freigegeben {
now := time.Now()
p.ApprovedAt = &now
}
f.participants[p.ID] = p
return p, nil
}
func (f *fakeStore) ListParticipants(ctx context.Context, submissionID string) ([]store.Participant, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []store.Participant
for _, p := range f.participants {
if p.SubmissionID == submissionID {
out = append(out, p)
}
}
return out, nil
}
func (f *fakeStore) GetParticipant(ctx context.Context, id string) (store.Participant, error) {
f.mu.Lock()
defer f.mu.Unlock()
p, ok := f.participants[id]
if !ok {
return store.Participant{}, store.ErrNotFound
}
return p, nil
}
func (f *fakeStore) UpdateParticipant(ctx context.Context, id string, vorgegeben, freigegeben bool) (store.Participant, error) {
f.mu.Lock()
defer f.mu.Unlock()
p, ok := f.participants[id]
if !ok {
return store.Participant{}, store.ErrNotFound
}
p.Vorgegeben = vorgegeben
p.Freigegeben = freigegeben
if freigegeben && p.ApprovedAt == nil {
now := time.Now()
p.ApprovedAt = &now
}
f.participants[id] = p
return p, nil
}
func (f *fakeStore) DeleteParticipant(ctx context.Context, id string) error {
f.mu.Lock()
defer f.mu.Unlock()
if _, ok := f.participants[id]; !ok {
return store.ErrNotFound
}
delete(f.participants, id)
return nil
}
// fakeTimestamper liefert einen offline erzeugten, strukturell gültigen
// (selbstsignierten) RFC-3161-Token — genug, damit evidence.TimestampTime
// ihn parsen kann, ohne eine echte TSA zu brauchen.