package app import ( "context" "errors" "fmt" "net/http" "time" "github.com/go-chi/chi/v5" "qctextbuilder/internal/buildsvc" "qctextbuilder/internal/config" "qctextbuilder/internal/httpserver" "qctextbuilder/internal/httpserver/handlers" "qctextbuilder/internal/httpserver/views" "qctextbuilder/internal/logging" "qctextbuilder/internal/mapping" "qctextbuilder/internal/onboarding" "qctextbuilder/internal/polling" "qctextbuilder/internal/qcclient" "qctextbuilder/internal/store/memory" "qctextbuilder/internal/templatesvc" ) type App struct { server *httpserver.Server pollingSvc *polling.Service } func New(cfg config.Config) (*App, error) { logger := logging.New() memStore := memory.New() qc := qcclient.New(cfg.QCBaseURL, cfg.QCToken, 15*time.Second, logger) templateSvc := templatesvc.New(qc, memStore, memStore) onboardSvc := onboarding.New(qc, memStore, memStore) mappingSvc := mapping.New() buildSvc := buildsvc.New(qc, memStore, memStore, memStore, mappingSvc, time.Duration(cfg.PollTimeoutSeconds)*time.Second) pollingSvc := polling.New(buildSvc, memStore, time.Duration(cfg.PollIntervalSeconds)*time.Second, cfg.PollMaxConcurrent, logger) api := handlers.NewAPI(templateSvc, onboardSvc, buildSvc) renderer, err := views.NewRenderer("web/templates/*.gohtml") if err != nil { return nil, fmt.Errorf("init renderer: %w", err) } ui := handlers.NewUI(templateSvc, onboardSvc, buildSvc, cfg, renderer) server := httpserver.New(cfg.HTTPAddr, logger, func(r chi.Router) { r.Get("/", ui.Home) r.Get("/settings", ui.Settings) r.Get("/templates", ui.Templates) r.Post("/templates/sync", ui.SyncTemplates) r.Get("/templates/{id}", ui.TemplateDetail) r.Post("/templates/{id}/onboard", ui.OnboardTemplate) r.Post("/templates/{id}/fields", ui.UpdateTemplateFields) r.Get("/builds/new", ui.BuildNew) r.Post("/builds", ui.CreateBuild) r.Get("/builds/{id}", ui.BuildDetail) r.Post("/builds/{id}/poll", ui.PollBuild) r.Post("/builds/{id}/fetch-editor-url", ui.FetchEditorURL) r.Get("/healthz", api.Health) r.Route("/api", func(r chi.Router) { r.Post("/templates/sync", api.SyncTemplates) r.Get("/templates", api.ListTemplates) r.Get("/templates/{id}", api.GetTemplateDetail) r.Post("/templates/{id}/onboard", api.OnboardTemplate) r.Put("/templates/{id}/fields", api.UpdateTemplateFields) r.Post("/site-builds", api.StartBuild) r.Get("/site-builds/{id}", api.GetBuild) r.Post("/site-builds/{id}/poll", api.PollBuildOnce) r.Post("/site-builds/{id}/fetch-editor-url", api.FetchBuildEditorURL) }) }) return &App{server: server, pollingSvc: pollingSvc}, nil } func (a *App) Run(ctx context.Context) error { go func() { if err := a.pollingSvc.Start(ctx); err != nil { // polling is best-effort in milestone-2; request flow works without supervisor } }() errCh := make(chan error, 1) go func() { if err := a.server.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) { errCh <- fmt.Errorf("http run: %w", err) } close(errCh) }() select { case <-ctx.Done(): shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return a.server.Shutdown(shutdownCtx) case err := <-errCh: return err } }