Update
This commit is contained in:
		@@ -1,4 +1,5 @@
 | 
			
		||||
import { Link, Route, Routes, useLocation } from "react-router-dom";
 | 
			
		||||
import CategoryList from "./components/CategoryList";
 | 
			
		||||
import EpisodeDetail from "./components/EpisodeDetail";
 | 
			
		||||
import EpisodeList from "./components/EpisodeList";
 | 
			
		||||
import FeedDetail from "./components/FeedDetail";
 | 
			
		||||
@@ -7,7 +8,7 @@ import FeedManager from "./components/FeedManager";
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  const location = useLocation();
 | 
			
		||||
  const isMainPage = ["/", "/feeds", "/feed-requests"].includes(
 | 
			
		||||
  const isMainPage = ["/", "/feeds", "/categories", "/feed-requests"].includes(
 | 
			
		||||
    location.pathname,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@@ -35,6 +36,12 @@ function App() {
 | 
			
		||||
            >
 | 
			
		||||
              フィード一覧
 | 
			
		||||
            </Link>
 | 
			
		||||
            <Link
 | 
			
		||||
              to="/categories"
 | 
			
		||||
              className={`tab ${location.pathname === "/categories" ? "active" : ""}`}
 | 
			
		||||
            >
 | 
			
		||||
              カテゴリ一覧
 | 
			
		||||
            </Link>
 | 
			
		||||
            <Link
 | 
			
		||||
              to="/feed-requests"
 | 
			
		||||
              className={`tab ${location.pathname === "/feed-requests" ? "active" : ""}`}
 | 
			
		||||
@@ -49,6 +56,7 @@ function App() {
 | 
			
		||||
        <Routes>
 | 
			
		||||
          <Route path="/" element={<EpisodeList />} />
 | 
			
		||||
          <Route path="/feeds" element={<FeedList />} />
 | 
			
		||||
          <Route path="/categories" element={<CategoryList />} />
 | 
			
		||||
          <Route path="/feed-requests" element={<FeedManager />} />
 | 
			
		||||
          <Route path="/episode/:episodeId" element={<EpisodeDetail />} />
 | 
			
		||||
          <Route path="/feeds/:feedId" element={<FeedDetail />} />
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										397
									
								
								frontend/src/components/CategoryList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										397
									
								
								frontend/src/components/CategoryList.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,397 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { Link } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
interface Feed {
 | 
			
		||||
  id: string;
 | 
			
		||||
  url: string;
 | 
			
		||||
  title?: string;
 | 
			
		||||
  description?: string;
 | 
			
		||||
  category?: string;
 | 
			
		||||
  lastUpdated?: string;
 | 
			
		||||
  createdAt: string;
 | 
			
		||||
  active: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface CategoryGroup {
 | 
			
		||||
  [category: string]: Feed[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CategoryList() {
 | 
			
		||||
  const [groupedFeeds, setGroupedFeeds] = useState<CategoryGroup>({});
 | 
			
		||||
  const [categories, setCategories] = useState<string[]>([]);
 | 
			
		||||
  const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
 | 
			
		||||
  const [filteredFeeds, setFilteredFeeds] = useState<Feed[]>([]);
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
  const [error, setError] = useState<string | null>(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchCategoriesAndFeeds();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectedCategory && groupedFeeds[selectedCategory]) {
 | 
			
		||||
      setFilteredFeeds(groupedFeeds[selectedCategory]);
 | 
			
		||||
    } else {
 | 
			
		||||
      setFilteredFeeds([]);
 | 
			
		||||
    }
 | 
			
		||||
  }, [selectedCategory, groupedFeeds]);
 | 
			
		||||
 | 
			
		||||
  const fetchCategoriesAndFeeds = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      setLoading(true);
 | 
			
		||||
      setError(null);
 | 
			
		||||
 | 
			
		||||
      // Fetch grouped feeds
 | 
			
		||||
      const [groupedResponse, categoriesResponse] = await Promise.all([
 | 
			
		||||
        fetch("/api/feeds/grouped-by-category"),
 | 
			
		||||
        fetch("/api/categories"),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
      if (!groupedResponse.ok || !categoriesResponse.ok) {
 | 
			
		||||
        throw new Error("カテゴリデータの取得に失敗しました");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const groupedData = await groupedResponse.json();
 | 
			
		||||
      const categoriesData = await categoriesResponse.json();
 | 
			
		||||
 | 
			
		||||
      setGroupedFeeds(groupedData.groupedFeeds || {});
 | 
			
		||||
      setCategories(categoriesData.categories || []);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error("Category fetch error:", err);
 | 
			
		||||
      setError(err instanceof Error ? err.message : "エラーが発生しました");
 | 
			
		||||
    } finally {
 | 
			
		||||
      setLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const formatDate = (dateString: string) => {
 | 
			
		||||
    return new Date(dateString).toLocaleString("ja-JP");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getFeedCount = (category: string) => {
 | 
			
		||||
    return groupedFeeds[category]?.length || 0;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getTotalEpisodesCount = (feeds: Feed[]) => {
 | 
			
		||||
    // This would require an additional API call to get episode counts per feed
 | 
			
		||||
    // For now, just return the feed count
 | 
			
		||||
    return feeds.length;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <div className="loading">読み込み中...</div>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (error) {
 | 
			
		||||
    return <div className="error">{error}</div>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const availableCategories = Object.keys(groupedFeeds).filter(
 | 
			
		||||
    (category) => groupedFeeds[category] && groupedFeeds[category].length > 0,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (availableCategories.length === 0) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="empty-state">
 | 
			
		||||
        <p>カテゴリ別のフィードがありません</p>
 | 
			
		||||
        <p>
 | 
			
		||||
          フィードにカテゴリが設定されていないか、アクティブなフィードがない可能性があります
 | 
			
		||||
        </p>
 | 
			
		||||
        <button
 | 
			
		||||
          className="btn btn-secondary"
 | 
			
		||||
          onClick={fetchCategoriesAndFeeds}
 | 
			
		||||
          style={{ marginTop: "10px" }}
 | 
			
		||||
        >
 | 
			
		||||
          再読み込み
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          marginBottom: "20px",
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          justifyContent: "space-between",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <h2>カテゴリ一覧 ({availableCategories.length}件)</h2>
 | 
			
		||||
        <button className="btn btn-secondary" onClick={fetchCategoriesAndFeeds}>
 | 
			
		||||
          更新
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {!selectedCategory ? (
 | 
			
		||||
        <div className="category-grid">
 | 
			
		||||
          {availableCategories.map((category) => (
 | 
			
		||||
            <div key={category} className="category-card">
 | 
			
		||||
              <div className="category-header">
 | 
			
		||||
                <h3
 | 
			
		||||
                  className="category-title"
 | 
			
		||||
                  onClick={() => setSelectedCategory(category)}
 | 
			
		||||
                >
 | 
			
		||||
                  {category}
 | 
			
		||||
                </h3>
 | 
			
		||||
                <div className="category-stats">
 | 
			
		||||
                  <span className="feed-count">
 | 
			
		||||
                    {getFeedCount(category)} フィード
 | 
			
		||||
                  </span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div className="category-feeds-preview">
 | 
			
		||||
                {groupedFeeds[category]?.slice(0, 3).map((feed) => (
 | 
			
		||||
                  <div key={feed.id} className="feed-preview">
 | 
			
		||||
                    <Link to={`/feeds/${feed.id}`} className="feed-preview-link">
 | 
			
		||||
                      {feed.title || feed.url}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </div>
 | 
			
		||||
                ))}
 | 
			
		||||
                {getFeedCount(category) > 3 && (
 | 
			
		||||
                  <div className="more-feeds">
 | 
			
		||||
                    他 {getFeedCount(category) - 3} フィード...
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div className="category-actions">
 | 
			
		||||
                <button
 | 
			
		||||
                  className="btn btn-primary"
 | 
			
		||||
                  onClick={() => setSelectedCategory(category)}
 | 
			
		||||
                >
 | 
			
		||||
                  詳細を見る
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          ))}
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div>
 | 
			
		||||
          <div className="category-detail-header">
 | 
			
		||||
            <button
 | 
			
		||||
              className="btn btn-secondary"
 | 
			
		||||
              onClick={() => setSelectedCategory(null)}
 | 
			
		||||
            >
 | 
			
		||||
              ← カテゴリ一覧に戻る
 | 
			
		||||
            </button>
 | 
			
		||||
            <h3>カテゴリ: {selectedCategory}</h3>
 | 
			
		||||
            <p>{filteredFeeds.length} フィード</p>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className="feed-grid">
 | 
			
		||||
            {filteredFeeds.map((feed) => (
 | 
			
		||||
              <div key={feed.id} className="feed-card">
 | 
			
		||||
                <div className="feed-card-header">
 | 
			
		||||
                  <h4 className="feed-title">
 | 
			
		||||
                    <Link to={`/feeds/${feed.id}`} className="feed-link">
 | 
			
		||||
                      {feed.title || feed.url}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </h4>
 | 
			
		||||
                  <div className="feed-url">
 | 
			
		||||
                    <a
 | 
			
		||||
                      href={feed.url}
 | 
			
		||||
                      target="_blank"
 | 
			
		||||
                      rel="noopener noreferrer"
 | 
			
		||||
                    >
 | 
			
		||||
                      {feed.url}
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {feed.description && (
 | 
			
		||||
                  <div className="feed-description">{feed.description}</div>
 | 
			
		||||
                )}
 | 
			
		||||
 | 
			
		||||
                <div className="feed-meta">
 | 
			
		||||
                  <div>作成日: {formatDate(feed.createdAt)}</div>
 | 
			
		||||
                  {feed.lastUpdated && (
 | 
			
		||||
                    <div>最終更新: {formatDate(feed.lastUpdated)}</div>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div className="feed-actions">
 | 
			
		||||
                  <Link to={`/feeds/${feed.id}`} className="btn btn-primary">
 | 
			
		||||
                    エピソード一覧を見る
 | 
			
		||||
                  </Link>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <style>{`
 | 
			
		||||
        .category-grid {
 | 
			
		||||
          display: grid;
 | 
			
		||||
          grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
 | 
			
		||||
          gap: 20px;
 | 
			
		||||
          margin-bottom: 30px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-card {
 | 
			
		||||
          border: 1px solid #e9ecef;
 | 
			
		||||
          border-radius: 12px;
 | 
			
		||||
          padding: 20px;
 | 
			
		||||
          background: white;
 | 
			
		||||
          box-shadow: 0 2px 8px rgba(0,0,0,0.1);
 | 
			
		||||
          transition: transform 0.2s ease, box-shadow 0.2s ease;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-card:hover {
 | 
			
		||||
          transform: translateY(-2px);
 | 
			
		||||
          box-shadow: 0 4px 16px rgba(0,0,0,0.15);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-header {
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-title {
 | 
			
		||||
          margin: 0 0 8px 0;
 | 
			
		||||
          font-size: 20px;
 | 
			
		||||
          color: #2c3e50;
 | 
			
		||||
          cursor: pointer;
 | 
			
		||||
          transition: color 0.2s ease;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-title:hover {
 | 
			
		||||
          color: #007bff;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-stats {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          gap: 15px;
 | 
			
		||||
          font-size: 14px;
 | 
			
		||||
          color: #6c757d;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-count {
 | 
			
		||||
          background: #f8f9fa;
 | 
			
		||||
          padding: 4px 8px;
 | 
			
		||||
          border-radius: 4px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-feeds-preview {
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-preview {
 | 
			
		||||
          margin-bottom: 6px;
 | 
			
		||||
          font-size: 14px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-preview-link {
 | 
			
		||||
          color: #007bff;
 | 
			
		||||
          text-decoration: none;
 | 
			
		||||
          display: block;
 | 
			
		||||
          white-space: nowrap;
 | 
			
		||||
          overflow: hidden;
 | 
			
		||||
          text-overflow: ellipsis;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-preview-link:hover {
 | 
			
		||||
          text-decoration: underline;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .more-feeds {
 | 
			
		||||
          font-size: 12px;
 | 
			
		||||
          color: #6c757d;
 | 
			
		||||
          font-style: italic;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-actions {
 | 
			
		||||
          text-align: right;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-detail-header {
 | 
			
		||||
          margin-bottom: 25px;
 | 
			
		||||
          padding-bottom: 15px;
 | 
			
		||||
          border-bottom: 1px solid #e9ecef;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-detail-header h3 {
 | 
			
		||||
          margin: 10px 0 5px 0;
 | 
			
		||||
          color: #2c3e50;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-detail-header p {
 | 
			
		||||
          margin: 0;
 | 
			
		||||
          color: #6c757d;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-grid {
 | 
			
		||||
          display: grid;
 | 
			
		||||
          grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
 | 
			
		||||
          gap: 20px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-card {
 | 
			
		||||
          border: 1px solid #e9ecef;
 | 
			
		||||
          border-radius: 8px;
 | 
			
		||||
          padding: 20px;
 | 
			
		||||
          background: white;
 | 
			
		||||
          box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-card-header {
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-title {
 | 
			
		||||
          margin: 0 0 8px 0;
 | 
			
		||||
          font-size: 16px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-link {
 | 
			
		||||
          text-decoration: none;
 | 
			
		||||
          color: #007bff;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-link:hover {
 | 
			
		||||
          text-decoration: underline;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-url {
 | 
			
		||||
          font-size: 12px;
 | 
			
		||||
          color: #666;
 | 
			
		||||
          word-break: break-all;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-url a {
 | 
			
		||||
          color: #666;
 | 
			
		||||
          text-decoration: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-url a:hover {
 | 
			
		||||
          color: #007bff;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-description {
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
          color: #333;
 | 
			
		||||
          line-height: 1.5;
 | 
			
		||||
          font-size: 14px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-meta {
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
          font-size: 12px;
 | 
			
		||||
          color: #666;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-meta div {
 | 
			
		||||
          margin-bottom: 4px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-actions {
 | 
			
		||||
          text-align: right;
 | 
			
		||||
        }
 | 
			
		||||
      `}</style>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CategoryList;
 | 
			
		||||
@@ -27,10 +27,14 @@ interface EpisodeWithFeedInfo {
 | 
			
		||||
  feedId: string;
 | 
			
		||||
  feedTitle?: string;
 | 
			
		||||
  feedUrl: string;
 | 
			
		||||
  feedCategory?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function EpisodeList() {
 | 
			
		||||
  const [episodes, setEpisodes] = useState<EpisodeWithFeedInfo[]>([]);
 | 
			
		||||
  const [filteredEpisodes, setFilteredEpisodes] = useState<EpisodeWithFeedInfo[]>([]);
 | 
			
		||||
  const [categories, setCategories] = useState<string[]>([]);
 | 
			
		||||
  const [selectedCategory, setSelectedCategory] = useState<string>("");
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
  const [error, setError] = useState<string | null>(null);
 | 
			
		||||
  const [currentAudio, setCurrentAudio] = useState<string | null>(null);
 | 
			
		||||
@@ -38,8 +42,13 @@ function EpisodeList() {
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchEpisodes();
 | 
			
		||||
    fetchCategories();
 | 
			
		||||
  }, [useDatabase]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    filterEpisodesByCategory();
 | 
			
		||||
  }, [episodes, selectedCategory]);
 | 
			
		||||
 | 
			
		||||
  const fetchEpisodes = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      setLoading(true);
 | 
			
		||||
@@ -106,6 +115,29 @@ function EpisodeList() {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const fetchCategories = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await fetch("/api/categories");
 | 
			
		||||
      if (response.ok) {
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
        setCategories(data.categories || []);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error("Error fetching categories:", err);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const filterEpisodesByCategory = () => {
 | 
			
		||||
    if (!selectedCategory) {
 | 
			
		||||
      setFilteredEpisodes(episodes);
 | 
			
		||||
    } else {
 | 
			
		||||
      const filtered = episodes.filter(episode => 
 | 
			
		||||
        episode.feedCategory === selectedCategory
 | 
			
		||||
      );
 | 
			
		||||
      setFilteredEpisodes(filtered);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const formatDate = (dateString: string) => {
 | 
			
		||||
    return new Date(dateString).toLocaleString("ja-JP");
 | 
			
		||||
  };
 | 
			
		||||
@@ -165,6 +197,21 @@ function EpisodeList() {
 | 
			
		||||
    return <div className="error">{error}</div>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (filteredEpisodes.length === 0 && episodes.length > 0 && selectedCategory) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="empty-state">
 | 
			
		||||
        <p>カテゴリ「{selectedCategory}」のエピソードがありません</p>
 | 
			
		||||
        <button
 | 
			
		||||
          className="btn btn-secondary"
 | 
			
		||||
          onClick={() => setSelectedCategory("")}
 | 
			
		||||
          style={{ marginTop: "10px" }}
 | 
			
		||||
        >
 | 
			
		||||
          全てのエピソードを表示
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (episodes.length === 0) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="empty-state">
 | 
			
		||||
@@ -193,8 +240,27 @@ function EpisodeList() {
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <h2>エピソード一覧 ({episodes.length}件)</h2>
 | 
			
		||||
        <h2>エピソード一覧 ({selectedCategory ? `${filteredEpisodes.length}/${episodes.length}` : episodes.length}件)</h2>
 | 
			
		||||
        <div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
 | 
			
		||||
          {categories.length > 0 && (
 | 
			
		||||
            <select
 | 
			
		||||
              value={selectedCategory}
 | 
			
		||||
              onChange={(e) => setSelectedCategory(e.target.value)}
 | 
			
		||||
              style={{
 | 
			
		||||
                padding: "5px 10px",
 | 
			
		||||
                fontSize: "14px",
 | 
			
		||||
                border: "1px solid #ccc",
 | 
			
		||||
                borderRadius: "4px",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <option value="">全カテゴリ</option>
 | 
			
		||||
              {categories.map((category) => (
 | 
			
		||||
                <option key={category} value={category}>
 | 
			
		||||
                  {category}
 | 
			
		||||
                </option>
 | 
			
		||||
              ))}
 | 
			
		||||
            </select>
 | 
			
		||||
          )}
 | 
			
		||||
          <span style={{ fontSize: "12px", color: "#666" }}>
 | 
			
		||||
            データソース: {useDatabase ? "データベース" : "XML"}
 | 
			
		||||
          </span>
 | 
			
		||||
@@ -214,7 +280,7 @@ function EpisodeList() {
 | 
			
		||||
          </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
          {episodes.map((episode) => (
 | 
			
		||||
          {filteredEpisodes.map((episode) => (
 | 
			
		||||
            <tr key={episode.id}>
 | 
			
		||||
              <td>
 | 
			
		||||
                <div style={{ marginBottom: "8px" }}>
 | 
			
		||||
@@ -242,6 +308,11 @@ function EpisodeList() {
 | 
			
		||||
                    >
 | 
			
		||||
                      {episode.feedTitle}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                    {episode.feedCategory && (
 | 
			
		||||
                      <span style={{ marginLeft: "8px", color: "#999", fontSize: "11px" }}>
 | 
			
		||||
                        ({episode.feedCategory})
 | 
			
		||||
                      </span>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
                {episode.articleTitle &&
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ interface Feed {
 | 
			
		||||
  url: string;
 | 
			
		||||
  title?: string;
 | 
			
		||||
  description?: string;
 | 
			
		||||
  category?: string;
 | 
			
		||||
  lastUpdated?: string;
 | 
			
		||||
  createdAt: string;
 | 
			
		||||
  active: boolean;
 | 
			
		||||
@@ -13,13 +14,21 @@ interface Feed {
 | 
			
		||||
 | 
			
		||||
function FeedList() {
 | 
			
		||||
  const [feeds, setFeeds] = useState<Feed[]>([]);
 | 
			
		||||
  const [filteredFeeds, setFilteredFeeds] = useState<Feed[]>([]);
 | 
			
		||||
  const [categories, setCategories] = useState<string[]>([]);
 | 
			
		||||
  const [selectedCategory, setSelectedCategory] = useState<string>("");
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
  const [error, setError] = useState<string | null>(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchFeeds();
 | 
			
		||||
    fetchCategories();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    filterFeedsByCategory();
 | 
			
		||||
  }, [feeds, selectedCategory]);
 | 
			
		||||
 | 
			
		||||
  const fetchFeeds = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      setLoading(true);
 | 
			
		||||
@@ -38,6 +47,29 @@ function FeedList() {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const fetchCategories = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await fetch("/api/categories");
 | 
			
		||||
      if (response.ok) {
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
        setCategories(data.categories || []);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error("Error fetching categories:", err);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const filterFeedsByCategory = () => {
 | 
			
		||||
    if (!selectedCategory) {
 | 
			
		||||
      setFilteredFeeds(feeds);
 | 
			
		||||
    } else {
 | 
			
		||||
      const filtered = feeds.filter(feed => 
 | 
			
		||||
        feed.category === selectedCategory
 | 
			
		||||
      );
 | 
			
		||||
      setFilteredFeeds(filtered);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const formatDate = (dateString: string) => {
 | 
			
		||||
    return new Date(dateString).toLocaleString("ja-JP");
 | 
			
		||||
  };
 | 
			
		||||
@@ -50,6 +82,21 @@ function FeedList() {
 | 
			
		||||
    return <div className="error">{error}</div>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (filteredFeeds.length === 0 && feeds.length > 0 && selectedCategory) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="empty-state">
 | 
			
		||||
        <p>カテゴリ「{selectedCategory}」のフィードがありません</p>
 | 
			
		||||
        <button
 | 
			
		||||
          className="btn btn-secondary"
 | 
			
		||||
          onClick={() => setSelectedCategory("")}
 | 
			
		||||
          style={{ marginTop: "10px" }}
 | 
			
		||||
        >
 | 
			
		||||
          全てのフィードを表示
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (feeds.length === 0) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="empty-state">
 | 
			
		||||
@@ -78,14 +125,35 @@ function FeedList() {
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <h2>フィード一覧 ({feeds.length}件)</h2>
 | 
			
		||||
        <button className="btn btn-secondary" onClick={fetchFeeds}>
 | 
			
		||||
          更新
 | 
			
		||||
        </button>
 | 
			
		||||
        <h2>フィード一覧 ({selectedCategory ? `${filteredFeeds.length}/${feeds.length}` : feeds.length}件)</h2>
 | 
			
		||||
        <div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
 | 
			
		||||
          {categories.length > 0 && (
 | 
			
		||||
            <select
 | 
			
		||||
              value={selectedCategory}
 | 
			
		||||
              onChange={(e) => setSelectedCategory(e.target.value)}
 | 
			
		||||
              style={{
 | 
			
		||||
                padding: "5px 10px",
 | 
			
		||||
                fontSize: "14px",
 | 
			
		||||
                border: "1px solid #ccc",
 | 
			
		||||
                borderRadius: "4px",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <option value="">全カテゴリ</option>
 | 
			
		||||
              {categories.map((category) => (
 | 
			
		||||
                <option key={category} value={category}>
 | 
			
		||||
                  {category}
 | 
			
		||||
                </option>
 | 
			
		||||
              ))}
 | 
			
		||||
            </select>
 | 
			
		||||
          )}
 | 
			
		||||
          <button className="btn btn-secondary" onClick={fetchFeeds}>
 | 
			
		||||
            更新
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className="feed-grid">
 | 
			
		||||
        {feeds.map((feed) => (
 | 
			
		||||
        {filteredFeeds.map((feed) => (
 | 
			
		||||
          <div key={feed.id} className="feed-card">
 | 
			
		||||
            <div className="feed-card-header">
 | 
			
		||||
              <h3 className="feed-title">
 | 
			
		||||
@@ -104,6 +172,12 @@ function FeedList() {
 | 
			
		||||
              <div className="feed-description">{feed.description}</div>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            {feed.category && (
 | 
			
		||||
              <div className="feed-category">
 | 
			
		||||
                <span className="category-badge">{feed.category}</span>
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            <div className="feed-meta">
 | 
			
		||||
              <div>作成日: {formatDate(feed.createdAt)}</div>
 | 
			
		||||
              {feed.lastUpdated && (
 | 
			
		||||
@@ -187,6 +261,20 @@ function FeedList() {
 | 
			
		||||
        .feed-actions {
 | 
			
		||||
          text-align: right;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-category {
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-badge {
 | 
			
		||||
          display: inline-block;
 | 
			
		||||
          background: #007bff;
 | 
			
		||||
          color: white;
 | 
			
		||||
          padding: 3px 8px;
 | 
			
		||||
          border-radius: 12px;
 | 
			
		||||
          font-size: 11px;
 | 
			
		||||
          font-weight: 500;
 | 
			
		||||
        }
 | 
			
		||||
      `}</style>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS feeds (
 | 
			
		||||
  url TEXT NOT NULL UNIQUE,
 | 
			
		||||
  title TEXT,
 | 
			
		||||
  description TEXT,
 | 
			
		||||
  category TEXT,
 | 
			
		||||
  last_updated TEXT,
 | 
			
		||||
  created_at TEXT NOT NULL,
 | 
			
		||||
  active BOOLEAN DEFAULT 1
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ class BatchScheduler {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private currentAbortController?: AbortController;
 | 
			
		||||
  private migrationCompleted = false;
 | 
			
		||||
 | 
			
		||||
  private readonly SIX_HOURS_MS = 6 * 60 * 60 * 1000; // 6 hours in milliseconds
 | 
			
		||||
 | 
			
		||||
@@ -84,6 +85,28 @@ class BatchScheduler {
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      console.log("🔄 Running scheduled batch process...");
 | 
			
		||||
      
 | 
			
		||||
      // Run migration for feeds without categories (only once)
 | 
			
		||||
      if (!this.migrationCompleted) {
 | 
			
		||||
        try {
 | 
			
		||||
          const { migrateFeedsWithCategories, getFeedCategoryMigrationStatus } = await import("./database.js");
 | 
			
		||||
          const migrationStatus = await getFeedCategoryMigrationStatus();
 | 
			
		||||
          
 | 
			
		||||
          if (!migrationStatus.migrationComplete) {
 | 
			
		||||
            console.log("🔄 Running feed category migration...");
 | 
			
		||||
            await migrateFeedsWithCategories();
 | 
			
		||||
            this.migrationCompleted = true;
 | 
			
		||||
          } else {
 | 
			
		||||
            console.log("✅ Feed category migration already complete");
 | 
			
		||||
            this.migrationCompleted = true;
 | 
			
		||||
          }
 | 
			
		||||
        } catch (migrationError) {
 | 
			
		||||
          console.error("❌ Error during feed category migration:", migrationError);
 | 
			
		||||
          // Don't fail the entire batch process due to migration error
 | 
			
		||||
          this.migrationCompleted = true; // Mark as completed to avoid retrying every batch
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      await batchProcess(this.currentAbortController.signal);
 | 
			
		||||
      console.log("✅ Scheduled batch process completed");
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -131,6 +131,7 @@ function initializeDatabase(): Database {
 | 
			
		||||
    url TEXT NOT NULL UNIQUE,
 | 
			
		||||
    title TEXT,
 | 
			
		||||
    description TEXT,
 | 
			
		||||
    category TEXT,
 | 
			
		||||
    last_updated TEXT,
 | 
			
		||||
    created_at TEXT NOT NULL,
 | 
			
		||||
    active BOOLEAN DEFAULT 1
 | 
			
		||||
@@ -267,6 +268,7 @@ export interface EpisodeWithFeedInfo {
 | 
			
		||||
  feedId: string;
 | 
			
		||||
  feedTitle?: string;
 | 
			
		||||
  feedUrl: string;
 | 
			
		||||
  feedCategory?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Feed management functions
 | 
			
		||||
@@ -280,11 +282,12 @@ export async function saveFeed(
 | 
			
		||||
    if (existingFeed) {
 | 
			
		||||
      // Update existing feed
 | 
			
		||||
      const updateStmt = db.prepare(
 | 
			
		||||
        "UPDATE feeds SET title = ?, description = ?, last_updated = ?, active = ? WHERE url = ?",
 | 
			
		||||
        "UPDATE feeds SET title = ?, description = ?, category = ?, last_updated = ?, active = ? WHERE url = ?",
 | 
			
		||||
      );
 | 
			
		||||
      updateStmt.run(
 | 
			
		||||
        feed.title || null,
 | 
			
		||||
        feed.description || null,
 | 
			
		||||
        feed.category || null,
 | 
			
		||||
        feed.lastUpdated || null,
 | 
			
		||||
        feed.active !== undefined ? (feed.active ? 1 : 0) : 1,
 | 
			
		||||
        feed.url,
 | 
			
		||||
@@ -296,13 +299,14 @@ export async function saveFeed(
 | 
			
		||||
      const createdAt = new Date().toISOString();
 | 
			
		||||
 | 
			
		||||
      const insertStmt = db.prepare(
 | 
			
		||||
        "INSERT INTO feeds (id, url, title, description, last_updated, created_at, active) VALUES (?, ?, ?, ?, ?, ?, ?)",
 | 
			
		||||
        "INSERT INTO feeds (id, url, title, description, category, last_updated, created_at, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
 | 
			
		||||
      );
 | 
			
		||||
      insertStmt.run(
 | 
			
		||||
        id,
 | 
			
		||||
        feed.url,
 | 
			
		||||
        feed.title || null,
 | 
			
		||||
        feed.description || null,
 | 
			
		||||
        feed.category || null,
 | 
			
		||||
        feed.lastUpdated || null,
 | 
			
		||||
        createdAt,
 | 
			
		||||
        feed.active !== undefined ? (feed.active ? 1 : 0) : 1,
 | 
			
		||||
@@ -407,7 +411,8 @@ export async function fetchEpisodesWithFeedInfo(): Promise<
 | 
			
		||||
        a.pub_date as articlePubDate,
 | 
			
		||||
        f.id as feedId,
 | 
			
		||||
        f.title as feedTitle,
 | 
			
		||||
        f.url as feedUrl
 | 
			
		||||
        f.url as feedUrl,
 | 
			
		||||
        f.category as feedCategory
 | 
			
		||||
      FROM episodes e
 | 
			
		||||
      JOIN articles a ON e.article_id = a.id
 | 
			
		||||
      JOIN feeds f ON a.feed_id = f.id
 | 
			
		||||
@@ -432,6 +437,7 @@ export async function fetchEpisodesWithFeedInfo(): Promise<
 | 
			
		||||
      feedId: row.feedId,
 | 
			
		||||
      feedTitle: row.feedTitle,
 | 
			
		||||
      feedUrl: row.feedUrl,
 | 
			
		||||
      feedCategory: row.feedCategory,
 | 
			
		||||
    }));
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("Error fetching episodes with feed info:", error);
 | 
			
		||||
@@ -459,7 +465,8 @@ export async function fetchEpisodesByFeedId(
 | 
			
		||||
        a.pub_date as articlePubDate,
 | 
			
		||||
        f.id as feedId,
 | 
			
		||||
        f.title as feedTitle,
 | 
			
		||||
        f.url as feedUrl
 | 
			
		||||
        f.url as feedUrl,
 | 
			
		||||
        f.category as feedCategory
 | 
			
		||||
      FROM episodes e
 | 
			
		||||
      JOIN articles a ON e.article_id = a.id
 | 
			
		||||
      JOIN feeds f ON a.feed_id = f.id
 | 
			
		||||
@@ -484,6 +491,7 @@ export async function fetchEpisodesByFeedId(
 | 
			
		||||
      feedId: row.feedId,
 | 
			
		||||
      feedTitle: row.feedTitle,
 | 
			
		||||
      feedUrl: row.feedUrl,
 | 
			
		||||
      feedCategory: row.feedCategory,
 | 
			
		||||
    }));
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("Error fetching episodes by feed ID:", error);
 | 
			
		||||
@@ -511,7 +519,8 @@ export async function fetchEpisodeWithSourceInfo(
 | 
			
		||||
        a.pub_date as articlePubDate,
 | 
			
		||||
        f.id as feedId,
 | 
			
		||||
        f.title as feedTitle,
 | 
			
		||||
        f.url as feedUrl
 | 
			
		||||
        f.url as feedUrl,
 | 
			
		||||
        f.category as feedCategory
 | 
			
		||||
      FROM episodes e
 | 
			
		||||
      JOIN articles a ON e.article_id = a.id
 | 
			
		||||
      JOIN feeds f ON a.feed_id = f.id
 | 
			
		||||
@@ -536,6 +545,7 @@ export async function fetchEpisodeWithSourceInfo(
 | 
			
		||||
      feedId: row.feedId,
 | 
			
		||||
      feedTitle: row.feedTitle,
 | 
			
		||||
      feedUrl: row.feedUrl,
 | 
			
		||||
      feedCategory: row.feedCategory,
 | 
			
		||||
    };
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("Error fetching episode with source info:", error);
 | 
			
		||||
@@ -1104,6 +1114,100 @@ export async function updateFeedRequestStatus(
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Migration function to classify existing feeds without categories
 | 
			
		||||
export async function migrateFeedsWithCategories(): Promise<void> {
 | 
			
		||||
  try {
 | 
			
		||||
    console.log("🔄 Starting feed category migration...");
 | 
			
		||||
 | 
			
		||||
    // Get all feeds without categories
 | 
			
		||||
    const stmt = db.prepare("SELECT * FROM feeds WHERE category IS NULL OR category = ''");
 | 
			
		||||
    const feedsWithoutCategories = stmt.all() as any[];
 | 
			
		||||
 | 
			
		||||
    if (feedsWithoutCategories.length === 0) {
 | 
			
		||||
      console.log("✅ All feeds already have categories assigned");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log(`📋 Found ${feedsWithoutCategories.length} feeds without categories`);
 | 
			
		||||
 | 
			
		||||
    // Import LLM service
 | 
			
		||||
    const { openAI_ClassifyFeed } = await import("./llm.js");
 | 
			
		||||
 | 
			
		||||
    let processedCount = 0;
 | 
			
		||||
    let errorCount = 0;
 | 
			
		||||
 | 
			
		||||
    for (const feed of feedsWithoutCategories) {
 | 
			
		||||
      try {
 | 
			
		||||
        // Use title for classification, fallback to URL if no title
 | 
			
		||||
        const titleForClassification = feed.title || feed.url;
 | 
			
		||||
        
 | 
			
		||||
        console.log(`🔍 Classifying feed: ${titleForClassification}`);
 | 
			
		||||
        
 | 
			
		||||
        // Classify the feed
 | 
			
		||||
        const category = await openAI_ClassifyFeed(titleForClassification);
 | 
			
		||||
        
 | 
			
		||||
        // Update the feed with the category
 | 
			
		||||
        const updateStmt = db.prepare("UPDATE feeds SET category = ? WHERE id = ?");
 | 
			
		||||
        updateStmt.run(category, feed.id);
 | 
			
		||||
        
 | 
			
		||||
        console.log(`✅ Assigned category "${category}" to feed: ${titleForClassification}`);
 | 
			
		||||
        processedCount++;
 | 
			
		||||
        
 | 
			
		||||
        // Add a small delay to avoid rate limiting
 | 
			
		||||
        await new Promise(resolve => setTimeout(resolve, 1000));
 | 
			
		||||
        
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error(`❌ Failed to classify feed ${feed.title || feed.url}:`, error);
 | 
			
		||||
        errorCount++;
 | 
			
		||||
        
 | 
			
		||||
        // Set a default category for failed classifications
 | 
			
		||||
        const defaultCategory = "その他";
 | 
			
		||||
        const updateStmt = db.prepare("UPDATE feeds SET category = ? WHERE id = ?");
 | 
			
		||||
        updateStmt.run(defaultCategory, feed.id);
 | 
			
		||||
        console.log(`⚠️  Assigned default category "${defaultCategory}" to feed: ${feed.title || feed.url}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log(`✅ Feed category migration completed`);
 | 
			
		||||
    console.log(`📊 Processed: ${processedCount}, Errors: ${errorCount}, Total: ${feedsWithoutCategories.length}`);
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("❌ Error during feed category migration:", error);
 | 
			
		||||
    throw error;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Function to get migration status
 | 
			
		||||
export async function getFeedCategoryMigrationStatus(): Promise<{
 | 
			
		||||
  totalFeeds: number;
 | 
			
		||||
  feedsWithCategories: number;
 | 
			
		||||
  feedsWithoutCategories: number;
 | 
			
		||||
  migrationComplete: boolean;
 | 
			
		||||
}> {
 | 
			
		||||
  try {
 | 
			
		||||
    const totalStmt = db.prepare("SELECT COUNT(*) as count FROM feeds WHERE active = 1");
 | 
			
		||||
    const totalResult = totalStmt.get() as any;
 | 
			
		||||
    const totalFeeds = totalResult.count;
 | 
			
		||||
 | 
			
		||||
    const withCategoriesStmt = db.prepare("SELECT COUNT(*) as count FROM feeds WHERE active = 1 AND category IS NOT NULL AND category != ''");
 | 
			
		||||
    const withCategoriesResult = withCategoriesStmt.get() as any;
 | 
			
		||||
    const feedsWithCategories = withCategoriesResult.count;
 | 
			
		||||
 | 
			
		||||
    const feedsWithoutCategories = totalFeeds - feedsWithCategories;
 | 
			
		||||
    const migrationComplete = feedsWithoutCategories === 0;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      totalFeeds,
 | 
			
		||||
      feedsWithCategories,
 | 
			
		||||
      feedsWithoutCategories,
 | 
			
		||||
      migrationComplete,
 | 
			
		||||
    };
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("Error getting migration status:", error);
 | 
			
		||||
    throw error;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function closeDatabase(): void {
 | 
			
		||||
  db.close();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user