From 023a7ab92604bc76b893cfa99b27d3bbb9dceb01 Mon Sep 17 00:00:00 2001 From: Satsuki Akiba Date: Sun, 8 Jun 2025 17:51:59 +0900 Subject: [PATCH] Add file deleting functionality to the admin panel --- admin-panel/src/App.tsx | 180 +++++++++++++++++++++++++++++++++++++++- admin-server.ts | 28 +++++++ services/database.ts | 36 ++++++++ 3 files changed, 240 insertions(+), 4 deletions(-) diff --git a/admin-panel/src/App.tsx b/admin-panel/src/App.tsx index 7089116..b7458f4 100644 --- a/admin-panel/src/App.tsx +++ b/admin-panel/src/App.tsx @@ -46,11 +46,30 @@ interface FeedRequest { adminNotes?: string; } +interface Episode { + id: string; + title: string; + description?: string; + audioPath: string; + duration?: number; + fileSize?: number; + createdAt: string; + articleId: string; + articleTitle: string; + articleLink: string; + articlePubDate: string; + feedId: string; + feedTitle?: string; + feedUrl: string; + feedCategory?: string; +} + function App() { const [feeds, setFeeds] = useState([]); const [stats, setStats] = useState(null); const [envVars, setEnvVars] = useState({}); const [feedRequests, setFeedRequests] = useState([]); + const [episodes, setEpisodes] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); @@ -62,7 +81,7 @@ function App() { {}, ); const [activeTab, setActiveTab] = useState< - "dashboard" | "feeds" | "env" | "batch" | "requests" + "dashboard" | "feeds" | "episodes" | "env" | "batch" | "requests" >("dashboard"); useEffect(() => { @@ -72,28 +91,31 @@ function App() { const loadData = async () => { setLoading(true); try { - const [feedsRes, statsRes, envRes, requestsRes] = await Promise.all([ + const [feedsRes, statsRes, envRes, requestsRes, episodesRes] = await Promise.all([ fetch("/api/admin/feeds"), fetch("/api/admin/stats"), fetch("/api/admin/env"), fetch("/api/admin/feed-requests"), + fetch("/api/admin/episodes"), ]); - if (!feedsRes.ok || !statsRes.ok || !envRes.ok || !requestsRes.ok) { + if (!feedsRes.ok || !statsRes.ok || !envRes.ok || !requestsRes.ok || !episodesRes.ok) { throw new Error("Failed to load data"); } - const [feedsData, statsData, envData, requestsData] = await Promise.all([ + const [feedsData, statsData, envData, requestsData, episodesData] = await Promise.all([ feedsRes.json(), statsRes.json(), envRes.json(), requestsRes.json(), + episodesRes.json(), ]); setFeeds(feedsData); setStats(statsData); setEnvVars(envData); setFeedRequests(requestsData); + setEpisodes(episodesData); setError(null); } catch (err) { setError("データの読み込みに失敗しました"); @@ -308,6 +330,34 @@ function App() { setApprovalNotes({ ...approvalNotes, [requestId]: notes }); }; + const deleteEpisode = async (episodeId: string, episodeTitle: string) => { + if ( + !confirm( + `本当にエピソード「${episodeTitle}」を削除しますか?\n\n音声ファイルも削除され、この操作は取り消せません。`, + ) + ) { + return; + } + + try { + const res = await fetch(`/api/admin/episodes/${episodeId}`, { + method: "DELETE", + }); + + const data = await res.json(); + + if (res.ok) { + setSuccess(data.message); + loadData(); + } else { + setError(data.error || "エピソード削除に失敗しました"); + } + } catch (err) { + setError("エピソード削除に失敗しました"); + console.error("Error deleting episode:", err); + } + }; + const filteredRequests = feedRequests.filter((request) => { if (requestFilter === "all") return true; return request.status === requestFilter; @@ -348,6 +398,12 @@ function App() { > フィード管理 + + + + ))} + + + )} + + )} + {activeTab === "batch" && ( <>

バッチ処理管理

diff --git a/admin-server.ts b/admin-server.ts index 2e49d47..c494b38 100644 --- a/admin-server.ts +++ b/admin-server.ts @@ -8,6 +8,7 @@ import { batchScheduler } from "./services/batch-scheduler.js"; import { config, validateConfig } from "./services/config.js"; import { deleteFeed, + deleteEpisode, fetchAllEpisodes, fetchEpisodesWithArticles, getAllCategories, @@ -254,6 +255,33 @@ app.get("/api/admin/episodes/simple", async (c) => { } }); +app.delete("/api/admin/episodes/:id", async (c) => { + try { + const episodeId = c.req.param("id"); + + if (!episodeId || episodeId.trim() === "") { + return c.json({ error: "Episode ID is required" }, 400); + } + + console.log("🗑️ Admin deleting episode ID:", episodeId); + + const deleted = await deleteEpisode(episodeId); + + if (deleted) { + return c.json({ + result: "DELETED", + message: "Episode deleted successfully", + episodeId, + }); + } else { + return c.json({ error: "Episode not found" }, 404); + } + } catch (error) { + console.error("Error deleting episode:", error); + return c.json({ error: "Failed to delete episode" }, 500); + } +}); + // Database diagnostic endpoint app.get("/api/admin/db-diagnostic", async (c) => { try { diff --git a/services/database.ts b/services/database.ts index 3c16de9..89060aa 100644 --- a/services/database.ts +++ b/services/database.ts @@ -1,6 +1,7 @@ import { Database } from "bun:sqlite"; import crypto from "crypto"; import fs from "fs"; +import path from "path"; import { config } from "./config.js"; // Database integrity fixes function @@ -1208,6 +1209,41 @@ export async function getFeedCategoryMigrationStatus(): Promise<{ } } +export async function deleteEpisode(episodeId: string): Promise { + try { + // Get episode info first to find the audio file path + const episodeStmt = db.prepare("SELECT audio_path FROM episodes WHERE id = ?"); + const episode = episodeStmt.get(episodeId) as any; + + if (!episode) { + return false; // Episode not found + } + + // Delete from database + const deleteStmt = db.prepare("DELETE FROM episodes WHERE id = ?"); + const result = deleteStmt.run(episodeId); + + // If database deletion successful, try to delete the audio file + if (result.changes > 0 && episode.audio_path) { + try { + const fullAudioPath = path.join(config.paths.projectRoot, episode.audio_path); + if (fs.existsSync(fullAudioPath)) { + fs.unlinkSync(fullAudioPath); + console.log(`🗑️ Deleted audio file: ${fullAudioPath}`); + } + } catch (fileError) { + console.warn(`⚠️ Failed to delete audio file ${episode.audio_path}:`, fileError); + // Don't fail the operation if file deletion fails + } + } + + return result.changes > 0; + } catch (error) { + console.error("Error deleting episode:", error); + throw error; + } +} + export function closeDatabase(): void { db.close(); }