Ports KanzlAI document upload + AI extraction into paliad. PDFs are stored
in Supabase Storage (bucket paliad-documents); Claude Sonnet extracts
deadlines with tool-forced structured output; the user reviews candidates
and picks which to persist as Fristen.
Backend
- internal/services/ai_service.go — Anthropic SDK wrapper. Uses native PDF
content blocks, forced tool_use for structured output, ephemeral prompt
caching on the system prompt. Sonnet 4.6.
- internal/services/storage.go — Supabase Storage REST client (upload,
download, delete). Nil when SUPABASE_SERVICE_KEY is unset.
- internal/services/dokument_service.go — upload (PDF magic-number check,
20 MB cap), list, download, extract, persist-confirmed-as-Fristen. All
visibility-checked through AkteService.GetByID.
- internal/handlers/dokumente.go — five endpoints plus /api/config/features
so the UI can hide disabled buttons.
- internal/handlers/ratelimit.go — in-memory per-user cap of 20 extractions
per UTC day (design §9.7).
- Both optional services (storage, AI) degrade to 501 with friendly German
messages when their env vars are unset.
Schema
- migration 013 adds fristen.source_document_id (FK to dokumente) and
dokumente.ai_extraction_count + ai_extracted_at for the UI badge.
Frontend
- Dokumente tab in /akten/{id}/dokumente replaces the Phase D placeholder:
drag-drop upload zone with live progress bar (XHR), document table with
download + extract actions, extraction-review modal with per-row
checkboxes, confidence chips, expandable source-quote, editable title +
due date + rule code, POST to the from-extraction endpoint.
- Upload + extract buttons hide automatically when the server reports the
feature is disabled.
- Full DE/EN i18n. CSS for the upload zone, extraction modal, and
confidence chips.
Env vars (not set here — flag to head):
- ANTHROPIC_API_KEY (enables extraction)
- SUPABASE_SERVICE_KEY (enables upload/download)
Branch: mai/ritchie/phase-h-ai-deadline
93 lines
3.1 KiB
Go
93 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
|
|
"mgit.msbls.de/m/patholo/internal/auth"
|
|
"mgit.msbls.de/m/patholo/internal/db"
|
|
"mgit.msbls.de/m/patholo/internal/handlers"
|
|
"mgit.msbls.de/m/patholo/internal/services"
|
|
)
|
|
|
|
func main() {
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "8080"
|
|
}
|
|
|
|
supabaseURL := os.Getenv("SUPABASE_URL")
|
|
supabaseAnonKey := os.Getenv("SUPABASE_ANON_KEY")
|
|
if supabaseURL == "" || supabaseAnonKey == "" {
|
|
log.Fatal("SUPABASE_URL and SUPABASE_ANON_KEY must be set")
|
|
}
|
|
|
|
client := auth.NewClient(supabaseURL, supabaseAnonKey)
|
|
|
|
giteaToken := os.Getenv("GITEA_TOKEN")
|
|
if giteaToken == "" {
|
|
log.Println("GITEA_TOKEN not set — file proxy will not be able to access private repos")
|
|
}
|
|
|
|
// Phase H: optional dependencies for document upload + AI extraction.
|
|
// Both services degrade to a 501 response when their env vars are unset;
|
|
// the UI hides their buttons based on GET /api/config/features.
|
|
supabaseServiceKey := os.Getenv("SUPABASE_SERVICE_KEY")
|
|
anthropicAPIKey := os.Getenv("ANTHROPIC_API_KEY")
|
|
storageClient := services.NewStorageClient(supabaseURL, supabaseServiceKey)
|
|
aiService := services.NewAIService(anthropicAPIKey)
|
|
if storageClient == nil {
|
|
log.Println("SUPABASE_SERVICE_KEY not set — document upload/download disabled")
|
|
}
|
|
if aiService == nil {
|
|
log.Println("ANTHROPIC_API_KEY not set — AI deadline extraction disabled")
|
|
}
|
|
|
|
// DATABASE_URL is optional during the Phase A → Phase D transition. The
|
|
// existing knowledge-platform features (Kostenrechner, Glossar, etc.) work
|
|
// without a DB. Akten/Frist endpoints return 503 until DATABASE_URL is set.
|
|
dbURL := os.Getenv("DATABASE_URL")
|
|
var svcBundle *handlers.Services
|
|
if dbURL != "" {
|
|
log.Println("applying database migrations…")
|
|
if err := db.ApplyMigrations(dbURL); err != nil {
|
|
log.Fatalf("migration failed: %v", err)
|
|
}
|
|
log.Println("database migrations applied")
|
|
|
|
pool, err := db.OpenPool(dbURL)
|
|
if err != nil {
|
|
log.Fatalf("open db pool: %v", err)
|
|
}
|
|
holidays := services.NewHolidayService(pool)
|
|
users := services.NewUserService(pool)
|
|
akteSvc := services.NewAkteService(pool, users)
|
|
rules := services.NewDeadlineRuleService(pool)
|
|
fristSvc := services.NewFristService(pool, akteSvc)
|
|
dokumentSvc := services.NewDokumentService(pool, storageClient, aiService, akteSvc, fristSvc)
|
|
svcBundle = &handlers.Services{
|
|
Akte: akteSvc,
|
|
Parteien: services.NewParteienService(pool, akteSvc),
|
|
Frist: fristSvc,
|
|
Rules: rules,
|
|
Calculator: services.NewDeadlineCalculator(holidays),
|
|
Users: users,
|
|
Fristenrechner: services.NewFristenrechnerService(rules, holidays),
|
|
Dashboard: services.NewDashboardService(pool, users),
|
|
Dokument: dokumentSvc,
|
|
}
|
|
log.Println("Phase B services initialised")
|
|
} else {
|
|
log.Println("DATABASE_URL not set — Akten/Frist endpoints will return 503")
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
handlers.Register(mux, client, giteaToken, svcBundle)
|
|
|
|
log.Printf("paliad server starting on :%s", port)
|
|
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|