Deklarix itself no longer depends on the Anthropic API — that was a separate API key/billing relationship from Claude Code (used to develop Deklarix), which the user did not intend to take on for the product itself. Consideration (Gegenleistung) is no longer guessed from text — it's a required form field now, since only the submitter actually knows whether a business relationship existed. A keyword-only system can't tell a covertly-paid post from a genuinely organic one; they read identically. What internal/extract *can* still determine reliably and deterministically from the caption: whether a disclosure keyword is present (werbung, anzeige, bezahlte partnerschaft, paid partnership, #ad, #werbung, #anzeige, #sponsored, #sponsoredby, #sponsoredpost — case-insensitive), its exact original-case wording, and whether it sits before the platform's "mehr anzeigen" truncation point (~125 chars Instagram, ~150 TikTok — rough estimates, platforms change these without notice, verify before real customer use). internal/extract's Anthropic HTTP client and tool-use schema are gone (client.go/api.go deleted), replaced by engine.go — a stateless Engine with no network calls. extract.Result/ParsePayload keep the exact same JSON shape as before (gegenleistung/kennzeichnung_vorhanden/ kennzeichnung_wortlaut/kennzeichnung_vor_kuerzung), so internal/store and internal/dossier needed no changes at all — only extract itself, the web form/handler (new consideration field), and main.go (no more ANTHROPIC_API_KEY requirement) changed. Trade-off the user was told and accepted: without an LLM, the system can no longer independently catch undisclosed paid content that carries no recognizable keyword at all — that now rests on the submitter's honesty. Creative or implicit disclosure phrasing outside the keyword list also won't be recognized. Verified against a real running instance with zero API keys configured: register -> check (real rule engine, correctly triggered WK-004 for a disclosure placed 130 characters in, past the Instagram threshold) -> archive -> PDF dossier download, all against real Postgres. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
111 lines
4.5 KiB
Go
111 lines
4.5 KiB
Go
// Package web ist die HTTP-Schicht: Routing, Templates, Handler.
|
|
// Bewusst html/template + htmx, kein Frontend-Build (siehe CLAUDE.md,
|
|
// Stack). Jede Submission gehört einem Account (Mandant); Auth-Cookies
|
|
// binden einen Request an einen angemeldeten Nutzer und damit an dessen
|
|
// Account — siehe middleware.go.
|
|
package web
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"time"
|
|
|
|
"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
|
|
var templatesFS embed.FS
|
|
|
|
//go:embed static/*
|
|
var staticFS embed.FS
|
|
|
|
// Extractor ist die Schnittstelle, die der Server für Stufe 1 braucht.
|
|
// *extract.Engine erfüllt sie (regelbasiert, kein externer Dienst);
|
|
// Tests injizieren einen Fake, um Facts unabhängig von der echten
|
|
// Erkennungslogik vorzugeben.
|
|
type Extractor interface {
|
|
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, accountID, 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)
|
|
|
|
CreateAccount(ctx context.Context, name string) (store.Account, error)
|
|
CreateUser(ctx context.Context, accountID, email, passwordHash, role string) (store.User, error)
|
|
GetUserByEmail(ctx context.Context, email string) (store.User, error)
|
|
GetUser(ctx context.Context, id string) (store.User, error)
|
|
CreateSession(ctx context.Context, token, userID string, expiresAt time.Time) (store.Session, error)
|
|
GetSession(ctx context.Context, token string) (store.Session, error)
|
|
DeleteSession(ctx context.Context, token string) error
|
|
}
|
|
|
|
// Server bündelt Routing und Abhängigkeiten der Web-Schicht.
|
|
type Server struct {
|
|
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. 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,
|
|
store: st,
|
|
timestamper: timestamper,
|
|
dossierDir: dossierDir,
|
|
templates: tmpl,
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /health", s.handleHealth)
|
|
mux.HandleFunc("GET /register", s.handleRegisterForm)
|
|
mux.HandleFunc("POST /register", s.handleRegister)
|
|
mux.HandleFunc("GET /login", s.handleLoginForm)
|
|
mux.HandleFunc("POST /login", s.handleLogin)
|
|
mux.HandleFunc("POST /logout", s.handleLogout)
|
|
mux.HandleFunc("GET /{$}", s.requirePage(s.handleIndex))
|
|
mux.HandleFunc("POST /pruefen", s.requireAPI(s.handleCheck))
|
|
mux.HandleFunc("POST /veroeffentlichen", s.requireAPI(s.handleArchive))
|
|
mux.HandleFunc("GET /dossier/{id}", s.requireAPI(s.handleDossierDownload))
|
|
mux.Handle("GET /static/", http.FileServerFS(staticFS))
|
|
s.mux = mux
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// ServeHTTP macht Server zu einem http.Handler.
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
s.mux.ServeHTTP(w, r)
|
|
}
|