From 6e2700fe3de07b1024542b7fcf2d7dcc03be8611 Mon Sep 17 00:00:00 2001
From: Satsuki Akiba
Date: Wed, 11 Jun 2025 23:03:40 +0900
Subject: [PATCH] Some fixes
---
admin-panel/src/App.tsx | 467 +++++++++++++++++-------
admin-server.ts | 21 +-
frontend/src/App.tsx | 15 +-
frontend/src/components/EpisodeList.tsx | 38 +-
frontend/src/components/FeedDetail.tsx | 10 +-
frontend/src/components/FeedList.tsx | 27 +-
server.ts | 36 +-
types/kuroshiro.d.ts | 2 +-
8 files changed, 423 insertions(+), 193 deletions(-)
diff --git a/admin-panel/src/App.tsx b/admin-panel/src/App.tsx
index 4f7090e..aa4d417 100644
--- a/admin-panel/src/App.tsx
+++ b/admin-panel/src/App.tsx
@@ -102,11 +102,26 @@ function App() {
const [approvalNotes, setApprovalNotes] = useState<{ [key: string]: string }>(
{},
);
- const [editingSettings, setEditingSettings] = useState<{ [key: string]: string }>({});
- const [categories, setCategories] = useState({ feedCategories: [], episodeCategories: [], allCategories: [] });
- const [categoryCounts, setCategoryCounts] = useState<{ [category: string]: CategoryCounts }>({});
+ const [editingSettings, setEditingSettings] = useState<{
+ [key: string]: string;
+ }>({});
+ const [categories, setCategories] = useState({
+ feedCategories: [],
+ episodeCategories: [],
+ allCategories: [],
+ });
+ const [categoryCounts, setCategoryCounts] = useState<{
+ [category: string]: CategoryCounts;
+ }>({});
const [activeTab, setActiveTab] = useState<
- "dashboard" | "feeds" | "episodes" | "env" | "settings" | "batch" | "requests" | "categories"
+ | "dashboard"
+ | "feeds"
+ | "episodes"
+ | "env"
+ | "settings"
+ | "batch"
+ | "requests"
+ | "categories"
>("dashboard");
useEffect(() => {
@@ -116,16 +131,23 @@ function App() {
const loadData = async () => {
setLoading(true);
try {
- const [feedsRes, statsRes, envRes, settingsRes, requestsRes, episodesRes, categoriesRes] =
- await Promise.all([
- fetch("/api/admin/feeds"),
- fetch("/api/admin/stats"),
- fetch("/api/admin/env"),
- fetch("/api/admin/settings"),
- fetch("/api/admin/feed-requests"),
- fetch("/api/admin/episodes"),
- fetch("/api/admin/categories/all"),
- ]);
+ const [
+ feedsRes,
+ statsRes,
+ envRes,
+ settingsRes,
+ requestsRes,
+ episodesRes,
+ categoriesRes,
+ ] = await Promise.all([
+ fetch("/api/admin/feeds"),
+ fetch("/api/admin/stats"),
+ fetch("/api/admin/env"),
+ fetch("/api/admin/settings"),
+ fetch("/api/admin/feed-requests"),
+ fetch("/api/admin/episodes"),
+ fetch("/api/admin/categories/all"),
+ ]);
if (
!feedsRes.ok ||
@@ -139,16 +161,23 @@ function App() {
throw new Error("Failed to load data");
}
- const [feedsData, statsData, envData, settingsData, requestsData, episodesData, categoriesData] =
- await Promise.all([
- feedsRes.json(),
- statsRes.json(),
- envRes.json(),
- settingsRes.json(),
- requestsRes.json(),
- episodesRes.json(),
- categoriesRes.json(),
- ]);
+ const [
+ feedsData,
+ statsData,
+ envData,
+ settingsData,
+ requestsData,
+ episodesData,
+ categoriesData,
+ ] = await Promise.all([
+ feedsRes.json(),
+ statsRes.json(),
+ envRes.json(),
+ settingsRes.json(),
+ requestsRes.json(),
+ episodesRes.json(),
+ categoriesRes.json(),
+ ]);
setFeeds(feedsData);
setStats(statsData);
@@ -157,24 +186,28 @@ function App() {
setFeedRequests(requestsData);
setEpisodes(episodesData);
setCategories(categoriesData);
-
+
// Load category counts for all categories
- const countsPromises = categoriesData.allCategories.map(async (category: string) => {
- const res = await fetch(`/api/admin/categories/${encodeURIComponent(category)}/counts`);
- if (res.ok) {
- const counts = await res.json();
- return { category, counts };
- }
- return { category, counts: { feedCount: 0, episodeCount: 0 } };
- });
-
+ const countsPromises = categoriesData.allCategories.map(
+ async (category: string) => {
+ const res = await fetch(
+ `/api/admin/categories/${encodeURIComponent(category)}/counts`,
+ );
+ if (res.ok) {
+ const counts = await res.json();
+ return { category, counts };
+ }
+ return { category, counts: { feedCount: 0, episodeCount: 0 } };
+ },
+ );
+
const countsResults = await Promise.all(countsPromises);
const countsMap: { [category: string]: CategoryCounts } = {};
countsResults.forEach(({ category, counts }) => {
countsMap[category] = counts;
});
setCategoryCounts(countsMap);
-
+
setError(null);
} catch (err) {
setError("データの読み込みに失敗しました");
@@ -426,26 +459,44 @@ function App() {
setEditingSettings({ ...editingSettings, [key]: value });
};
- const deleteCategory = async (category: string, target: "feeds" | "episodes" | "both") => {
- const targetText = target === "both" ? "フィードとエピソード" : target === "feeds" ? "フィード" : "エピソード";
- const counts = categoryCounts[category] || { feedCount: 0, episodeCount: 0 };
- const totalCount = target === "both" ? counts.feedCount + counts.episodeCount :
- target === "feeds" ? counts.feedCount : counts.episodeCount;
+ const deleteCategory = async (
+ category: string,
+ target: "feeds" | "episodes" | "both",
+ ) => {
+ const targetText =
+ target === "both"
+ ? "フィードとエピソード"
+ : target === "feeds"
+ ? "フィード"
+ : "エピソード";
+ const counts = categoryCounts[category] || {
+ feedCount: 0,
+ episodeCount: 0,
+ };
+ const totalCount =
+ target === "both"
+ ? counts.feedCount + counts.episodeCount
+ : target === "feeds"
+ ? counts.feedCount
+ : counts.episodeCount;
if (
!confirm(
- `本当にカテゴリ「${category}」を${targetText}から削除しますか?\n\n${totalCount}件のアイテムが影響を受けます。この操作は取り消せません。`
+ `本当にカテゴリ「${category}」を${targetText}から削除しますか?\n\n${totalCount}件のアイテムが影響を受けます。この操作は取り消せません。`,
)
) {
return;
}
try {
- const res = await fetch(`/api/admin/categories/${encodeURIComponent(category)}`, {
- method: "DELETE",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ target }),
- });
+ const res = await fetch(
+ `/api/admin/categories/${encodeURIComponent(category)}`,
+ {
+ method: "DELETE",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ target }),
+ },
+ );
const data = await res.json();
@@ -1230,26 +1281,33 @@ function App() {
-
+
- {settings.filter(s => s.value !== null && s.value !== "").length}
+ {
+ settings.filter(
+ (s) => s.value !== null && s.value !== "",
+ ).length
+ }
設定済み
- {settings.filter(s => s.required).length}
+ {settings.filter((s) => s.required).length}
必須設定
- {settings.filter(s => s.isCredential).length}
+ {settings.filter((s) => s.isCredential).length}
認証情報
@@ -1258,63 +1316,109 @@ function App() {
{settings.map((setting) => (
-
+
-
-
{setting.key}
+
+
+ {setting.key}
+
{setting.required && (
- 必須
+
+ 必須
+
)}
{setting.isCredential && (
- 認証情報
+
+ 認証情報
+
)}
-
+
{setting.description}
- デフォルト値: {setting.defaultValue || "なし"} |
- 最終更新: {new Date(setting.updatedAt).toLocaleString("ja-JP")}
+ デフォルト値: {setting.defaultValue || "なし"} |
+ 最終更新:{" "}
+ {new Date(setting.updatedAt).toLocaleString("ja-JP")}
-
+
{editingSettings[setting.key] !== undefined ? (
-
+
updateEditingValue(setting.key, e.target.value)}
+ onChange={(e) =>
+ updateEditingValue(setting.key, e.target.value)
+ }
placeholder={setting.defaultValue || "値を入力..."}
style={{
flex: 1,
padding: "6px 10px",
border: "1px solid #ddd",
borderRadius: "4px",
- fontSize: "14px"
+ fontSize: "14px",
}}
/>
) : (
-
-
+ alignItems: "center",
+ gap: "12px",
+ }}
+ >
+
{setting.value === null || setting.value === "" ? (
- 未設定
+
+ 未設定
+
) : setting.isCredential ? (
••••••••
) : (
@@ -1350,7 +1470,9 @@ function App() {
>
@@ -1396,17 +1526,26 @@ function App() {
-
+
-
{categories.allCategories.length}
+
+ {categories.allCategories.length}
+
総カテゴリ数
-
{categories.feedCategories.length}
+
+ {categories.feedCategories.length}
+
フィードカテゴリ
-
{categories.episodeCategories.length}
+
+ {categories.episodeCategories.length}
+
エピソードカテゴリ
@@ -1425,16 +1564,27 @@ function App() {
) : (
カテゴリ一覧 ({categories.allCategories.length}件)
-
+
削除対象を選択してから削除ボタンをクリックしてください。
-
+
{categories.allCategories.map((category) => {
- const counts = categoryCounts[category] || { feedCount: 0, episodeCount: 0 };
- const isInFeeds = categories.feedCategories.includes(category);
- const isInEpisodes = categories.episodeCategories.includes(category);
-
+ const counts = categoryCounts[category] || {
+ feedCount: 0,
+ episodeCount: 0,
+ };
+ const isInFeeds =
+ categories.feedCategories.includes(category);
+ const isInEpisodes =
+ categories.episodeCategories.includes(category);
+
return (
-
+
{category}
-
+
フィード: {counts.feedCount}件
|
エピソード: {counts.episodeCount}件
|
- 合計: {counts.feedCount + counts.episodeCount}件
+
+ 合計: {counts.feedCount + counts.episodeCount}件
+
-
+
フィード: {isInFeeds ? "使用中" : "未使用"}
-
+
エピソード: {isInEpisodes ? "使用中" : "未使用"}
-
+
{isInFeeds && (
@@ -1485,8 +1663,13 @@ function App() {
{isInEpisodes && (
@@ -1495,7 +1678,10 @@ function App() {
@@ -1525,10 +1711,21 @@ function App() {
paddingLeft: "20px",
}}
>
-
フィードから削除: フィードのカテゴリのみを削除します
- エピソードから削除: エピソードのカテゴリのみを削除します
- すべてから削除: フィードとエピソード両方からカテゴリを削除します
- 削除されたカテゴリは NULL に設定され、分類が解除されます
+
+ フィードから削除:{" "}
+ フィードのカテゴリのみを削除します
+
+
+ エピソードから削除:{" "}
+ エピソードのカテゴリのみを削除します
+
+
+ すべてから削除:{" "}
+ フィードとエピソード両方からカテゴリを削除します
+
+
+ 削除されたカテゴリは NULL に設定され、分類が解除されます
+
この操作は元に戻すことができません
diff --git a/admin-server.ts b/admin-server.ts
index 347507f..f0ed5f9 100644
--- a/admin-server.ts
+++ b/admin-server.ts
@@ -8,17 +8,17 @@ import { batchScheduler } from "./services/batch-scheduler.js";
import { config } from "./services/config.js";
import { closeBrowser } from "./services/content-extractor.js";
import {
+ deleteCategoryFromBoth,
deleteEpisode,
+ deleteEpisodeCategory,
deleteFeed,
+ deleteFeedCategory,
fetchAllEpisodes,
fetchEpisodesWithArticles,
getAllCategories,
getAllFeedsIncludingInactive,
getAllUsedCategories,
getCategoryCounts,
- deleteCategoryFromBoth,
- deleteFeedCategory,
- deleteEpisodeCategory,
getFeedByUrl,
getFeedRequests,
getFeedsByCategory,
@@ -375,14 +375,19 @@ app.get("/api/admin/categories/:category/counts", async (c) => {
app.delete("/api/admin/categories/:category", async (c) => {
try {
const category = decodeURIComponent(c.req.param("category"));
- const { target } = await c.req.json<{ target: "feeds" | "episodes" | "both" }>();
+ const { target } = await c.req.json<{
+ target: "feeds" | "episodes" | "both";
+ }>();
if (!category || category.trim() === "") {
return c.json({ error: "Category name is required" }, 400);
}
if (!target || !["feeds", "episodes", "both"].includes(target)) {
- return c.json({ error: "Valid target (feeds, episodes, or both) is required" }, 400);
+ return c.json(
+ { error: "Valid target (feeds, episodes, or both) is required" },
+ 400,
+ );
}
console.log(`🗑️ Admin deleting category "${category}" from ${target}`);
@@ -396,7 +401,7 @@ app.delete("/api/admin/categories/:category", async (c) => {
category,
feedChanges: result.feedChanges,
episodeChanges: result.episodeChanges,
- totalChanges: result.feedChanges + result.episodeChanges
+ totalChanges: result.feedChanges + result.episodeChanges,
});
} else if (target === "feeds") {
const changes = await deleteFeedCategory(category);
@@ -406,7 +411,7 @@ app.delete("/api/admin/categories/:category", async (c) => {
category,
feedChanges: changes,
episodeChanges: 0,
- totalChanges: changes
+ totalChanges: changes,
});
} else if (target === "episodes") {
const changes = await deleteEpisodeCategory(category);
@@ -416,7 +421,7 @@ app.delete("/api/admin/categories/:category", async (c) => {
category,
feedChanges: 0,
episodeChanges: changes,
- totalChanges: changes
+ totalChanges: changes,
});
}
} catch (error) {
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index fd0cbdf..2622a5a 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,5 @@
-import { Link, Route, Routes, useLocation } from "react-router-dom";
import { useEffect, useState } from "react";
+import { Link, Route, Routes, useLocation } from "react-router-dom";
import EpisodeDetail from "./components/EpisodeDetail";
import EpisodeList from "./components/EpisodeList";
import FeedDetail from "./components/FeedDetail";
@@ -10,13 +10,16 @@ import RSSEndpoints from "./components/RSSEndpoints";
function App() {
const location = useLocation();
const [isDarkMode, setIsDarkMode] = useState(() => {
- const saved = localStorage.getItem('darkMode');
+ const saved = localStorage.getItem("darkMode");
return saved ? JSON.parse(saved) : false;
});
useEffect(() => {
- document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light');
- localStorage.setItem('darkMode', JSON.stringify(isDarkMode));
+ document.documentElement.setAttribute(
+ "data-theme",
+ isDarkMode ? "dark" : "light",
+ );
+ localStorage.setItem("darkMode", JSON.stringify(isDarkMode));
}, [isDarkMode]);
const toggleDarkMode = () => {
@@ -41,12 +44,12 @@ function App() {
RSS フィードから自動生成された音声ポッドキャスト
-
diff --git a/frontend/src/components/EpisodeList.tsx b/frontend/src/components/EpisodeList.tsx
index f3205ac..26f7573 100644
--- a/frontend/src/components/EpisodeList.tsx
+++ b/frontend/src/components/EpisodeList.tsx
@@ -63,7 +63,7 @@ function EpisodeList() {
filterEpisodesByCategory();
}
}, [episodes, selectedCategory, searchQuery]);
-
+
// Reset to page 1 when category changes (but don't trigger if already on page 1)
useEffect(() => {
if (currentPage !== 1) {
@@ -85,28 +85,30 @@ function EpisodeList() {
page: currentPage.toString(),
limit: pageSize.toString(),
});
-
+
if (selectedCategory) {
searchParams.append("category", selectedCategory);
}
-
- const response = await fetch(`/api/episodes-with-feed-info?${searchParams}`);
+
+ const response = await fetch(
+ `/api/episodes-with-feed-info?${searchParams}`,
+ );
if (!response.ok) {
throw new Error("データベースからの取得に失敗しました");
}
const data = await response.json();
-
+
// Handle paginated response
if (data.episodes !== undefined) {
const dbEpisodes = data.episodes || [];
-
+
if (dbEpisodes.length === 0 && data.total === 0) {
// Database is empty, fallback to XML
console.log("Database is empty, falling back to XML parsing...");
setUseDatabase(false);
return;
}
-
+
setEpisodes(dbEpisodes);
setTotalEpisodes(data.total || 0);
setTotalPages(data.totalPages || 1);
@@ -402,9 +404,7 @@ function EpisodeList() {
))}
)}
- {isSearching && (
-
検索中...
- )}
+ {isSearching &&
検索中...}
@@ -527,7 +527,7 @@ function EpisodeList() {
))}
-
+
{/* Pagination Controls - only show for database mode */}
{useDatabase && totalPages > 1 && (
@@ -538,7 +538,7 @@ function EpisodeList() {
>
前へ
-
+
{/* Page numbers */}
{/* First page */}
@@ -553,13 +553,13 @@ function EpisodeList() {
{currentPage > 4 && ...}
>
)}
-
+
{/* Current page and nearby pages */}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNum = Math.max(1, currentPage - 2) + i;
if (pageNum > totalPages) return null;
if (pageNum < Math.max(1, currentPage - 2)) return null;
-
+
return (
-
+
-
+
{/* Page size selector */}
表示件数:
diff --git a/frontend/src/components/FeedDetail.tsx b/frontend/src/components/FeedDetail.tsx
index 8eea124..e6f57dd 100644
--- a/frontend/src/components/FeedDetail.tsx
+++ b/frontend/src/components/FeedDetail.tsx
@@ -222,7 +222,10 @@ function FeedDetail() {
{episode.title}
@@ -242,7 +245,10 @@ function FeedDetail() {
href={episode.articleLink}
target="_blank"
rel="noopener noreferrer"
- style={{ fontSize: "12px", color: "var(--text-secondary)" }}
+ style={{
+ fontSize: "12px",
+ color: "var(--text-secondary)",
+ }}
>
元記事を見る
diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx
index 0a1c0c4..f1eed4c 100644
--- a/frontend/src/components/FeedList.tsx
+++ b/frontend/src/components/FeedList.tsx
@@ -19,7 +19,7 @@ function FeedList() {
const [selectedCategory, setSelectedCategory] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
-
+
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(15);
@@ -46,7 +46,7 @@ function FeedList() {
try {
setLoading(true);
setError(null);
-
+
// Build query parameters
const params = new URLSearchParams();
params.append("page", currentPage.toString());
@@ -54,17 +54,17 @@ function FeedList() {
if (selectedCategory) {
params.append("category", selectedCategory);
}
-
+
const response = await fetch(`/api/feeds?${params.toString()}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "フィードの取得に失敗しました");
}
-
+
const data = await response.json();
-
+
// Handle paginated response
- if (data.feeds && typeof data.total !== 'undefined') {
+ if (data.feeds && typeof data.total !== "undefined") {
setFeeds(data.feeds);
setFilteredFeeds(data.feeds);
setTotalFeeds(data.total);
@@ -96,7 +96,6 @@ function FeedList() {
}
};
-
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString("ja-JP");
};
@@ -197,7 +196,11 @@ function FeedList() {
-
@@ -268,7 +271,7 @@ function FeedList() {
>
前へ
-
+
{/* Page numbers */}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
@@ -282,12 +285,12 @@ function FeedList() {
} else {
pageNum = currentPage - 2 + i;
}
-
+
return (
setCurrentPage(pageNum)}
>
{pageNum}
@@ -295,7 +298,7 @@ function FeedList() {
);
})}
-
+
{
const page = c.req.query("page");
const limit = c.req.query("limit");
const category = c.req.query("category");
-
+
// If pagination parameters are provided, use paginated endpoint
if (page || limit) {
- const { fetchActiveFeedsPaginated } = await import("./services/database.js");
+ const { fetchActiveFeedsPaginated } = await import(
+ "./services/database.js"
+ );
const pageNum = page ? Number.parseInt(page, 10) : 1;
const limitNum = limit ? Number.parseInt(limit, 10) : 10;
-
+
// Validate pagination parameters
if (Number.isNaN(pageNum) || pageNum < 1) {
return c.json({ error: "Invalid page number" }, 400);
@@ -526,8 +528,12 @@ app.get("/api/feeds", async (c) => {
if (Number.isNaN(limitNum) || limitNum < 1 || limitNum > 100) {
return c.json({ error: "Invalid limit (must be between 1-100)" }, 400);
}
-
- const result = await fetchActiveFeedsPaginated(pageNum, limitNum, category || undefined);
+
+ const result = await fetchActiveFeedsPaginated(
+ pageNum,
+ limitNum,
+ category || undefined,
+ );
return c.json(result);
} else {
// Original behavior for backward compatibility
@@ -630,13 +636,15 @@ app.get("/api/episodes-with-feed-info", async (c) => {
const page = c.req.query("page");
const limit = c.req.query("limit");
const category = c.req.query("category");
-
+
// If pagination parameters are provided, use paginated endpoint
if (page || limit) {
- const { fetchEpisodesWithFeedInfoPaginated } = await import("./services/database.js");
+ const { fetchEpisodesWithFeedInfoPaginated } = await import(
+ "./services/database.js"
+ );
const pageNum = page ? Number.parseInt(page, 10) : 1;
const limitNum = limit ? Number.parseInt(limit, 10) : 20;
-
+
// Validate pagination parameters
if (Number.isNaN(pageNum) || pageNum < 1) {
return c.json({ error: "Invalid page number" }, 400);
@@ -644,12 +652,18 @@ app.get("/api/episodes-with-feed-info", async (c) => {
if (Number.isNaN(limitNum) || limitNum < 1 || limitNum > 100) {
return c.json({ error: "Invalid limit (must be between 1-100)" }, 400);
}
-
- const result = await fetchEpisodesWithFeedInfoPaginated(pageNum, limitNum, category || undefined);
+
+ const result = await fetchEpisodesWithFeedInfoPaginated(
+ pageNum,
+ limitNum,
+ category || undefined,
+ );
return c.json(result);
} else {
// Original behavior for backward compatibility
- const { fetchEpisodesWithFeedInfo } = await import("./services/database.js");
+ const { fetchEpisodesWithFeedInfo } = await import(
+ "./services/database.js"
+ );
const episodes = await fetchEpisodesWithFeedInfo();
return c.json({ episodes });
}
diff --git a/types/kuroshiro.d.ts b/types/kuroshiro.d.ts
index 54fb868..5e4613d 100644
--- a/types/kuroshiro.d.ts
+++ b/types/kuroshiro.d.ts
@@ -22,4 +22,4 @@ declare module "kuroshiro-analyzer-mecab" {
}
export = KuroshiroAnalyzerMecab;
-}
\ No newline at end of file
+}