package router // Podcast routes (plan §8). All under /api/podcasts with the JWT // middleware; scoping is RLS's job (D20) — handlers add no visibility // plumbing. import ( "bytes" "net/http" "bookhoard/templates" "github.com/labstack/echo/v5" ) func registerPodcastRoutes(cfg *Config) { g := cfg.Echo.Group("/api/podcasts", createJWTMiddleware(cfg)) // Episode-list renderer (issue #1): htmx actions answer with the // refreshed partial directly — no event indirection to miss. The // view assembly lives here (templates) while the action logic lives // in the handler; this closure is the sanctioned seam. cfg.PodcastHandler.SetEpisodeListRenderer(func(c *echo.Context, feedID string, includePolicySelect, includeOrderSelect bool) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { return err } feedUUID, err := parsePodcastUUID(feedID) if err != nil { return err } data, err := buildPodcastDetailData(c, cfg, user, feedUUID) if err != nil { return err } var buf bytes.Buffer render := templates.PodcastEpisodeList if templates.HasActiveDownloads(data.Episodes) { render = templates.PodcastEpisodeListPolling } if err := render(*data).Render(c.Request().Context(), &buf); err != nil { return err } if includePolicySelect { // Issue #3: out-of-band swap of the auto-download select — // the dropdown reflects the flipped policy immediately. if err := templates.PodcastAutoDownloadSelectOOB(data.Feed).Render(c.Request().Context(), &buf); err != nil { return err } } if includeOrderSelect { // §0 item 3: out-of-band swap of the order select — the // "Reverse feed order" button flips to reverse; the // dropdown mirrors the server state immediately. if err := templates.PodcastOrderSelectOOB(data.Feed.ID, data.Order).Render(c.Request().Context(), &buf); err != nil { return err } } return c.HTML(http.StatusOK, buf.String()) }) g.POST("/feeds", cfg.PodcastHandler.AddFeed) g.GET("/feeds", cfg.PodcastHandler.ListFeeds) g.GET("/search", cfg.PodcastHandler.Search) // :id-scoped (RLS answers 404 for feeds outside the caller's scope). feeds := g.Group("/feeds/:id") feeds.GET("", cfg.PodcastHandler.GetFeed) feeds.GET("/sequence", cfg.PodcastHandler.GetFeedSequence) feeds.PUT("/order", cfg.PodcastHandler.SetFeedOrder) feeds.PUT("/subscription", cfg.PodcastHandler.UpdateSubscription) feeds.DELETE("/subscription", cfg.PodcastHandler.DeleteSubscription) feeds.POST("/refresh", cfg.PodcastHandler.RefreshFeed) feeds.POST("/episodes/:eid/download", cfg.PodcastHandler.DownloadEpisodes) feeds.DELETE("", cfg.PodcastHandler.DeleteFeed) // admin-only (handler checks role) }