package main import ( "context" "database/sql" "embed" "flag" "fmt" "log" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" ) //go:embed migrations/*.sql var migrationsFS embed.FS type Server struct { db *sql.DB issuesDir string flowsDir string hub *SSEHub } func main() { port := flag.Int("port", 8487, "HTTP port") dbPath := flag.String("db", "operations.db", "SQLite path") registryRoot := flag.String("registry", "", "fn_registry root (default: auto-detect from cwd)") flag.Parse() root := *registryRoot if root == "" { root = detectRegistryRoot() } issuesDir := filepath.Join(root, "dev", "issues") flowsDir := filepath.Join(root, "dev", "flows") for _, d := range []string{issuesDir, flowsDir} { if _, err := os.Stat(d); err != nil { log.Fatalf("missing dir %s: %v", d, err) } } db, err := openDB(*dbPath) if err != nil { log.Fatalf("openDB: %v", err) } defer db.Close() if err := applyMigrations(db); err != nil { log.Fatalf("applyMigrations: %v", err) } srv := &Server{ db: db, issuesDir: issuesDir, flowsDir: flowsDir, hub: newSSEHub(), } if err := srv.ingestAll(); err != nil { log.Fatalf("initial ingest: %v", err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go srv.watchLoop(ctx, srv.issuesDir, "issue") go srv.watchLoop(ctx, srv.flowsDir, "flow") mux := http.NewServeMux() srv.registerRoutes(mux) httpSrv := &http.Server{ Addr: fmt.Sprintf(":%d", *port), Handler: withMiddleware(mux), ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, } go func() { log.Printf("kanban_cpp_backend listening on :%d (registry=%s)", *port, root) if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %v", err) } }() sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) <-sig log.Println("shutdown") shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 5*time.Second) defer cancelShutdown() httpSrv.Shutdown(shutdownCtx) } func detectRegistryRoot() string { wd, _ := os.Getwd() for d := wd; d != "/" && d != "."; d = filepath.Dir(d) { if _, err := os.Stat(filepath.Join(d, "registry.db")); err == nil { return d } } log.Fatalf("could not auto-detect fn_registry root from %s", wd) return "" }