feat: wire persistence into the web layer (check → archive → dossier)
This is the piece that turns a Pre-Publish-Prüfung into an actual
archived, provable record instead of a one-off form response.
extract.Client.Extract now returns Result{Facts, RawJSON} instead of
just Facts — RawJSON is the model's exact, unmodified JSON, which is
what belongs in extraction.payload (the audit trail), not a re-encoded
view through our own Facts struct. extract.ParsePayload reconstructs
Facts from a stored payload later, reusing the same parsing/validation
path Extract uses (including the enum guard), so a previously-saved
extraction can be read back exactly as it would have been the first
time.
internal/dossier.BuildContent no longer requires AssetHash: most checks
right now are caption-only (no image/video upload wired yet), and
inventing a placeholder hash for a nonexistent asset would itself be an
integrity problem in an evidence tool. Content shows "kein Asset
hinterlegt" instead.
internal/web gains a narrow Store interface (mirroring the Extractor
pattern — only the methods these handlers use, not the full
*store.Store) so its test suite stays network/DB-free via an in-memory
fake:
- POST /pruefen persists submission + extraction + findings and marks
the submission "checked". A needsClarification result persists the
extraction (there's something worth keeping) but no findings and no
status change, and the template omits the archive option entirely.
- POST /veroeffentlichen re-derives Facts from the stored payload, hashes
the canonical submission+facts+findings metadata, gets an RFC-3161
timestamp, generates the PDF dossier to disk, and persists the
evidence_package row before marking the submission "published".
- GET /dossier/{id} serves the generated PDF.
Tested end-to-end offline: a fake Timestamper builds a real, structurally
valid self-signed RFC-3161 response so the full check→archive→download
flow runs against an in-memory store, verifying the downloaded bytes are
an actual PDF and the dossier file lands on disk — without hitting a
real database, TSA, or the Claude API.
cmd/deklarix/main.go now wires store.Store, evidence.NewHTTPTimestamper
(TSA_URL, default FreeTSA), and DOSSIER_DIR (default "dossiers") into
web.NewServer alongside the extractor and rule set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,10 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/netcell-it/deklarix/internal/evidence"
|
||||
"github.com/netcell-it/deklarix/internal/extract"
|
||||
"github.com/netcell-it/deklarix/internal/rules"
|
||||
"github.com/netcell-it/deklarix/internal/store"
|
||||
)
|
||||
|
||||
//go:embed templates/*.html
|
||||
@@ -28,31 +30,61 @@ var staticFS embed.FS
|
||||
// *extract.Client erfüllt sie; Tests injizieren einen Fake statt echte
|
||||
// Claude-API-Aufrufe zu machen.
|
||||
type Extractor interface {
|
||||
Extract(ctx context.Context, in extract.Input) (rules.Facts, error)
|
||||
Extract(ctx context.Context, in extract.Input) (extract.Result, error)
|
||||
ModelVersion() string
|
||||
}
|
||||
|
||||
// Store ist die Schnittstelle, die der Server zur Persistenz braucht —
|
||||
// bewusst schmal (nur was diese Handler tatsächlich nutzen), nicht der
|
||||
// komplette *store.Store. *store.Store erfüllt sie; Tests injizieren
|
||||
// einen Fake statt eine echte Datenbank zu brauchen.
|
||||
type Store interface {
|
||||
CreateSubmission(ctx context.Context, platform, postType, caption string) (store.Submission, error)
|
||||
GetSubmission(ctx context.Context, id string) (store.Submission, error)
|
||||
SetSubmissionStatus(ctx context.Context, id, status string) error
|
||||
CreateExtraction(ctx context.Context, submissionID string, payload []byte, modelVersion, promptVersion string) (store.Extraction, error)
|
||||
GetLatestExtraction(ctx context.Context, submissionID string) (store.Extraction, error)
|
||||
CreateFinding(ctx context.Context, submissionID string, extractionID *string, ruleID string, ruleVersion int, severity, title, fix string, sources []string) (store.Finding, error)
|
||||
ListCurrentFindings(ctx context.Context, submissionID string) ([]store.Finding, error)
|
||||
CreateEvidencePackage(ctx context.Context, submissionID, dossierPath, sha256Hex string, timestampToken []byte) (store.EvidencePackage, error)
|
||||
GetLatestEvidencePackage(ctx context.Context, submissionID string) (store.EvidencePackage, error)
|
||||
}
|
||||
|
||||
// Server bündelt Routing und Abhängigkeiten der Web-Schicht.
|
||||
type Server struct {
|
||||
mux *http.ServeMux
|
||||
extractor Extractor
|
||||
ruleSet []rules.Rule
|
||||
templates *template.Template
|
||||
mux *http.ServeMux
|
||||
extractor Extractor
|
||||
ruleSet []rules.Rule
|
||||
store Store
|
||||
timestamper evidence.Timestamper
|
||||
dossierDir string
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
// NewServer erstellt den Server. ruleSet kommt von rules.Load und wird
|
||||
// einmal beim Start geladen, nicht pro Request.
|
||||
func NewServer(extractor Extractor, ruleSet []rules.Rule) (*Server, error) {
|
||||
// einmal beim Start geladen, nicht pro Request. dossierDir ist das
|
||||
// Verzeichnis, in das erzeugte Nachweis-Dossiers geschrieben werden.
|
||||
func NewServer(extractor Extractor, ruleSet []rules.Rule, st Store, timestamper evidence.Timestamper, dossierDir string) (*Server, error) {
|
||||
tmpl, err := template.ParseFS(templatesFS, "templates/*.html")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("web: templates parsen: %w", err)
|
||||
}
|
||||
|
||||
s := &Server{extractor: extractor, ruleSet: ruleSet, templates: tmpl}
|
||||
s := &Server{
|
||||
extractor: extractor,
|
||||
ruleSet: ruleSet,
|
||||
store: st,
|
||||
timestamper: timestamper,
|
||||
dossierDir: dossierDir,
|
||||
templates: tmpl,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /health", s.handleHealth)
|
||||
mux.HandleFunc("GET /{$}", s.handleIndex)
|
||||
mux.HandleFunc("POST /pruefen", s.handleCheck)
|
||||
mux.HandleFunc("POST /veroeffentlichen", s.handleArchive)
|
||||
mux.HandleFunc("GET /dossier/{id}", s.handleDossierDownload)
|
||||
mux.Handle("GET /static/", http.FileServerFS(staticFS))
|
||||
s.mux = mux
|
||||
|
||||
|
||||
Reference in New Issue
Block a user