Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

151 řádky
4.6KB

  1. package app
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "strings"
  8. "time"
  9. "github.com/go-chi/chi/v5"
  10. "qctextbuilder/internal/buildsvc"
  11. "qctextbuilder/internal/config"
  12. "qctextbuilder/internal/domain"
  13. "qctextbuilder/internal/draftsvc"
  14. "qctextbuilder/internal/httpserver"
  15. "qctextbuilder/internal/httpserver/handlers"
  16. "qctextbuilder/internal/httpserver/views"
  17. "qctextbuilder/internal/logging"
  18. "qctextbuilder/internal/mapping"
  19. "qctextbuilder/internal/onboarding"
  20. "qctextbuilder/internal/polling"
  21. "qctextbuilder/internal/qcclient"
  22. "qctextbuilder/internal/store"
  23. "qctextbuilder/internal/store/memory"
  24. "qctextbuilder/internal/store/sqlite"
  25. "qctextbuilder/internal/templatesvc"
  26. )
  27. type App struct {
  28. server *httpserver.Server
  29. pollingSvc *polling.Service
  30. }
  31. func New(cfg config.Config) (*App, error) {
  32. logger := logging.New()
  33. var (
  34. templateStore store.TemplateStore
  35. manifestStore store.ManifestStore
  36. buildStore store.BuildStore
  37. draftStore store.DraftStore
  38. settingsStore store.SettingsStore
  39. )
  40. driver := strings.ToLower(strings.TrimSpace(cfg.DBDriver))
  41. switch driver {
  42. case "", "sqlite":
  43. sqliteStore, err := sqlite.New(cfg.DBURL)
  44. if err != nil {
  45. return nil, fmt.Errorf("init sqlite store: %w", err)
  46. }
  47. templateStore = sqliteStore
  48. manifestStore = sqliteStore
  49. buildStore = sqliteStore
  50. draftStore = sqliteStore
  51. settingsStore = sqliteStore
  52. default:
  53. memStore := memory.New()
  54. templateStore = memStore
  55. manifestStore = memStore
  56. buildStore = memStore
  57. draftStore = memStore
  58. settingsStore = memStore
  59. }
  60. qc := qcclient.New(cfg.QCBaseURL, cfg.QCToken, 15*time.Second, logger)
  61. templateSvc := templatesvc.New(qc, templateStore, manifestStore)
  62. onboardSvc := onboarding.New(qc, templateStore, manifestStore)
  63. draftSvc := draftsvc.New(draftStore, templateStore, manifestStore)
  64. mappingSvc := mapping.New()
  65. buildSvc := buildsvc.New(qc, templateStore, manifestStore, buildStore, mappingSvc, time.Duration(cfg.PollTimeoutSeconds)*time.Second)
  66. pollingSvc := polling.New(buildSvc, buildStore, time.Duration(cfg.PollIntervalSeconds)*time.Second, cfg.PollMaxConcurrent, logger)
  67. api := handlers.NewAPI(templateSvc, onboardSvc, draftSvc, buildSvc)
  68. _ = settingsStore.UpsertSettings(context.Background(), domain.AppSettings{
  69. QCBaseURL: cfg.QCBaseURL,
  70. QCBearerTokenEncrypted: cfg.QCToken,
  71. LanguageOutputMode: "EN",
  72. JobPollIntervalSeconds: cfg.PollIntervalSeconds,
  73. JobPollTimeoutSeconds: cfg.PollTimeoutSeconds,
  74. })
  75. renderer, err := views.NewRenderer("web/templates/*.gohtml")
  76. if err != nil {
  77. return nil, fmt.Errorf("init renderer: %w", err)
  78. }
  79. ui := handlers.NewUI(templateSvc, onboardSvc, draftSvc, buildSvc, cfg, renderer)
  80. server := httpserver.New(cfg.HTTPAddr, logger, func(r chi.Router) {
  81. r.Get("/", ui.Home)
  82. r.Get("/settings", ui.Settings)
  83. r.Get("/templates", ui.Templates)
  84. r.Post("/templates/sync", ui.SyncTemplates)
  85. r.Get("/templates/{id}", ui.TemplateDetail)
  86. r.Post("/templates/{id}/onboard", ui.OnboardTemplate)
  87. r.Post("/templates/{id}/fields", ui.UpdateTemplateFields)
  88. r.Get("/builds/new", ui.BuildNew)
  89. r.Post("/builds/drafts", ui.SaveDraft)
  90. r.Post("/builds", ui.CreateBuild)
  91. r.Get("/builds/{id}", ui.BuildDetail)
  92. r.Post("/builds/{id}/poll", ui.PollBuild)
  93. r.Post("/builds/{id}/fetch-editor-url", ui.FetchEditorURL)
  94. r.Get("/healthz", api.Health)
  95. r.Route("/api", func(r chi.Router) {
  96. r.Post("/templates/sync", api.SyncTemplates)
  97. r.Get("/templates", api.ListTemplates)
  98. r.Get("/templates/{id}", api.GetTemplateDetail)
  99. r.Post("/templates/{id}/onboard", api.OnboardTemplate)
  100. r.Put("/templates/{id}/fields", api.UpdateTemplateFields)
  101. r.Post("/drafts/intake", api.IntakeDraft)
  102. r.Get("/drafts", api.ListDrafts)
  103. r.Get("/drafts/{id}", api.GetDraft)
  104. r.Put("/drafts/{id}", api.UpdateDraft)
  105. r.Post("/site-builds", api.StartBuild)
  106. r.Get("/site-builds/{id}", api.GetBuild)
  107. r.Post("/site-builds/{id}/poll", api.PollBuildOnce)
  108. r.Post("/site-builds/{id}/fetch-editor-url", api.FetchBuildEditorURL)
  109. })
  110. })
  111. return &App{server: server, pollingSvc: pollingSvc}, nil
  112. }
  113. func (a *App) Run(ctx context.Context) error {
  114. go func() {
  115. if err := a.pollingSvc.Start(ctx); err != nil {
  116. // polling is best-effort in milestone-2; request flow works without supervisor
  117. }
  118. }()
  119. errCh := make(chan error, 1)
  120. go func() {
  121. if err := a.server.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {
  122. errCh <- fmt.Errorf("http run: %w", err)
  123. }
  124. close(errCh)
  125. }()
  126. select {
  127. case <-ctx.Done():
  128. shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  129. defer cancel()
  130. return a.server.Shutdown(shutdownCtx)
  131. case err := <-errCh:
  132. return err
  133. }
  134. }