fix: Restore all files lost during destructive rebase

A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit 21a844cb8a
1986 changed files with 744143 additions and 1731 deletions

View File

@@ -0,0 +1,638 @@
package handlers
import (
"fmt"
"net/http"
"os"
"time"
"github.com/breakpilot/ai-compliance-sdk/internal/funding"
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gopkg.in/yaml.v3"
)
// FundingHandlers handles funding application API endpoints
type FundingHandlers struct {
store funding.Store
providerRegistry *llm.ProviderRegistry
wizardSchema *WizardSchema
bundeslandProfiles map[string]*BundeslandProfile
}
// WizardSchema represents the loaded wizard schema
type WizardSchema struct {
Metadata struct {
Version string `yaml:"version"`
Name string `yaml:"name"`
Description string `yaml:"description"`
TotalSteps int `yaml:"total_steps"`
} `yaml:"metadata"`
Steps []WizardStep `yaml:"steps"`
FundingAssistant struct {
Enabled bool `yaml:"enabled"`
Model string `yaml:"model"`
SystemPrompt string `yaml:"system_prompt"`
StepContexts map[int]string `yaml:"step_contexts"`
QuickPrompts []QuickPrompt `yaml:"quick_prompts"`
} `yaml:"funding_assistant"`
Presets map[string]Preset `yaml:"presets"`
}
// WizardStep represents a step in the wizard
type WizardStep struct {
Number int `yaml:"number" json:"number"`
ID string `yaml:"id" json:"id"`
Title string `yaml:"title" json:"title"`
Subtitle string `yaml:"subtitle" json:"subtitle"`
Description string `yaml:"description" json:"description"`
Icon string `yaml:"icon" json:"icon"`
IsRequired bool `yaml:"is_required" json:"is_required"`
Fields []WizardField `yaml:"fields" json:"fields"`
AssistantContext string `yaml:"assistant_context" json:"assistant_context"`
}
// WizardField represents a field in the wizard
type WizardField struct {
ID string `yaml:"id" json:"id"`
Type string `yaml:"type" json:"type"`
Label string `yaml:"label" json:"label"`
Placeholder string `yaml:"placeholder,omitempty" json:"placeholder,omitempty"`
Required bool `yaml:"required,omitempty" json:"required,omitempty"`
Options []FieldOption `yaml:"options,omitempty" json:"options,omitempty"`
HelpText string `yaml:"help_text,omitempty" json:"help_text,omitempty"`
MaxLength int `yaml:"max_length,omitempty" json:"max_length,omitempty"`
Min *int `yaml:"min,omitempty" json:"min,omitempty"`
Max *int `yaml:"max,omitempty" json:"max,omitempty"`
Default interface{} `yaml:"default,omitempty" json:"default,omitempty"`
Conditional string `yaml:"conditional,omitempty" json:"conditional,omitempty"`
}
// FieldOption represents an option for select fields
type FieldOption struct {
Value string `yaml:"value" json:"value"`
Label string `yaml:"label" json:"label"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
}
// QuickPrompt represents a quick prompt for the assistant
type QuickPrompt struct {
Label string `yaml:"label" json:"label"`
Prompt string `yaml:"prompt" json:"prompt"`
}
// Preset represents a BreakPilot preset
type Preset struct {
ID string `yaml:"id" json:"id"`
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
BudgetItems []funding.BudgetItem `yaml:"budget_items" json:"budget_items"`
AutoFill map[string]interface{} `yaml:"auto_fill" json:"auto_fill"`
}
// BundeslandProfile represents a federal state profile
type BundeslandProfile struct {
Name string `yaml:"name" json:"name"`
Short string `yaml:"short" json:"short"`
FundingPrograms []string `yaml:"funding_programs" json:"funding_programs"`
DefaultFundingRate float64 `yaml:"default_funding_rate" json:"default_funding_rate"`
RequiresMEP bool `yaml:"requires_mep" json:"requires_mep"`
ContactAuthority ContactAuthority `yaml:"contact_authority" json:"contact_authority"`
SpecialRequirements []string `yaml:"special_requirements" json:"special_requirements"`
}
// ContactAuthority represents a contact authority
type ContactAuthority struct {
Name string `yaml:"name" json:"name"`
Department string `yaml:"department,omitempty" json:"department,omitempty"`
Website string `yaml:"website" json:"website"`
Email string `yaml:"email,omitempty" json:"email,omitempty"`
}
// NewFundingHandlers creates new funding handlers
func NewFundingHandlers(store funding.Store, providerRegistry *llm.ProviderRegistry) *FundingHandlers {
h := &FundingHandlers{
store: store,
providerRegistry: providerRegistry,
}
// Load wizard schema
if err := h.loadWizardSchema(); err != nil {
fmt.Printf("Warning: Could not load wizard schema: %v\n", err)
}
// Load bundesland profiles
if err := h.loadBundeslandProfiles(); err != nil {
fmt.Printf("Warning: Could not load bundesland profiles: %v\n", err)
}
return h
}
func (h *FundingHandlers) loadWizardSchema() error {
data, err := os.ReadFile("policies/funding/foerderantrag_wizard_v1.yaml")
if err != nil {
return err
}
h.wizardSchema = &WizardSchema{}
return yaml.Unmarshal(data, h.wizardSchema)
}
func (h *FundingHandlers) loadBundeslandProfiles() error {
data, err := os.ReadFile("policies/funding/bundesland_profiles.yaml")
if err != nil {
return err
}
var profiles struct {
Bundeslaender map[string]*BundeslandProfile `yaml:"bundeslaender"`
}
if err := yaml.Unmarshal(data, &profiles); err != nil {
return err
}
h.bundeslandProfiles = profiles.Bundeslaender
return nil
}
// ============================================================================
// Application CRUD
// ============================================================================
// CreateApplication creates a new funding application
// POST /sdk/v1/funding/applications
func (h *FundingHandlers) CreateApplication(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var req funding.CreateApplicationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
app := &funding.FundingApplication{
TenantID: tenantID,
Title: req.Title,
FundingProgram: req.FundingProgram,
Status: funding.ApplicationStatusDraft,
CurrentStep: 1,
TotalSteps: 8,
WizardData: make(map[string]interface{}),
CreatedBy: userID,
UpdatedBy: userID,
}
// Initialize school profile with federal state
app.SchoolProfile = &funding.SchoolProfile{
FederalState: req.FederalState,
}
// Apply preset if specified
if req.PresetID != "" && h.wizardSchema != nil {
if preset, ok := h.wizardSchema.Presets[req.PresetID]; ok {
app.Budget = &funding.Budget{
BudgetItems: preset.BudgetItems,
}
app.WizardData["preset_id"] = req.PresetID
app.WizardData["preset_applied"] = true
for k, v := range preset.AutoFill {
app.WizardData[k] = v
}
}
}
if err := h.store.CreateApplication(c.Request.Context(), app); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Add history entry
h.store.AddHistoryEntry(c.Request.Context(), &funding.ApplicationHistoryEntry{
ApplicationID: app.ID,
Action: "created",
PerformedBy: userID,
Notes: "Antrag erstellt",
})
c.JSON(http.StatusCreated, app)
}
// GetApplication retrieves a funding application
// GET /sdk/v1/funding/applications/:id
func (h *FundingHandlers) GetApplication(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"})
return
}
app, err := h.store.GetApplication(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, app)
}
// ListApplications returns a list of funding applications
// GET /sdk/v1/funding/applications
func (h *FundingHandlers) ListApplications(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
filter := funding.ApplicationFilter{
Page: 1,
PageSize: 20,
}
// Parse query parameters
if status := c.Query("status"); status != "" {
s := funding.ApplicationStatus(status)
filter.Status = &s
}
if program := c.Query("program"); program != "" {
p := funding.FundingProgram(program)
filter.FundingProgram = &p
}
result, err := h.store.ListApplications(c.Request.Context(), tenantID, filter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
// UpdateApplication updates a funding application
// PUT /sdk/v1/funding/applications/:id
func (h *FundingHandlers) UpdateApplication(c *gin.Context) {
userID := rbac.GetUserID(c)
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"})
return
}
app, err := h.store.GetApplication(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
var req funding.UpdateApplicationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Title != nil {
app.Title = *req.Title
}
if req.WizardData != nil {
for k, v := range req.WizardData {
app.WizardData[k] = v
}
}
if req.CurrentStep != nil {
app.CurrentStep = *req.CurrentStep
}
app.UpdatedBy = userID
if err := h.store.UpdateApplication(c.Request.Context(), app); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, app)
}
// DeleteApplication deletes a funding application
// DELETE /sdk/v1/funding/applications/:id
func (h *FundingHandlers) DeleteApplication(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"})
return
}
if err := h.store.DeleteApplication(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "application archived"})
}
// ============================================================================
// Wizard Endpoints
// ============================================================================
// GetWizardSchema returns the wizard schema
// GET /sdk/v1/funding/wizard/schema
func (h *FundingHandlers) GetWizardSchema(c *gin.Context) {
if h.wizardSchema == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "wizard schema not loaded"})
return
}
c.JSON(http.StatusOK, gin.H{
"metadata": h.wizardSchema.Metadata,
"steps": h.wizardSchema.Steps,
"presets": h.wizardSchema.Presets,
"assistant": gin.H{
"enabled": h.wizardSchema.FundingAssistant.Enabled,
"quick_prompts": h.wizardSchema.FundingAssistant.QuickPrompts,
},
})
}
// SaveWizardStep saves wizard step data
// POST /sdk/v1/funding/applications/:id/wizard
func (h *FundingHandlers) SaveWizardStep(c *gin.Context) {
userID := rbac.GetUserID(c)
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"})
return
}
var req funding.SaveWizardStepRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Save step data
if err := h.store.SaveWizardStep(c.Request.Context(), id, req.Step, req.Data); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Get updated progress
progress, err := h.store.GetWizardProgress(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Add history entry
h.store.AddHistoryEntry(c.Request.Context(), &funding.ApplicationHistoryEntry{
ApplicationID: id,
Action: "wizard_step_saved",
PerformedBy: userID,
Notes: fmt.Sprintf("Schritt %d gespeichert", req.Step),
})
c.JSON(http.StatusOK, progress)
}
// AskAssistant handles LLM assistant queries
// POST /sdk/v1/funding/wizard/ask
func (h *FundingHandlers) AskAssistant(c *gin.Context) {
var req funding.AssistantRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if h.wizardSchema == nil || !h.wizardSchema.FundingAssistant.Enabled {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "assistant not available"})
return
}
// Build system prompt with step context
systemPrompt := h.wizardSchema.FundingAssistant.SystemPrompt
if stepContext, ok := h.wizardSchema.FundingAssistant.StepContexts[req.CurrentStep]; ok {
systemPrompt += "\n\nKontext fuer diesen Schritt:\n" + stepContext
}
// Build messages
messages := []llm.Message{
{Role: "system", Content: systemPrompt},
}
for _, msg := range req.History {
messages = append(messages, llm.Message{
Role: msg.Role,
Content: msg.Content,
})
}
messages = append(messages, llm.Message{
Role: "user",
Content: req.Question,
})
// Generate response using registry
chatReq := &llm.ChatRequest{
Messages: messages,
Temperature: 0.3,
MaxTokens: 1000,
}
response, err := h.providerRegistry.Chat(c.Request.Context(), chatReq)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, funding.AssistantResponse{
Answer: response.Message.Content,
})
}
// ============================================================================
// Status Endpoints
// ============================================================================
// SubmitApplication submits an application for review
// POST /sdk/v1/funding/applications/:id/submit
func (h *FundingHandlers) SubmitApplication(c *gin.Context) {
userID := rbac.GetUserID(c)
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"})
return
}
app, err := h.store.GetApplication(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// Validate that all required steps are completed
progress, _ := h.store.GetWizardProgress(c.Request.Context(), id)
if progress == nil || len(progress.CompletedSteps) < app.TotalSteps {
c.JSON(http.StatusBadRequest, gin.H{"error": "not all required steps completed"})
return
}
// Update status
app.Status = funding.ApplicationStatusSubmitted
now := time.Now()
app.SubmittedAt = &now
app.UpdatedBy = userID
if err := h.store.UpdateApplication(c.Request.Context(), app); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Add history entry
h.store.AddHistoryEntry(c.Request.Context(), &funding.ApplicationHistoryEntry{
ApplicationID: id,
Action: "submitted",
PerformedBy: userID,
Notes: "Antrag eingereicht",
})
c.JSON(http.StatusOK, app)
}
// ============================================================================
// Export Endpoints
// ============================================================================
// ExportApplication exports all documents as ZIP
// GET /sdk/v1/funding/applications/:id/export
func (h *FundingHandlers) ExportApplication(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"})
return
}
app, err := h.store.GetApplication(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// Generate export (this will be implemented in export.go)
// For now, return a placeholder response
c.JSON(http.StatusOK, gin.H{
"message": "Export generation initiated",
"application_id": app.ID,
"status": "processing",
})
}
// PreviewApplication generates a PDF preview
// GET /sdk/v1/funding/applications/:id/preview
func (h *FundingHandlers) PreviewApplication(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"})
return
}
app, err := h.store.GetApplication(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// Generate PDF preview (placeholder)
c.JSON(http.StatusOK, gin.H{
"message": "Preview generation initiated",
"application_id": app.ID,
})
}
// ============================================================================
// Bundesland Profile Endpoints
// ============================================================================
// GetBundeslandProfiles returns all bundesland profiles
// GET /sdk/v1/funding/bundeslaender
func (h *FundingHandlers) GetBundeslandProfiles(c *gin.Context) {
if h.bundeslandProfiles == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "bundesland profiles not loaded"})
return
}
c.JSON(http.StatusOK, h.bundeslandProfiles)
}
// GetBundeslandProfile returns a specific bundesland profile
// GET /sdk/v1/funding/bundeslaender/:state
func (h *FundingHandlers) GetBundeslandProfile(c *gin.Context) {
state := c.Param("state")
if h.bundeslandProfiles == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "bundesland profiles not loaded"})
return
}
profile, ok := h.bundeslandProfiles[state]
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "bundesland not found"})
return
}
c.JSON(http.StatusOK, profile)
}
// ============================================================================
// Statistics Endpoint
// ============================================================================
// GetStatistics returns funding statistics
// GET /sdk/v1/funding/statistics
func (h *FundingHandlers) GetStatistics(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
stats, err := h.store.GetStatistics(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
// ============================================================================
// History Endpoint
// ============================================================================
// GetApplicationHistory returns the audit trail
// GET /sdk/v1/funding/applications/:id/history
func (h *FundingHandlers) GetApplicationHistory(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"})
return
}
history, err := h.store.GetHistory(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, history)
}