feat(admin): all-podcasts view — disk-first maintenance for every feed
§0 item 8 (Phase-4 item 1 pulled forward, user directive: maintenance
tooling ships WITH the features it maintains):
- /admin/podcasts (AdminMiddleware): every feed from EVERY user via the
RLS admin escape (first-admin identity, poller/fetcher contract) —
feed + host, library, subscriber count/names, downloaded/total, DISK
USAGE, last fetch. Sorted disk DESC ('what's eating the disk'),
totals header (feeds · episodes · bytes on disk).
- Per-feed actions: Refresh · Delete files (rows → pending, disk freed;
confirms spell out the re-materialization contract) · Remove feed
(rows + files for everyone — the same admin delete as the detail
page). All confirms ride the global confirm modal.
- New sqlc: ListAllPodcastFeedsAdmin (correlated aggregates),
ResetPodcastEpisodeFileDeleted / ResetPodcastFeedFilesDeleted —
downloaded rows reset to pending with download-derived facts cleared
(audio_metadata, chapter_count, cover, size, mime); file_path KEPT
(fetch-maintained layout — a re-download lands on the same path).
file deletion via the existing DeleteFeedFiles file-removal pattern
(episode files + .cover.jpg siblings).
- Poll upsert never touches download_state, so deleted files STAY
deleted — a feed refresh re-lists metadata but re-materializes
nothing (pinned by TestAdminDeleteEpisodeFiles).
- Per-episode admin delete-file: POST /feeds/:id/episodes/:eid/
delete-file (admin) — one episode's file gone, row pending, siblings
untouched; HX swaps the episode list. (Found while testing: the
handler first read c.Param("id") — the FEED's uuid — instead of
:eid; the 404 'episode not found' trail led straight to it.)
- Tests: admin sees other users' feeds + subscriber names; regular
users 403 on page AND actions; single + feed-level deletion reset
rows/clear facts/delete files; refresh leaves pending rows pending.
- Admin nav gains 'Podcasts' under Administration.
Wrong-gotcha note for future agents: parseUUID on c.Param('id') inside
a /feeds/:id/episodes/:eid route resolves the FEED, not the episode.
This commit is contained in:
1 parent
b293741eb1
commit
0741e98ea5
12 files changed
+1191
-47
No files matched your search
@@ -58,6 +58,68 @@ func (h *PodcastHandler) hxEpisodeList(c *echo.Context, feedID string, includePo
|
||||
return hxAccepted(c, "")
|
||||
}
|
||||
|
||||
// AdminDeleteFeedFiles POST /api/podcasts/feeds/:id/delete-files —
|
||||
// admin-only (§0 item 8): delete every downloaded episode file of the
|
||||
// feed; rows reset to pending. HX answers HX-Refresh (the admin table
|
||||
// re-renders with fresh aggregates).
|
||||
func (h *PodcastHandler) AdminDeleteFeedFiles(c *echo.Context) error {
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
if user.Role != "admin" {
|
||||
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin only"})
|
||||
}
|
||||
feedID, err := parseUUID(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid feed id"})
|
||||
}
|
||||
// media_items writes run under the first-admin identity (invariant
|
||||
// 12 — same contract as the fetcher/worker).
|
||||
actx, err := h.worker.AdminCtx(c.Request().Context())
|
||||
if err != nil {
|
||||
return podcastErrorResponse(c, err)
|
||||
}
|
||||
reset, err := h.podcasts.AdminDeleteFeedFiles(actx, feedID)
|
||||
if err != nil {
|
||||
return podcastErrorResponse(c, err)
|
||||
}
|
||||
log.Printf("[PODCAST] admin: deleted files for %d episode(s) of feed %s (user %s)", reset, feedID, user.Username)
|
||||
if isHXRequest(c) {
|
||||
c.Response().Header().Set("HX-Refresh", "true")
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"reset_episodes": reset})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"reset_episodes": reset})
|
||||
}
|
||||
|
||||
// AdminDeleteEpisodeFile POST /api/podcasts/episodes/:id/delete-file —
|
||||
// admin-only (§0 item 8): ONE episode's file deleted, row reset to
|
||||
// pending. HX answers with the refreshed episode list.
|
||||
func (h *PodcastHandler) AdminDeleteEpisodeFile(c *echo.Context) error {
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
if user.Role != "admin" {
|
||||
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin only"})
|
||||
}
|
||||
episodeID, err := parseUUID(c.Param("eid"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid episode id"})
|
||||
}
|
||||
actx, err := h.worker.AdminCtx(c.Request().Context())
|
||||
if err != nil {
|
||||
return podcastErrorResponse(c, err)
|
||||
}
|
||||
if err := h.podcasts.AdminDeleteEpisodeFile(actx, episodeID); err != nil {
|
||||
return podcastErrorResponse(c, err)
|
||||
}
|
||||
log.Printf("[PODCAST] admin: deleted file of episode %s (user %s)", episodeID, user.Username)
|
||||
if isHXRequest(c) {
|
||||
// Re-render the episode list: the row needs its fresh state. The
|
||||
// episode's feed id comes from the row we just reset.
|
||||
if ep, err := h.db.GetPodcastEpisode(actx, episodeID); err == nil && ep.SourceFeedID.Valid {
|
||||
return h.hxEpisodeList(c, ep.SourceFeedID.String(), false, false)
|
||||
}
|
||||
return hxAccepted(c, "")
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"message": "file deleted"})
|
||||
}
|
||||
|
||||
// flipSubscriptionToAll implements issue #3: Download All (button or
|
||||
// add-feed checkbox) sets the CALLER's subscription policy to 'all' —
|
||||
// per-user, never global (files are shared D6, policies are not). The
|
||||
|
||||
Reference in new issue
Block a user