All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 28s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Successful in 1m45s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 21s
- edu-search-service von breakpilot-pwa nach breakpilot-lehrer kopiert (ohne vendor) - opensearch + edu-search-service in docker-compose.yml hinzugefuegt - voice-service aus docker-compose.yml entfernt (jetzt in breakpilot-core) - geo-service aus docker-compose.yml entfernt (nicht mehr benoetigt) - CI/CD: edu-search-service zu Gitea Actions und Woodpecker hinzugefuegt (Go lint, test mit go mod download, build, SBOM) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
188 lines
5.3 KiB
Go
188 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/breakpilot/edu-search-service/internal/api/handlers"
|
|
"github.com/breakpilot/edu-search-service/internal/config"
|
|
"github.com/breakpilot/edu-search-service/internal/database"
|
|
"github.com/breakpilot/edu-search-service/internal/indexer"
|
|
"github.com/breakpilot/edu-search-service/internal/orchestrator"
|
|
"github.com/breakpilot/edu-search-service/internal/search"
|
|
"github.com/breakpilot/edu-search-service/internal/staff"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func main() {
|
|
log.Println("Starting edu-search-service...")
|
|
|
|
// Load configuration
|
|
cfg := config.Load()
|
|
log.Printf("Configuration loaded: Port=%s, OpenSearch=%s, Index=%s",
|
|
cfg.Port, cfg.OpenSearchURL, cfg.IndexName)
|
|
|
|
// Initialize OpenSearch indexer client
|
|
indexClient, err := indexer.NewClient(
|
|
cfg.OpenSearchURL,
|
|
cfg.OpenSearchUsername,
|
|
cfg.OpenSearchPassword,
|
|
cfg.IndexName,
|
|
)
|
|
if err != nil {
|
|
log.Fatalf("Failed to create indexer client: %v", err)
|
|
}
|
|
|
|
// Create index if not exists
|
|
ctx := context.Background()
|
|
if err := indexClient.CreateIndex(ctx); err != nil {
|
|
log.Printf("Warning: Could not create index (may already exist): %v", err)
|
|
}
|
|
|
|
// Initialize search service
|
|
searchService, err := search.NewService(
|
|
cfg.OpenSearchURL,
|
|
cfg.OpenSearchUsername,
|
|
cfg.OpenSearchPassword,
|
|
cfg.IndexName,
|
|
)
|
|
if err != nil {
|
|
log.Fatalf("Failed to create search service: %v", err)
|
|
}
|
|
|
|
// Initialize seed store for admin API
|
|
if err := handlers.InitSeedStore(cfg.SeedsDir); err != nil {
|
|
log.Printf("Warning: Could not initialize seed store: %v", err)
|
|
}
|
|
|
|
// Create handler
|
|
handler := handlers.NewHandler(cfg, searchService, indexClient)
|
|
|
|
// Initialize PostgreSQL for Staff/Publications database
|
|
dbCfg := &database.Config{
|
|
Host: cfg.DBHost,
|
|
Port: cfg.DBPort,
|
|
User: cfg.DBUser,
|
|
Password: cfg.DBPassword,
|
|
DBName: cfg.DBName,
|
|
SSLMode: cfg.DBSSLMode,
|
|
}
|
|
|
|
db, err := database.New(ctx, dbCfg)
|
|
if err != nil {
|
|
log.Printf("Warning: Could not connect to PostgreSQL for staff database: %v", err)
|
|
log.Println("Staff/Publications features will be disabled")
|
|
} else {
|
|
defer db.Close()
|
|
log.Println("Connected to PostgreSQL for staff/publications database")
|
|
|
|
// Run migrations
|
|
if err := db.RunMigrations(ctx); err != nil {
|
|
log.Printf("Warning: Could not run migrations: %v", err)
|
|
}
|
|
}
|
|
|
|
// Create repository for Staff handlers (may be nil if DB connection failed)
|
|
var repo *database.Repository
|
|
if db != nil {
|
|
repo = database.NewRepository(db)
|
|
}
|
|
|
|
// Setup Gin router
|
|
gin.SetMode(gin.ReleaseMode)
|
|
router := gin.New()
|
|
router.Use(gin.Recovery())
|
|
router.Use(gin.Logger())
|
|
|
|
// CORS middleware
|
|
router.Use(func(c *gin.Context) {
|
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
|
|
if c.Request.Method == "OPTIONS" {
|
|
c.AbortWithStatus(204)
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
})
|
|
|
|
// Setup routes
|
|
handlers.SetupRoutes(router, handler, cfg.APIKey)
|
|
|
|
// Setup Staff/Publications routes if database is available
|
|
if repo != nil {
|
|
staffHandlers := handlers.NewStaffHandlers(repo, cfg.StaffCrawlerEmail)
|
|
apiV1 := router.Group("/api/v1")
|
|
staffHandlers.RegisterRoutes(apiV1)
|
|
log.Println("Staff/Publications API routes registered")
|
|
|
|
// Setup AI Extraction routes for vast.ai integration
|
|
aiHandlers := handlers.NewAIExtractionHandlers(repo)
|
|
aiHandlers.RegisterRoutes(apiV1)
|
|
log.Println("AI Extraction API routes registered")
|
|
}
|
|
|
|
// Setup Orchestrator routes if database is available
|
|
if db != nil {
|
|
orchRepo := orchestrator.NewPostgresRepository(db.Pool)
|
|
|
|
// Create real crawlers with adapters for orchestrator interface
|
|
staffCrawler := staff.NewStaffCrawler(repo)
|
|
staffAdapter := staff.NewOrchestratorAdapter(staffCrawler, repo)
|
|
pubAdapter := staff.NewPublicationOrchestratorAdapter(repo)
|
|
|
|
orch := orchestrator.NewOrchestrator(orchRepo, staffAdapter, pubAdapter)
|
|
orchHandler := handlers.NewOrchestratorHandler(orch, orchRepo)
|
|
|
|
v1 := router.Group("/v1")
|
|
v1.Use(handlers.AuthMiddleware(cfg.APIKey))
|
|
handlers.SetupOrchestratorRoutes(v1, orchHandler)
|
|
log.Println("Orchestrator API routes registered")
|
|
|
|
// Setup Audience routes (reuses orchRepo which implements AudienceRepository)
|
|
audienceHandler := handlers.NewAudienceHandler(orchRepo)
|
|
handlers.SetupAudienceRoutes(v1, audienceHandler)
|
|
log.Println("Audience API routes registered")
|
|
}
|
|
|
|
// Create HTTP server
|
|
srv := &http.Server{
|
|
Addr: ":" + cfg.Port,
|
|
Handler: router,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 30 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
// Start server in goroutine
|
|
go func() {
|
|
log.Printf("Server listening on port %s", cfg.Port)
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("Server error: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Graceful shutdown
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
|
|
log.Println("Shutting down server...")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
log.Fatalf("Server forced to shutdown: %v", err)
|
|
}
|
|
|
|
log.Println("Server exited")
|
|
}
|