package main import ( "context" "database/sql" "html/template" "net/http" "github.com/gin-gonic/gin" _ "github.com/jackc/pgx/v5/stdlib" "github.com/pressly/goose/v3" "github.com/sirupsen/logrus" ) // TemplateWrapper adapts html/template.Template to TemplateExecutor // (implements Execute(http.ResponseWriter, interface{}) error) type TemplateWrapper struct { Tmpl *template.Template } func (tw *TemplateWrapper) Execute(wr http.ResponseWriter, data interface{}) error { return tw.Tmpl.Execute(wr, data) } func main() { logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) logrus.SetLevel(logrus.InfoLevel) if err := loadConfig("config/config.yaml"); err != nil { logrus.Fatalf("Failed to load config/config.yaml: %v", err) } logrus.Infof("Loaded config: %+v", appConfig) visitDB := NewVisitDB() if err := loadUITemplate("web_templates/ui.html"); err != nil { logrus.Fatalf("Failed to load web_templates/ui.html: %v", err) } if err := loadDBEditTemplate("web_templates/ui_dbedit.html"); err != nil { logrus.Fatalf("Failed to load web_templates/ui_dbedit.html: %v", err) } if err := loadAdminChatsTemplate("web_templates/ui_admin_chats.html"); err != nil { logrus.Fatalf("Failed to load web_templates/ui_admin_chats.html: %v", err) } uiAdminLoginTmpl := &TemplateWrapper{Tmpl: template.Must(template.ParseFiles("web_templates/ui_admin_login.html"))} // Initialize PostgreSQL repository first dsn := buildDefaultDSN() logrus.Info("Connecting to PostgreSQL with DSN: ", dsn) // Run goose migrations using database/sql with pgx driver before initializing repository if dsn != "" { sqlDB, err := sql.Open("pgx", dsn) if err != nil { logrus.Fatalf("Failed to open database for migration: %v", err) } defer sqlDB.Close() goose.SetDialect("postgres") if err := goose.Up(sqlDB, "migrations"); err != nil { logrus.Fatalf("Goose migration failed: %v", err) } logrus.Info("Database migrations applied successfully (goose)") } repo, err := NewPGChatRepository(context.Background(), dsn) if err != nil { logrus.WithError(err).Warn("PostgreSQL repository disabled (connection failed)") } else if repo == nil { logrus.Info("PostgreSQL repository not configured (no DSN)") } // defer repo.Close() // optionally enable // Initialize LLM client llm := NewLLMClientFromEnv(repo) // Launch background backfill of sentence embeddings (non-blocking) startSentenceEmbeddingBackfill(repo, llm, &visitDB) // Wrap templates for controller uiTmpl := &TemplateWrapper{Tmpl: uiTemplate} uiDBEditTmpl := &TemplateWrapper{Tmpl: uiDBEditTemplate} uiAdminChatsTmpl := &TemplateWrapper{Tmpl: uiAdminChatsTemplate} // Create controller ctrl := NewController( repo, llm, &visitDB, uiTmpl, uiDBEditTmpl, uiAdminChatsTmpl, uiAdminLoginTmpl, ) r := gin.Default() // Admin login/logout routes (no auth) r.GET("/admin/login", ctrl.AdminLoginPage) r.POST("/admin/login", ctrl.AdminLogin) r.GET("/admin/logout", ctrl.AdminLogout) // All /admin* routes require authentication except login/logout admin := r.Group("/admin", AdminAuthMiddleware()) admin.GET("/", ctrl.AdminUI) admin.GET("/chats", ctrl.ListChatInteractions) admin.GET("/chats/events", ctrl.ListLLMEvents) admin.GET("/chats/ui", ctrl.AdminChatsUI) admin.POST("/chats/snapshot", ctrl.SaveKnowledgeModel) admin.GET("/chats/snapshots", ctrl.ListKnowledgeModels) admin.GET("/chats/snapshot/:id", ctrl.GetKnowledgeModel) r.GET("/", ctrl.RootUI) r.GET("/health", ctrl.Health) r.POST("/chat", ctrl.HandleChat) r.GET("/db.yaml", ctrl.DownloadDB) r.POST("/admin/create-initial-admin", ctrl.CreateInitialAdmin) r.Run(":8080") }