Add category deletion feature
This commit is contained in:
		@@ -74,6 +74,17 @@ interface Setting {
 | 
				
			|||||||
  updatedAt: string;
 | 
					  updatedAt: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface CategoryData {
 | 
				
			||||||
 | 
					  feedCategories: string[];
 | 
				
			||||||
 | 
					  episodeCategories: string[];
 | 
				
			||||||
 | 
					  allCategories: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface CategoryCounts {
 | 
				
			||||||
 | 
					  feedCount: number;
 | 
				
			||||||
 | 
					  episodeCount: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function App() {
 | 
					function App() {
 | 
				
			||||||
  const [feeds, setFeeds] = useState<Feed[]>([]);
 | 
					  const [feeds, setFeeds] = useState<Feed[]>([]);
 | 
				
			||||||
  const [stats, setStats] = useState<Stats | null>(null);
 | 
					  const [stats, setStats] = useState<Stats | null>(null);
 | 
				
			||||||
@@ -92,8 +103,10 @@ function App() {
 | 
				
			|||||||
    {},
 | 
					    {},
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const [editingSettings, setEditingSettings] = useState<{ [key: string]: string }>({});
 | 
					  const [editingSettings, setEditingSettings] = useState<{ [key: string]: string }>({});
 | 
				
			||||||
 | 
					  const [categories, setCategories] = useState<CategoryData>({ feedCategories: [], episodeCategories: [], allCategories: [] });
 | 
				
			||||||
 | 
					  const [categoryCounts, setCategoryCounts] = useState<{ [category: string]: CategoryCounts }>({});
 | 
				
			||||||
  const [activeTab, setActiveTab] = useState<
 | 
					  const [activeTab, setActiveTab] = useState<
 | 
				
			||||||
    "dashboard" | "feeds" | "episodes" | "env" | "settings" | "batch" | "requests"
 | 
					    "dashboard" | "feeds" | "episodes" | "env" | "settings" | "batch" | "requests" | "categories"
 | 
				
			||||||
  >("dashboard");
 | 
					  >("dashboard");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
@@ -103,7 +116,7 @@ function App() {
 | 
				
			|||||||
  const loadData = async () => {
 | 
					  const loadData = async () => {
 | 
				
			||||||
    setLoading(true);
 | 
					    setLoading(true);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const [feedsRes, statsRes, envRes, settingsRes, requestsRes, episodesRes] =
 | 
					      const [feedsRes, statsRes, envRes, settingsRes, requestsRes, episodesRes, categoriesRes] =
 | 
				
			||||||
        await Promise.all([
 | 
					        await Promise.all([
 | 
				
			||||||
          fetch("/api/admin/feeds"),
 | 
					          fetch("/api/admin/feeds"),
 | 
				
			||||||
          fetch("/api/admin/stats"),
 | 
					          fetch("/api/admin/stats"),
 | 
				
			||||||
@@ -111,6 +124,7 @@ function App() {
 | 
				
			|||||||
          fetch("/api/admin/settings"),
 | 
					          fetch("/api/admin/settings"),
 | 
				
			||||||
          fetch("/api/admin/feed-requests"),
 | 
					          fetch("/api/admin/feed-requests"),
 | 
				
			||||||
          fetch("/api/admin/episodes"),
 | 
					          fetch("/api/admin/episodes"),
 | 
				
			||||||
 | 
					          fetch("/api/admin/categories/all"),
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
@@ -119,12 +133,13 @@ function App() {
 | 
				
			|||||||
        !envRes.ok ||
 | 
					        !envRes.ok ||
 | 
				
			||||||
        !settingsRes.ok ||
 | 
					        !settingsRes.ok ||
 | 
				
			||||||
        !requestsRes.ok ||
 | 
					        !requestsRes.ok ||
 | 
				
			||||||
        !episodesRes.ok
 | 
					        !episodesRes.ok ||
 | 
				
			||||||
 | 
					        !categoriesRes.ok
 | 
				
			||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
        throw new Error("Failed to load data");
 | 
					        throw new Error("Failed to load data");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const [feedsData, statsData, envData, settingsData, requestsData, episodesData] =
 | 
					      const [feedsData, statsData, envData, settingsData, requestsData, episodesData, categoriesData] =
 | 
				
			||||||
        await Promise.all([
 | 
					        await Promise.all([
 | 
				
			||||||
          feedsRes.json(),
 | 
					          feedsRes.json(),
 | 
				
			||||||
          statsRes.json(),
 | 
					          statsRes.json(),
 | 
				
			||||||
@@ -132,6 +147,7 @@ function App() {
 | 
				
			|||||||
          settingsRes.json(),
 | 
					          settingsRes.json(),
 | 
				
			||||||
          requestsRes.json(),
 | 
					          requestsRes.json(),
 | 
				
			||||||
          episodesRes.json(),
 | 
					          episodesRes.json(),
 | 
				
			||||||
 | 
					          categoriesRes.json(),
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      setFeeds(feedsData);
 | 
					      setFeeds(feedsData);
 | 
				
			||||||
@@ -140,6 +156,25 @@ function App() {
 | 
				
			|||||||
      setSettings(settingsData);
 | 
					      setSettings(settingsData);
 | 
				
			||||||
      setFeedRequests(requestsData);
 | 
					      setFeedRequests(requestsData);
 | 
				
			||||||
      setEpisodes(episodesData);
 | 
					      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 countsResults = await Promise.all(countsPromises);
 | 
				
			||||||
 | 
					      const countsMap: { [category: string]: CategoryCounts } = {};
 | 
				
			||||||
 | 
					      countsResults.forEach(({ category, counts }) => {
 | 
				
			||||||
 | 
					        countsMap[category] = counts;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      setCategoryCounts(countsMap);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
      setError(null);
 | 
					      setError(null);
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      setError("データの読み込みに失敗しました");
 | 
					      setError("データの読み込みに失敗しました");
 | 
				
			||||||
@@ -391,6 +426,41 @@ function App() {
 | 
				
			|||||||
    setEditingSettings({ ...editingSettings, [key]: value });
 | 
					    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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      !confirm(
 | 
				
			||||||
 | 
					        `本当にカテゴリ「${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 data = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (res.ok) {
 | 
				
			||||||
 | 
					        setSuccess(data.message);
 | 
				
			||||||
 | 
					        loadData(); // Reload data to update category list and counts
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        setError(data.error || "カテゴリ削除に失敗しました");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      setError("カテゴリ削除に失敗しました");
 | 
				
			||||||
 | 
					      console.error("Error deleting category:", err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const deleteEpisode = async (episodeId: string, episodeTitle: string) => {
 | 
					  const deleteEpisode = async (episodeId: string, episodeTitle: string) => {
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
      !confirm(
 | 
					      !confirm(
 | 
				
			||||||
@@ -483,6 +553,12 @@ function App() {
 | 
				
			|||||||
            >
 | 
					            >
 | 
				
			||||||
              設定管理
 | 
					              設定管理
 | 
				
			||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
 | 
					            <button
 | 
				
			||||||
 | 
					              className={`btn ${activeTab === "categories" ? "btn-primary" : "btn-secondary"}`}
 | 
				
			||||||
 | 
					              onClick={() => setActiveTab("categories")}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              カテゴリ管理
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
            <button
 | 
					            <button
 | 
				
			||||||
              className={`btn ${activeTab === "env" ? "btn-primary" : "btn-secondary"}`}
 | 
					              className={`btn ${activeTab === "env" ? "btn-primary" : "btn-secondary"}`}
 | 
				
			||||||
              onClick={() => setActiveTab("env")}
 | 
					              onClick={() => setActiveTab("env")}
 | 
				
			||||||
@@ -1312,6 +1388,153 @@ function App() {
 | 
				
			|||||||
            </>
 | 
					            </>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {activeTab === "categories" && (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              <h3>カテゴリ管理</h3>
 | 
				
			||||||
 | 
					              <p style={{ marginBottom: "20px", color: "#7f8c8d" }}>
 | 
				
			||||||
 | 
					                フィードとエピソードのカテゴリを管理できます。不要なカテゴリを削除してデータベースをクリーンアップできます。
 | 
				
			||||||
 | 
					              </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div style={{ marginBottom: "20px" }}>
 | 
				
			||||||
 | 
					                <div className="stats-grid" style={{ gridTemplateColumns: "repeat(3, 1fr)" }}>
 | 
				
			||||||
 | 
					                  <div className="stat-card">
 | 
				
			||||||
 | 
					                    <div className="value">{categories.allCategories.length}</div>
 | 
				
			||||||
 | 
					                    <div className="label">総カテゴリ数</div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div className="stat-card">
 | 
				
			||||||
 | 
					                    <div className="value">{categories.feedCategories.length}</div>
 | 
				
			||||||
 | 
					                    <div className="label">フィードカテゴリ</div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div className="stat-card">
 | 
				
			||||||
 | 
					                    <div className="value">{categories.episodeCategories.length}</div>
 | 
				
			||||||
 | 
					                    <div className="label">エピソードカテゴリ</div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              {categories.allCategories.length === 0 ? (
 | 
				
			||||||
 | 
					                <p
 | 
				
			||||||
 | 
					                  style={{
 | 
				
			||||||
 | 
					                    color: "#7f8c8d",
 | 
				
			||||||
 | 
					                    textAlign: "center",
 | 
				
			||||||
 | 
					                    padding: "20px",
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  カテゴリがありません
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                <div style={{ marginTop: "24px" }}>
 | 
				
			||||||
 | 
					                  <h4>カテゴリ一覧 ({categories.allCategories.length}件)</h4>
 | 
				
			||||||
 | 
					                  <div style={{ marginBottom: "16px", fontSize: "14px", color: "#666" }}>
 | 
				
			||||||
 | 
					                    削除対象を選択してから削除ボタンをクリックしてください。
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  
 | 
				
			||||||
 | 
					                  <div className="category-list">
 | 
				
			||||||
 | 
					                    {categories.allCategories.map((category) => {
 | 
				
			||||||
 | 
					                      const counts = categoryCounts[category] || { feedCount: 0, episodeCount: 0 };
 | 
				
			||||||
 | 
					                      const isInFeeds = categories.feedCategories.includes(category);
 | 
				
			||||||
 | 
					                      const isInEpisodes = categories.episodeCategories.includes(category);
 | 
				
			||||||
 | 
					                      
 | 
				
			||||||
 | 
					                      return (
 | 
				
			||||||
 | 
					                        <div
 | 
				
			||||||
 | 
					                          key={category}
 | 
				
			||||||
 | 
					                          className="feed-item"
 | 
				
			||||||
 | 
					                          style={{ marginBottom: "16px" }}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          <div className="feed-info">
 | 
				
			||||||
 | 
					                            <h4 style={{ margin: "0 0 8px 0", fontSize: "16px" }}>
 | 
				
			||||||
 | 
					                              {category}
 | 
				
			||||||
 | 
					                            </h4>
 | 
				
			||||||
 | 
					                            <div style={{ fontSize: "14px", color: "#666", marginBottom: "8px" }}>
 | 
				
			||||||
 | 
					                              <span>フィード: {counts.feedCount}件</span>
 | 
				
			||||||
 | 
					                              <span style={{ margin: "0 12px" }}>|</span>
 | 
				
			||||||
 | 
					                              <span>エピソード: {counts.episodeCount}件</span>
 | 
				
			||||||
 | 
					                              <span style={{ margin: "0 12px" }}>|</span>
 | 
				
			||||||
 | 
					                              <span>合計: {counts.feedCount + counts.episodeCount}件</span>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                            <div style={{ fontSize: "12px", color: "#999" }}>
 | 
				
			||||||
 | 
					                              <span style={{
 | 
				
			||||||
 | 
					                                padding: "2px 6px",
 | 
				
			||||||
 | 
					                                background: isInFeeds ? "#e3f2fd" : "#f5f5f5",
 | 
				
			||||||
 | 
					                                color: isInFeeds ? "#1976d2" : "#999",
 | 
				
			||||||
 | 
					                                borderRadius: "4px",
 | 
				
			||||||
 | 
					                                marginRight: "8px"
 | 
				
			||||||
 | 
					                              }}>
 | 
				
			||||||
 | 
					                                フィード: {isInFeeds ? "使用中" : "未使用"}
 | 
				
			||||||
 | 
					                              </span>
 | 
				
			||||||
 | 
					                              <span style={{
 | 
				
			||||||
 | 
					                                padding: "2px 6px",
 | 
				
			||||||
 | 
					                                background: isInEpisodes ? "#e8f5e8" : "#f5f5f5",
 | 
				
			||||||
 | 
					                                color: isInEpisodes ? "#388e3c" : "#999",
 | 
				
			||||||
 | 
					                                borderRadius: "4px"
 | 
				
			||||||
 | 
					                              }}>
 | 
				
			||||||
 | 
					                                エピソード: {isInEpisodes ? "使用中" : "未使用"}
 | 
				
			||||||
 | 
					                              </span>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                          <div className="feed-actions" style={{ flexDirection: "column", gap: "8px", minWidth: "160px" }}>
 | 
				
			||||||
 | 
					                            {isInFeeds && (
 | 
				
			||||||
 | 
					                              <button
 | 
				
			||||||
 | 
					                                className="btn btn-warning"
 | 
				
			||||||
 | 
					                                onClick={() => deleteCategory(category, "feeds")}
 | 
				
			||||||
 | 
					                                style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
				
			||||||
 | 
					                              >
 | 
				
			||||||
 | 
					                                フィードから削除
 | 
				
			||||||
 | 
					                              </button>
 | 
				
			||||||
 | 
					                            )}
 | 
				
			||||||
 | 
					                            {isInEpisodes && (
 | 
				
			||||||
 | 
					                              <button
 | 
				
			||||||
 | 
					                                className="btn btn-warning"
 | 
				
			||||||
 | 
					                                onClick={() => deleteCategory(category, "episodes")}
 | 
				
			||||||
 | 
					                                style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
				
			||||||
 | 
					                              >
 | 
				
			||||||
 | 
					                                エピソードから削除
 | 
				
			||||||
 | 
					                              </button>
 | 
				
			||||||
 | 
					                            )}
 | 
				
			||||||
 | 
					                            {(isInFeeds || isInEpisodes) && (
 | 
				
			||||||
 | 
					                              <button
 | 
				
			||||||
 | 
					                                className="btn btn-danger"
 | 
				
			||||||
 | 
					                                onClick={() => deleteCategory(category, "both")}
 | 
				
			||||||
 | 
					                                style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
				
			||||||
 | 
					                              >
 | 
				
			||||||
 | 
					                                すべてから削除
 | 
				
			||||||
 | 
					                              </button>
 | 
				
			||||||
 | 
					                            )}
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    })}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                style={{
 | 
				
			||||||
 | 
					                  marginTop: "20px",
 | 
				
			||||||
 | 
					                  padding: "16px",
 | 
				
			||||||
 | 
					                  background: "#f8f9fa",
 | 
				
			||||||
 | 
					                  borderRadius: "4px",
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <h4>カテゴリ削除について</h4>
 | 
				
			||||||
 | 
					                <ul
 | 
				
			||||||
 | 
					                  style={{
 | 
				
			||||||
 | 
					                    fontSize: "14px",
 | 
				
			||||||
 | 
					                    color: "#6c757d",
 | 
				
			||||||
 | 
					                    marginTop: "8px",
 | 
				
			||||||
 | 
					                    paddingLeft: "20px",
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <li><strong>フィードから削除:</strong> フィードのカテゴリのみを削除します</li>
 | 
				
			||||||
 | 
					                  <li><strong>エピソードから削除:</strong> エピソードのカテゴリのみを削除します</li>
 | 
				
			||||||
 | 
					                  <li><strong>すべてから削除:</strong> フィードとエピソード両方からカテゴリを削除します</li>
 | 
				
			||||||
 | 
					                  <li>削除されたカテゴリは NULL に設定され、分類が解除されます</li>
 | 
				
			||||||
 | 
					                  <li>この操作は元に戻すことができません</li>
 | 
				
			||||||
 | 
					                </ul>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          {activeTab === "env" && (
 | 
					          {activeTab === "env" && (
 | 
				
			||||||
            <>
 | 
					            <>
 | 
				
			||||||
              <h3>環境変数設定</h3>
 | 
					              <h3>環境変数設定</h3>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,11 @@ import {
 | 
				
			|||||||
  fetchEpisodesWithArticles,
 | 
					  fetchEpisodesWithArticles,
 | 
				
			||||||
  getAllCategories,
 | 
					  getAllCategories,
 | 
				
			||||||
  getAllFeedsIncludingInactive,
 | 
					  getAllFeedsIncludingInactive,
 | 
				
			||||||
 | 
					  getAllUsedCategories,
 | 
				
			||||||
 | 
					  getCategoryCounts,
 | 
				
			||||||
 | 
					  deleteCategoryFromBoth,
 | 
				
			||||||
 | 
					  deleteFeedCategory,
 | 
				
			||||||
 | 
					  deleteEpisodeCategory,
 | 
				
			||||||
  getFeedByUrl,
 | 
					  getFeedByUrl,
 | 
				
			||||||
  getFeedRequests,
 | 
					  getFeedRequests,
 | 
				
			||||||
  getFeedsByCategory,
 | 
					  getFeedsByCategory,
 | 
				
			||||||
@@ -345,6 +350,81 @@ app.delete("/api/admin/episodes/:id", async (c) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Category management API endpoints
 | 
				
			||||||
 | 
					app.get("/api/admin/categories/all", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const categories = await getAllUsedCategories();
 | 
				
			||||||
 | 
					    return c.json(categories);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error fetching all used categories:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to fetch categories" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.get("/api/admin/categories/:category/counts", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const category = decodeURIComponent(c.req.param("category"));
 | 
				
			||||||
 | 
					    const counts = await getCategoryCounts(category);
 | 
				
			||||||
 | 
					    return c.json(counts);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error fetching category counts:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to fetch category counts" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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" }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(`🗑️  Admin deleting category "${category}" from ${target}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let result;
 | 
				
			||||||
 | 
					    if (target === "both") {
 | 
				
			||||||
 | 
					      result = await deleteCategoryFromBoth(category);
 | 
				
			||||||
 | 
					      return c.json({
 | 
				
			||||||
 | 
					        result: "DELETED",
 | 
				
			||||||
 | 
					        message: `Category "${category}" deleted from feeds and episodes`,
 | 
				
			||||||
 | 
					        category,
 | 
				
			||||||
 | 
					        feedChanges: result.feedChanges,
 | 
				
			||||||
 | 
					        episodeChanges: result.episodeChanges,
 | 
				
			||||||
 | 
					        totalChanges: result.feedChanges + result.episodeChanges
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else if (target === "feeds") {
 | 
				
			||||||
 | 
					      const changes = await deleteFeedCategory(category);
 | 
				
			||||||
 | 
					      return c.json({
 | 
				
			||||||
 | 
					        result: "DELETED",
 | 
				
			||||||
 | 
					        message: `Category "${category}" deleted from feeds`,
 | 
				
			||||||
 | 
					        category,
 | 
				
			||||||
 | 
					        feedChanges: changes,
 | 
				
			||||||
 | 
					        episodeChanges: 0,
 | 
				
			||||||
 | 
					        totalChanges: changes
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else if (target === "episodes") {
 | 
				
			||||||
 | 
					      const changes = await deleteEpisodeCategory(category);
 | 
				
			||||||
 | 
					      return c.json({
 | 
				
			||||||
 | 
					        result: "DELETED",
 | 
				
			||||||
 | 
					        message: `Category "${category}" deleted from episodes`,
 | 
				
			||||||
 | 
					        category,
 | 
				
			||||||
 | 
					        feedChanges: 0,
 | 
				
			||||||
 | 
					        episodeChanges: changes,
 | 
				
			||||||
 | 
					        totalChanges: changes
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error deleting category:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to delete category" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Database diagnostic endpoint
 | 
					// Database diagnostic endpoint
 | 
				
			||||||
app.get("/api/admin/db-diagnostic", async (c) => {
 | 
					app.get("/api/admin/db-diagnostic", async (c) => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1633,6 +1633,97 @@ export async function updateEpisodeCategory(
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Category cleanup functions
 | 
				
			||||||
 | 
					export async function deleteFeedCategory(category: string): Promise<number> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const stmt = db.prepare("UPDATE feeds SET category = NULL WHERE category = ?");
 | 
				
			||||||
 | 
					    const result = stmt.run(category);
 | 
				
			||||||
 | 
					    return result.changes;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error deleting feed category:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function deleteEpisodeCategory(category: string): Promise<number> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const stmt = db.prepare("UPDATE episodes SET category = NULL WHERE category = ?");
 | 
				
			||||||
 | 
					    const result = stmt.run(category);
 | 
				
			||||||
 | 
					    return result.changes;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error deleting episode category:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function deleteCategoryFromBoth(category: string): Promise<{feedChanges: number, episodeChanges: number}> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    db.exec("BEGIN TRANSACTION");
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const feedChanges = await deleteFeedCategory(category);
 | 
				
			||||||
 | 
					    const episodeChanges = await deleteEpisodeCategory(category);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    db.exec("COMMIT");
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return { feedChanges, episodeChanges };
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    db.exec("ROLLBACK");
 | 
				
			||||||
 | 
					    console.error("Error deleting category from both tables:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getAllUsedCategories(): Promise<{feedCategories: string[], episodeCategories: string[], allCategories: string[]}> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    // Get feed categories
 | 
				
			||||||
 | 
					    const feedCatStmt = db.prepare(
 | 
				
			||||||
 | 
					      "SELECT DISTINCT category FROM feeds WHERE category IS NOT NULL AND category != '' ORDER BY category"
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const feedCatRows = feedCatStmt.all() as any[];
 | 
				
			||||||
 | 
					    const feedCategories = feedCatRows.map(row => row.category);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Get episode categories
 | 
				
			||||||
 | 
					    const episodeCatStmt = db.prepare(
 | 
				
			||||||
 | 
					      "SELECT DISTINCT category FROM episodes WHERE category IS NOT NULL AND category != '' ORDER BY category"
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const episodeCatRows = episodeCatStmt.all() as any[];
 | 
				
			||||||
 | 
					    const episodeCategories = episodeCatRows.map(row => row.category);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Get all unique categories
 | 
				
			||||||
 | 
					    const allCategoriesSet = new Set([...feedCategories, ...episodeCategories]);
 | 
				
			||||||
 | 
					    const allCategories = Array.from(allCategoriesSet).sort();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      feedCategories,
 | 
				
			||||||
 | 
					      episodeCategories,
 | 
				
			||||||
 | 
					      allCategories
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error getting all used categories:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getCategoryCounts(category: string): Promise<{feedCount: number, episodeCount: number}> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    // Count feeds with this category
 | 
				
			||||||
 | 
					    const feedCountStmt = db.prepare("SELECT COUNT(*) as count FROM feeds WHERE category = ?");
 | 
				
			||||||
 | 
					    const feedCountResult = feedCountStmt.get(category) as { count: number };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Count episodes with this category
 | 
				
			||||||
 | 
					    const episodeCountStmt = db.prepare("SELECT COUNT(*) as count FROM episodes WHERE category = ?");
 | 
				
			||||||
 | 
					    const episodeCountResult = episodeCountStmt.get(category) as { count: number };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      feedCount: feedCountResult.count,
 | 
				
			||||||
 | 
					      episodeCount: episodeCountResult.count
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error getting category counts:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Migration function to classify existing episodes without categories
 | 
					// Migration function to classify existing episodes without categories
 | 
				
			||||||
export async function migrateEpisodesWithCategories(): Promise<void> {
 | 
					export async function migrateEpisodesWithCategories(): Promise<void> {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -127,7 +127,10 @@ ${articleDetails}
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const response = await openai.chat.completions.create({
 | 
					    const response = await openai.chat.completions.create({
 | 
				
			||||||
      model: config.openai.modelName,
 | 
					      model: config.openai.modelName,
 | 
				
			||||||
      messages: [{ role: "system", content: prompt.trim() }, {role:"user", content: sendContent.trim()}],
 | 
					      messages: [
 | 
				
			||||||
 | 
					        { role: "system", content: prompt.trim() },
 | 
				
			||||||
 | 
					        { role: "user", content: sendContent.trim() },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
      temperature: 0.6,
 | 
					      temperature: 0.6,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -171,7 +174,9 @@ export async function openAI_ClassifyEpisode(
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const prompt = `
 | 
					  const prompt = `
 | 
				
			||||||
ポッドキャストエピソードの情報を見て、適切なトピックカテゴリに分類してください。
 | 
					以下のポッドキャストエピソードの情報を見て、適切なトピックカテゴリに分類してください。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					${textForClassification}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
以下のカテゴリから1つを選択してください:
 | 
					以下のカテゴリから1つを選択してください:
 | 
				
			||||||
- テクノロジー
 | 
					- テクノロジー
 | 
				
			||||||
@@ -191,8 +196,8 @@ export async function openAI_ClassifyEpisode(
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const response = await openai.chat.completions.create({
 | 
					    const response = await openai.chat.completions.create({
 | 
				
			||||||
      model: config.openai.modelName,
 | 
					      model: config.openai.modelName,
 | 
				
			||||||
      messages: [{ role: "system", content: prompt.trim() }, {role: "user", content: textForClassification.trim()}],
 | 
					      messages: [{ role: "user", content: prompt.trim() }],
 | 
				
			||||||
      temperature: 0.3,
 | 
					      temperature: 0.2,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const category = response.choices[0]?.message?.content?.trim();
 | 
					    const category = response.choices[0]?.message?.content?.trim();
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user