Some fixes
This commit is contained in:
		@@ -102,11 +102,26 @@ function App() {
 | 
				
			|||||||
  const [approvalNotes, setApprovalNotes] = useState<{ [key: string]: string }>(
 | 
					  const [approvalNotes, setApprovalNotes] = useState<{ [key: string]: string }>(
 | 
				
			||||||
    {},
 | 
					    {},
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const [editingSettings, setEditingSettings] = useState<{ [key: string]: string }>({});
 | 
					  const [editingSettings, setEditingSettings] = useState<{
 | 
				
			||||||
  const [categories, setCategories] = useState<CategoryData>({ feedCategories: [], episodeCategories: [], allCategories: [] });
 | 
					    [key: string]: string;
 | 
				
			||||||
  const [categoryCounts, setCategoryCounts] = useState<{ [category: string]: CategoryCounts }>({});
 | 
					  }>({});
 | 
				
			||||||
 | 
					  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" | "categories"
 | 
					    | "dashboard"
 | 
				
			||||||
 | 
					    | "feeds"
 | 
				
			||||||
 | 
					    | "episodes"
 | 
				
			||||||
 | 
					    | "env"
 | 
				
			||||||
 | 
					    | "settings"
 | 
				
			||||||
 | 
					    | "batch"
 | 
				
			||||||
 | 
					    | "requests"
 | 
				
			||||||
 | 
					    | "categories"
 | 
				
			||||||
  >("dashboard");
 | 
					  >("dashboard");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
@@ -116,16 +131,23 @@ function App() {
 | 
				
			|||||||
  const loadData = async () => {
 | 
					  const loadData = async () => {
 | 
				
			||||||
    setLoading(true);
 | 
					    setLoading(true);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const [feedsRes, statsRes, envRes, settingsRes, requestsRes, episodesRes, categoriesRes] =
 | 
					      const [
 | 
				
			||||||
        await Promise.all([
 | 
					        feedsRes,
 | 
				
			||||||
          fetch("/api/admin/feeds"),
 | 
					        statsRes,
 | 
				
			||||||
          fetch("/api/admin/stats"),
 | 
					        envRes,
 | 
				
			||||||
          fetch("/api/admin/env"),
 | 
					        settingsRes,
 | 
				
			||||||
          fetch("/api/admin/settings"),
 | 
					        requestsRes,
 | 
				
			||||||
          fetch("/api/admin/feed-requests"),
 | 
					        episodesRes,
 | 
				
			||||||
          fetch("/api/admin/episodes"),
 | 
					        categoriesRes,
 | 
				
			||||||
          fetch("/api/admin/categories/all"),
 | 
					      ] = 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 (
 | 
					      if (
 | 
				
			||||||
        !feedsRes.ok ||
 | 
					        !feedsRes.ok ||
 | 
				
			||||||
@@ -139,16 +161,23 @@ function App() {
 | 
				
			|||||||
        throw new Error("Failed to load data");
 | 
					        throw new Error("Failed to load data");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const [feedsData, statsData, envData, settingsData, requestsData, episodesData, categoriesData] =
 | 
					      const [
 | 
				
			||||||
        await Promise.all([
 | 
					        feedsData,
 | 
				
			||||||
          feedsRes.json(),
 | 
					        statsData,
 | 
				
			||||||
          statsRes.json(),
 | 
					        envData,
 | 
				
			||||||
          envRes.json(),
 | 
					        settingsData,
 | 
				
			||||||
          settingsRes.json(),
 | 
					        requestsData,
 | 
				
			||||||
          requestsRes.json(),
 | 
					        episodesData,
 | 
				
			||||||
          episodesRes.json(),
 | 
					        categoriesData,
 | 
				
			||||||
          categoriesRes.json(),
 | 
					      ] = await Promise.all([
 | 
				
			||||||
        ]);
 | 
					        feedsRes.json(),
 | 
				
			||||||
 | 
					        statsRes.json(),
 | 
				
			||||||
 | 
					        envRes.json(),
 | 
				
			||||||
 | 
					        settingsRes.json(),
 | 
				
			||||||
 | 
					        requestsRes.json(),
 | 
				
			||||||
 | 
					        episodesRes.json(),
 | 
				
			||||||
 | 
					        categoriesRes.json(),
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      setFeeds(feedsData);
 | 
					      setFeeds(feedsData);
 | 
				
			||||||
      setStats(statsData);
 | 
					      setStats(statsData);
 | 
				
			||||||
@@ -159,14 +188,18 @@ function App() {
 | 
				
			|||||||
      setCategories(categoriesData);
 | 
					      setCategories(categoriesData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Load category counts for all categories
 | 
					      // Load category counts for all categories
 | 
				
			||||||
      const countsPromises = categoriesData.allCategories.map(async (category: string) => {
 | 
					      const countsPromises = categoriesData.allCategories.map(
 | 
				
			||||||
        const res = await fetch(`/api/admin/categories/${encodeURIComponent(category)}/counts`);
 | 
					        async (category: string) => {
 | 
				
			||||||
        if (res.ok) {
 | 
					          const res = await fetch(
 | 
				
			||||||
          const counts = await res.json();
 | 
					            `/api/admin/categories/${encodeURIComponent(category)}/counts`,
 | 
				
			||||||
          return { category, counts };
 | 
					          );
 | 
				
			||||||
        }
 | 
					          if (res.ok) {
 | 
				
			||||||
        return { category, counts: { feedCount: 0, episodeCount: 0 } };
 | 
					            const counts = await res.json();
 | 
				
			||||||
      });
 | 
					            return { category, counts };
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return { category, counts: { feedCount: 0, episodeCount: 0 } };
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const countsResults = await Promise.all(countsPromises);
 | 
					      const countsResults = await Promise.all(countsPromises);
 | 
				
			||||||
      const countsMap: { [category: string]: CategoryCounts } = {};
 | 
					      const countsMap: { [category: string]: CategoryCounts } = {};
 | 
				
			||||||
@@ -426,26 +459,44 @@ function App() {
 | 
				
			|||||||
    setEditingSettings({ ...editingSettings, [key]: value });
 | 
					    setEditingSettings({ ...editingSettings, [key]: value });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const deleteCategory = async (category: string, target: "feeds" | "episodes" | "both") => {
 | 
					  const deleteCategory = async (
 | 
				
			||||||
    const targetText = target === "both" ? "フィードとエピソード" : target === "feeds" ? "フィード" : "エピソード";
 | 
					    category: string,
 | 
				
			||||||
    const counts = categoryCounts[category] || { feedCount: 0, episodeCount: 0 };
 | 
					    target: "feeds" | "episodes" | "both",
 | 
				
			||||||
    const totalCount = target === "both" ? counts.feedCount + counts.episodeCount : 
 | 
					  ) => {
 | 
				
			||||||
                      target === "feeds" ? counts.feedCount : counts.episodeCount;
 | 
					    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 (
 | 
					    if (
 | 
				
			||||||
      !confirm(
 | 
					      !confirm(
 | 
				
			||||||
        `本当にカテゴリ「${category}」を${targetText}から削除しますか?\n\n${totalCount}件のアイテムが影響を受けます。この操作は取り消せません。`
 | 
					        `本当にカテゴリ「${category}」を${targetText}から削除しますか?\n\n${totalCount}件のアイテムが影響を受けます。この操作は取り消せません。`,
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const res = await fetch(`/api/admin/categories/${encodeURIComponent(category)}`, {
 | 
					      const res = await fetch(
 | 
				
			||||||
        method: "DELETE",
 | 
					        `/api/admin/categories/${encodeURIComponent(category)}`,
 | 
				
			||||||
        headers: { "Content-Type": "application/json" },
 | 
					        {
 | 
				
			||||||
        body: JSON.stringify({ target }),
 | 
					          method: "DELETE",
 | 
				
			||||||
      });
 | 
					          headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
					          body: JSON.stringify({ target }),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const data = await res.json();
 | 
					      const data = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1230,26 +1281,33 @@ function App() {
 | 
				
			|||||||
              </p>
 | 
					              </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <div style={{ marginBottom: "20px" }}>
 | 
					              <div style={{ marginBottom: "20px" }}>
 | 
				
			||||||
                <div className="stats-grid" style={{ gridTemplateColumns: "repeat(4, 1fr)" }}>
 | 
					                <div
 | 
				
			||||||
 | 
					                  className="stats-grid"
 | 
				
			||||||
 | 
					                  style={{ gridTemplateColumns: "repeat(4, 1fr)" }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
                  <div className="stat-card">
 | 
					                  <div className="stat-card">
 | 
				
			||||||
                    <div className="value">{settings.length}</div>
 | 
					                    <div className="value">{settings.length}</div>
 | 
				
			||||||
                    <div className="label">総設定数</div>
 | 
					                    <div className="label">総設定数</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div className="stat-card">
 | 
					                  <div className="stat-card">
 | 
				
			||||||
                    <div className="value">
 | 
					                    <div className="value">
 | 
				
			||||||
                      {settings.filter(s => s.value !== null && s.value !== "").length}
 | 
					                      {
 | 
				
			||||||
 | 
					                        settings.filter(
 | 
				
			||||||
 | 
					                          (s) => s.value !== null && s.value !== "",
 | 
				
			||||||
 | 
					                        ).length
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                    <div className="label">設定済み</div>
 | 
					                    <div className="label">設定済み</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div className="stat-card">
 | 
					                  <div className="stat-card">
 | 
				
			||||||
                    <div className="value">
 | 
					                    <div className="value">
 | 
				
			||||||
                      {settings.filter(s => s.required).length}
 | 
					                      {settings.filter((s) => s.required).length}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                    <div className="label">必須設定</div>
 | 
					                    <div className="label">必須設定</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div className="stat-card">
 | 
					                  <div className="stat-card">
 | 
				
			||||||
                    <div className="value">
 | 
					                    <div className="value">
 | 
				
			||||||
                      {settings.filter(s => s.isCredential).length}
 | 
					                      {settings.filter((s) => s.isCredential).length}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                    <div className="label">認証情報</div>
 | 
					                    <div className="label">認証情報</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
@@ -1258,63 +1316,109 @@ function App() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
              <div className="settings-list">
 | 
					              <div className="settings-list">
 | 
				
			||||||
                {settings.map((setting) => (
 | 
					                {settings.map((setting) => (
 | 
				
			||||||
                  <div key={setting.key} className="setting-item" style={{ 
 | 
					                  <div
 | 
				
			||||||
                    display: "flex", 
 | 
					                    key={setting.key}
 | 
				
			||||||
                    alignItems: "center",
 | 
					                    className="setting-item"
 | 
				
			||||||
                    padding: "16px",
 | 
					                    style={{
 | 
				
			||||||
                    border: "1px solid #ddd",
 | 
					                      display: "flex",
 | 
				
			||||||
                    borderRadius: "4px",
 | 
					                      alignItems: "center",
 | 
				
			||||||
                    marginBottom: "12px",
 | 
					                      padding: "16px",
 | 
				
			||||||
                    backgroundColor: setting.required && (!setting.value || setting.value === "") ? "#fff5f5" : "#fff"
 | 
					                      border: "1px solid #ddd",
 | 
				
			||||||
                  }}>
 | 
					                      borderRadius: "4px",
 | 
				
			||||||
 | 
					                      marginBottom: "12px",
 | 
				
			||||||
 | 
					                      backgroundColor:
 | 
				
			||||||
 | 
					                        setting.required &&
 | 
				
			||||||
 | 
					                        (!setting.value || setting.value === "")
 | 
				
			||||||
 | 
					                          ? "#fff5f5"
 | 
				
			||||||
 | 
					                          : "#fff",
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
                    <div style={{ flex: 1 }}>
 | 
					                    <div style={{ flex: 1 }}>
 | 
				
			||||||
                      <div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "4px" }}>
 | 
					                      <div
 | 
				
			||||||
                        <h4 style={{ margin: 0, fontSize: "16px" }}>{setting.key}</h4>
 | 
					                        style={{
 | 
				
			||||||
 | 
					                          display: "flex",
 | 
				
			||||||
 | 
					                          alignItems: "center",
 | 
				
			||||||
 | 
					                          gap: "8px",
 | 
				
			||||||
 | 
					                          marginBottom: "4px",
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <h4 style={{ margin: 0, fontSize: "16px" }}>
 | 
				
			||||||
 | 
					                          {setting.key}
 | 
				
			||||||
 | 
					                        </h4>
 | 
				
			||||||
                        {setting.required && (
 | 
					                        {setting.required && (
 | 
				
			||||||
                          <span style={{
 | 
					                          <span
 | 
				
			||||||
                            padding: "2px 6px",
 | 
					                            style={{
 | 
				
			||||||
                            background: "#dc3545",
 | 
					                              padding: "2px 6px",
 | 
				
			||||||
                            color: "white",
 | 
					                              background: "#dc3545",
 | 
				
			||||||
                            borderRadius: "4px",
 | 
					                              color: "white",
 | 
				
			||||||
                            fontSize: "10px"
 | 
					                              borderRadius: "4px",
 | 
				
			||||||
                          }}>必須</span>
 | 
					                              fontSize: "10px",
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
 | 
					                            必須
 | 
				
			||||||
 | 
					                          </span>
 | 
				
			||||||
                        )}
 | 
					                        )}
 | 
				
			||||||
                        {setting.isCredential && (
 | 
					                        {setting.isCredential && (
 | 
				
			||||||
                          <span style={{
 | 
					                          <span
 | 
				
			||||||
                            padding: "2px 6px",
 | 
					                            style={{
 | 
				
			||||||
                            background: "#ffc107",
 | 
					                              padding: "2px 6px",
 | 
				
			||||||
                            color: "black",
 | 
					                              background: "#ffc107",
 | 
				
			||||||
                            borderRadius: "4px",
 | 
					                              color: "black",
 | 
				
			||||||
                            fontSize: "10px"
 | 
					                              borderRadius: "4px",
 | 
				
			||||||
                          }}>認証情報</span>
 | 
					                              fontSize: "10px",
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
 | 
					                            認証情報
 | 
				
			||||||
 | 
					                          </span>
 | 
				
			||||||
                        )}
 | 
					                        )}
 | 
				
			||||||
                      </div>
 | 
					                      </div>
 | 
				
			||||||
                      <p style={{ margin: "0 0 8px 0", fontSize: "14px", color: "#666" }}>
 | 
					                      <p
 | 
				
			||||||
 | 
					                        style={{
 | 
				
			||||||
 | 
					                          margin: "0 0 8px 0",
 | 
				
			||||||
 | 
					                          fontSize: "14px",
 | 
				
			||||||
 | 
					                          color: "#666",
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
                        {setting.description}
 | 
					                        {setting.description}
 | 
				
			||||||
                      </p>
 | 
					                      </p>
 | 
				
			||||||
                      <div style={{ fontSize: "12px", color: "#999" }}>
 | 
					                      <div style={{ fontSize: "12px", color: "#999" }}>
 | 
				
			||||||
                        デフォルト値: {setting.defaultValue || "なし"} |
 | 
					                        デフォルト値: {setting.defaultValue || "なし"} |
 | 
				
			||||||
                        最終更新: {new Date(setting.updatedAt).toLocaleString("ja-JP")}
 | 
					                        最終更新:{" "}
 | 
				
			||||||
 | 
					                        {new Date(setting.updatedAt).toLocaleString("ja-JP")}
 | 
				
			||||||
                      </div>
 | 
					                      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                      {editingSettings[setting.key] !== undefined ? (
 | 
					                      {editingSettings[setting.key] !== undefined ? (
 | 
				
			||||||
                        <div style={{ marginTop: "8px", display: "flex", gap: "8px", alignItems: "center" }}>
 | 
					                        <div
 | 
				
			||||||
 | 
					                          style={{
 | 
				
			||||||
 | 
					                            marginTop: "8px",
 | 
				
			||||||
 | 
					                            display: "flex",
 | 
				
			||||||
 | 
					                            gap: "8px",
 | 
				
			||||||
 | 
					                            alignItems: "center",
 | 
				
			||||||
 | 
					                          }}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
                          <input
 | 
					                          <input
 | 
				
			||||||
                            type={setting.isCredential ? "password" : "text"}
 | 
					                            type={setting.isCredential ? "password" : "text"}
 | 
				
			||||||
                            value={editingSettings[setting.key]}
 | 
					                            value={editingSettings[setting.key]}
 | 
				
			||||||
                            onChange={(e) => updateEditingValue(setting.key, e.target.value)}
 | 
					                            onChange={(e) =>
 | 
				
			||||||
 | 
					                              updateEditingValue(setting.key, e.target.value)
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
                            placeholder={setting.defaultValue || "値を入力..."}
 | 
					                            placeholder={setting.defaultValue || "値を入力..."}
 | 
				
			||||||
                            style={{
 | 
					                            style={{
 | 
				
			||||||
                              flex: 1,
 | 
					                              flex: 1,
 | 
				
			||||||
                              padding: "6px 10px",
 | 
					                              padding: "6px 10px",
 | 
				
			||||||
                              border: "1px solid #ddd",
 | 
					                              border: "1px solid #ddd",
 | 
				
			||||||
                              borderRadius: "4px",
 | 
					                              borderRadius: "4px",
 | 
				
			||||||
                              fontSize: "14px"
 | 
					                              fontSize: "14px",
 | 
				
			||||||
                            }}
 | 
					                            }}
 | 
				
			||||||
                          />
 | 
					                          />
 | 
				
			||||||
                          <button
 | 
					                          <button
 | 
				
			||||||
                            className="btn btn-success"
 | 
					                            className="btn btn-success"
 | 
				
			||||||
                            onClick={() => updateSetting(setting.key, editingSettings[setting.key])}
 | 
					                            onClick={() =>
 | 
				
			||||||
 | 
					                              updateSetting(
 | 
				
			||||||
 | 
					                                setting.key,
 | 
				
			||||||
 | 
					                                editingSettings[setting.key],
 | 
				
			||||||
 | 
					                              )
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
                            style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
					                            style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
				
			||||||
                          >
 | 
					                          >
 | 
				
			||||||
                            保存
 | 
					                            保存
 | 
				
			||||||
@@ -1328,20 +1432,36 @@ function App() {
 | 
				
			|||||||
                          </button>
 | 
					                          </button>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                      ) : (
 | 
					                      ) : (
 | 
				
			||||||
                        <div style={{ marginTop: "8px", display: "flex", alignItems: "center", gap: "12px" }}>
 | 
					                        <div
 | 
				
			||||||
                          <div style={{
 | 
					                          style={{
 | 
				
			||||||
                            flex: 1,
 | 
					                            marginTop: "8px",
 | 
				
			||||||
                            padding: "6px 10px",
 | 
					 | 
				
			||||||
                            background: "#f8f9fa",
 | 
					 | 
				
			||||||
                            border: "1px solid #e9ecef",
 | 
					 | 
				
			||||||
                            borderRadius: "4px",
 | 
					 | 
				
			||||||
                            fontSize: "14px",
 | 
					 | 
				
			||||||
                            minHeight: "32px",
 | 
					 | 
				
			||||||
                            display: "flex",
 | 
					                            display: "flex",
 | 
				
			||||||
                            alignItems: "center"
 | 
					                            alignItems: "center",
 | 
				
			||||||
                          }}>
 | 
					                            gap: "12px",
 | 
				
			||||||
 | 
					                          }}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          <div
 | 
				
			||||||
 | 
					                            style={{
 | 
				
			||||||
 | 
					                              flex: 1,
 | 
				
			||||||
 | 
					                              padding: "6px 10px",
 | 
				
			||||||
 | 
					                              background: "#f8f9fa",
 | 
				
			||||||
 | 
					                              border: "1px solid #e9ecef",
 | 
				
			||||||
 | 
					                              borderRadius: "4px",
 | 
				
			||||||
 | 
					                              fontSize: "14px",
 | 
				
			||||||
 | 
					                              minHeight: "32px",
 | 
				
			||||||
 | 
					                              display: "flex",
 | 
				
			||||||
 | 
					                              alignItems: "center",
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
                            {setting.value === null || setting.value === "" ? (
 | 
					                            {setting.value === null || setting.value === "" ? (
 | 
				
			||||||
                              <span style={{ color: "#dc3545", fontStyle: "italic" }}>未設定</span>
 | 
					                              <span
 | 
				
			||||||
 | 
					                                style={{
 | 
				
			||||||
 | 
					                                  color: "#dc3545",
 | 
				
			||||||
 | 
					                                  fontStyle: "italic",
 | 
				
			||||||
 | 
					                                }}
 | 
				
			||||||
 | 
					                              >
 | 
				
			||||||
 | 
					                                未設定
 | 
				
			||||||
 | 
					                              </span>
 | 
				
			||||||
                            ) : setting.isCredential ? (
 | 
					                            ) : setting.isCredential ? (
 | 
				
			||||||
                              <span style={{ color: "#666" }}>••••••••</span>
 | 
					                              <span style={{ color: "#666" }}>••••••••</span>
 | 
				
			||||||
                            ) : (
 | 
					                            ) : (
 | 
				
			||||||
@@ -1350,7 +1470,9 @@ function App() {
 | 
				
			|||||||
                          </div>
 | 
					                          </div>
 | 
				
			||||||
                          <button
 | 
					                          <button
 | 
				
			||||||
                            className="btn btn-primary"
 | 
					                            className="btn btn-primary"
 | 
				
			||||||
                            onClick={() => startEditingSetting(setting.key, setting.value)}
 | 
					                            onClick={() =>
 | 
				
			||||||
 | 
					                              startEditingSetting(setting.key, setting.value)
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
                            style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
					                            style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
				
			||||||
                          >
 | 
					                          >
 | 
				
			||||||
                            編集
 | 
					                            編集
 | 
				
			||||||
@@ -1379,10 +1501,18 @@ function App() {
 | 
				
			|||||||
                    paddingLeft: "20px",
 | 
					                    paddingLeft: "20px",
 | 
				
			||||||
                  }}
 | 
					                  }}
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                  <li><strong>必須設定:</strong> アプリケーションの動作に必要な設定です</li>
 | 
					                  <li>
 | 
				
			||||||
                  <li><strong>認証情報:</strong> セキュリティ上重要な情報で、表示時にマスクされます</li>
 | 
					                    <strong>必須設定:</strong>{" "}
 | 
				
			||||||
 | 
					                    アプリケーションの動作に必要な設定です
 | 
				
			||||||
 | 
					                  </li>
 | 
				
			||||||
 | 
					                  <li>
 | 
				
			||||||
 | 
					                    <strong>認証情報:</strong>{" "}
 | 
				
			||||||
 | 
					                    セキュリティ上重要な情報で、表示時にマスクされます
 | 
				
			||||||
 | 
					                  </li>
 | 
				
			||||||
                  <li>設定の変更は即座にアプリケーションに反映されます</li>
 | 
					                  <li>設定の変更は即座にアプリケーションに反映されます</li>
 | 
				
			||||||
                  <li>デフォルト値が設定されている項目は、空にするとデフォルト値が使用されます</li>
 | 
					                  <li>
 | 
				
			||||||
 | 
					                    デフォルト値が設定されている項目は、空にするとデフォルト値が使用されます
 | 
				
			||||||
 | 
					                  </li>
 | 
				
			||||||
                </ul>
 | 
					                </ul>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </>
 | 
					            </>
 | 
				
			||||||
@@ -1396,17 +1526,26 @@ function App() {
 | 
				
			|||||||
              </p>
 | 
					              </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <div style={{ marginBottom: "20px" }}>
 | 
					              <div style={{ marginBottom: "20px" }}>
 | 
				
			||||||
                <div className="stats-grid" style={{ gridTemplateColumns: "repeat(3, 1fr)" }}>
 | 
					                <div
 | 
				
			||||||
 | 
					                  className="stats-grid"
 | 
				
			||||||
 | 
					                  style={{ gridTemplateColumns: "repeat(3, 1fr)" }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
                  <div className="stat-card">
 | 
					                  <div className="stat-card">
 | 
				
			||||||
                    <div className="value">{categories.allCategories.length}</div>
 | 
					                    <div className="value">
 | 
				
			||||||
 | 
					                      {categories.allCategories.length}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
                    <div className="label">総カテゴリ数</div>
 | 
					                    <div className="label">総カテゴリ数</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div className="stat-card">
 | 
					                  <div className="stat-card">
 | 
				
			||||||
                    <div className="value">{categories.feedCategories.length}</div>
 | 
					                    <div className="value">
 | 
				
			||||||
 | 
					                      {categories.feedCategories.length}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
                    <div className="label">フィードカテゴリ</div>
 | 
					                    <div className="label">フィードカテゴリ</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div className="stat-card">
 | 
					                  <div className="stat-card">
 | 
				
			||||||
                    <div className="value">{categories.episodeCategories.length}</div>
 | 
					                    <div className="value">
 | 
				
			||||||
 | 
					                      {categories.episodeCategories.length}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
                    <div className="label">エピソードカテゴリ</div>
 | 
					                    <div className="label">エピソードカテゴリ</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
@@ -1425,15 +1564,26 @@ function App() {
 | 
				
			|||||||
              ) : (
 | 
					              ) : (
 | 
				
			||||||
                <div style={{ marginTop: "24px" }}>
 | 
					                <div style={{ marginTop: "24px" }}>
 | 
				
			||||||
                  <h4>カテゴリ一覧 ({categories.allCategories.length}件)</h4>
 | 
					                  <h4>カテゴリ一覧 ({categories.allCategories.length}件)</h4>
 | 
				
			||||||
                  <div style={{ marginBottom: "16px", fontSize: "14px", color: "#666" }}>
 | 
					                  <div
 | 
				
			||||||
 | 
					                    style={{
 | 
				
			||||||
 | 
					                      marginBottom: "16px",
 | 
				
			||||||
 | 
					                      fontSize: "14px",
 | 
				
			||||||
 | 
					                      color: "#666",
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
                    削除対象を選択してから削除ボタンをクリックしてください。
 | 
					                    削除対象を選択してから削除ボタンをクリックしてください。
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  <div className="category-list">
 | 
					                  <div className="category-list">
 | 
				
			||||||
                    {categories.allCategories.map((category) => {
 | 
					                    {categories.allCategories.map((category) => {
 | 
				
			||||||
                      const counts = categoryCounts[category] || { feedCount: 0, episodeCount: 0 };
 | 
					                      const counts = categoryCounts[category] || {
 | 
				
			||||||
                      const isInFeeds = categories.feedCategories.includes(category);
 | 
					                        feedCount: 0,
 | 
				
			||||||
                      const isInEpisodes = categories.episodeCategories.includes(category);
 | 
					                        episodeCount: 0,
 | 
				
			||||||
 | 
					                      };
 | 
				
			||||||
 | 
					                      const isInFeeds =
 | 
				
			||||||
 | 
					                        categories.feedCategories.includes(category);
 | 
				
			||||||
 | 
					                      const isInEpisodes =
 | 
				
			||||||
 | 
					                        categories.episodeCategories.includes(category);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                      return (
 | 
					                      return (
 | 
				
			||||||
                        <div
 | 
					                        <div
 | 
				
			||||||
@@ -1442,42 +1592,70 @@ function App() {
 | 
				
			|||||||
                          style={{ marginBottom: "16px" }}
 | 
					                          style={{ marginBottom: "16px" }}
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
                          <div className="feed-info">
 | 
					                          <div className="feed-info">
 | 
				
			||||||
                            <h4 style={{ margin: "0 0 8px 0", fontSize: "16px" }}>
 | 
					                            <h4
 | 
				
			||||||
 | 
					                              style={{ margin: "0 0 8px 0", fontSize: "16px" }}
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
                              {category}
 | 
					                              {category}
 | 
				
			||||||
                            </h4>
 | 
					                            </h4>
 | 
				
			||||||
                            <div style={{ fontSize: "14px", color: "#666", marginBottom: "8px" }}>
 | 
					                            <div
 | 
				
			||||||
 | 
					                              style={{
 | 
				
			||||||
 | 
					                                fontSize: "14px",
 | 
				
			||||||
 | 
					                                color: "#666",
 | 
				
			||||||
 | 
					                                marginBottom: "8px",
 | 
				
			||||||
 | 
					                              }}
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
                              <span>フィード: {counts.feedCount}件</span>
 | 
					                              <span>フィード: {counts.feedCount}件</span>
 | 
				
			||||||
                              <span style={{ margin: "0 12px" }}>|</span>
 | 
					                              <span style={{ margin: "0 12px" }}>|</span>
 | 
				
			||||||
                              <span>エピソード: {counts.episodeCount}件</span>
 | 
					                              <span>エピソード: {counts.episodeCount}件</span>
 | 
				
			||||||
                              <span style={{ margin: "0 12px" }}>|</span>
 | 
					                              <span style={{ margin: "0 12px" }}>|</span>
 | 
				
			||||||
                              <span>合計: {counts.feedCount + counts.episodeCount}件</span>
 | 
					                              <span>
 | 
				
			||||||
 | 
					                                合計: {counts.feedCount + counts.episodeCount}件
 | 
				
			||||||
 | 
					                              </span>
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
                            <div style={{ fontSize: "12px", color: "#999" }}>
 | 
					                            <div style={{ fontSize: "12px", color: "#999" }}>
 | 
				
			||||||
                              <span style={{
 | 
					                              <span
 | 
				
			||||||
                                padding: "2px 6px",
 | 
					                                style={{
 | 
				
			||||||
                                background: isInFeeds ? "#e3f2fd" : "#f5f5f5",
 | 
					                                  padding: "2px 6px",
 | 
				
			||||||
                                color: isInFeeds ? "#1976d2" : "#999",
 | 
					                                  background: isInFeeds ? "#e3f2fd" : "#f5f5f5",
 | 
				
			||||||
                                borderRadius: "4px",
 | 
					                                  color: isInFeeds ? "#1976d2" : "#999",
 | 
				
			||||||
                                marginRight: "8px"
 | 
					                                  borderRadius: "4px",
 | 
				
			||||||
                              }}>
 | 
					                                  marginRight: "8px",
 | 
				
			||||||
 | 
					                                }}
 | 
				
			||||||
 | 
					                              >
 | 
				
			||||||
                                フィード: {isInFeeds ? "使用中" : "未使用"}
 | 
					                                フィード: {isInFeeds ? "使用中" : "未使用"}
 | 
				
			||||||
                              </span>
 | 
					                              </span>
 | 
				
			||||||
                              <span style={{
 | 
					                              <span
 | 
				
			||||||
                                padding: "2px 6px",
 | 
					                                style={{
 | 
				
			||||||
                                background: isInEpisodes ? "#e8f5e8" : "#f5f5f5",
 | 
					                                  padding: "2px 6px",
 | 
				
			||||||
                                color: isInEpisodes ? "#388e3c" : "#999",
 | 
					                                  background: isInEpisodes
 | 
				
			||||||
                                borderRadius: "4px"
 | 
					                                    ? "#e8f5e8"
 | 
				
			||||||
                              }}>
 | 
					                                    : "#f5f5f5",
 | 
				
			||||||
 | 
					                                  color: isInEpisodes ? "#388e3c" : "#999",
 | 
				
			||||||
 | 
					                                  borderRadius: "4px",
 | 
				
			||||||
 | 
					                                }}
 | 
				
			||||||
 | 
					                              >
 | 
				
			||||||
                                エピソード: {isInEpisodes ? "使用中" : "未使用"}
 | 
					                                エピソード: {isInEpisodes ? "使用中" : "未使用"}
 | 
				
			||||||
                              </span>
 | 
					                              </span>
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
                          </div>
 | 
					                          </div>
 | 
				
			||||||
                          <div className="feed-actions" style={{ flexDirection: "column", gap: "8px", minWidth: "160px" }}>
 | 
					                          <div
 | 
				
			||||||
 | 
					                            className="feed-actions"
 | 
				
			||||||
 | 
					                            style={{
 | 
				
			||||||
 | 
					                              flexDirection: "column",
 | 
				
			||||||
 | 
					                              gap: "8px",
 | 
				
			||||||
 | 
					                              minWidth: "160px",
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
                            {isInFeeds && (
 | 
					                            {isInFeeds && (
 | 
				
			||||||
                              <button
 | 
					                              <button
 | 
				
			||||||
                                className="btn btn-warning"
 | 
					                                className="btn btn-warning"
 | 
				
			||||||
                                onClick={() => deleteCategory(category, "feeds")}
 | 
					                                onClick={() =>
 | 
				
			||||||
                                style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
					                                  deleteCategory(category, "feeds")
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                style={{
 | 
				
			||||||
 | 
					                                  fontSize: "12px",
 | 
				
			||||||
 | 
					                                  padding: "6px 12px",
 | 
				
			||||||
 | 
					                                }}
 | 
				
			||||||
                              >
 | 
					                              >
 | 
				
			||||||
                                フィードから削除
 | 
					                                フィードから削除
 | 
				
			||||||
                              </button>
 | 
					                              </button>
 | 
				
			||||||
@@ -1485,8 +1663,13 @@ function App() {
 | 
				
			|||||||
                            {isInEpisodes && (
 | 
					                            {isInEpisodes && (
 | 
				
			||||||
                              <button
 | 
					                              <button
 | 
				
			||||||
                                className="btn btn-warning"
 | 
					                                className="btn btn-warning"
 | 
				
			||||||
                                onClick={() => deleteCategory(category, "episodes")}
 | 
					                                onClick={() =>
 | 
				
			||||||
                                style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
					                                  deleteCategory(category, "episodes")
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                style={{
 | 
				
			||||||
 | 
					                                  fontSize: "12px",
 | 
				
			||||||
 | 
					                                  padding: "6px 12px",
 | 
				
			||||||
 | 
					                                }}
 | 
				
			||||||
                              >
 | 
					                              >
 | 
				
			||||||
                                エピソードから削除
 | 
					                                エピソードから削除
 | 
				
			||||||
                              </button>
 | 
					                              </button>
 | 
				
			||||||
@@ -1495,7 +1678,10 @@ function App() {
 | 
				
			|||||||
                              <button
 | 
					                              <button
 | 
				
			||||||
                                className="btn btn-danger"
 | 
					                                className="btn btn-danger"
 | 
				
			||||||
                                onClick={() => deleteCategory(category, "both")}
 | 
					                                onClick={() => deleteCategory(category, "both")}
 | 
				
			||||||
                                style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
					                                style={{
 | 
				
			||||||
 | 
					                                  fontSize: "12px",
 | 
				
			||||||
 | 
					                                  padding: "6px 12px",
 | 
				
			||||||
 | 
					                                }}
 | 
				
			||||||
                              >
 | 
					                              >
 | 
				
			||||||
                                すべてから削除
 | 
					                                すべてから削除
 | 
				
			||||||
                              </button>
 | 
					                              </button>
 | 
				
			||||||
@@ -1525,10 +1711,21 @@ function App() {
 | 
				
			|||||||
                    paddingLeft: "20px",
 | 
					                    paddingLeft: "20px",
 | 
				
			||||||
                  }}
 | 
					                  }}
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                  <li><strong>フィードから削除:</strong> フィードのカテゴリのみを削除します</li>
 | 
					                  <li>
 | 
				
			||||||
                  <li><strong>エピソードから削除:</strong> エピソードのカテゴリのみを削除します</li>
 | 
					                    <strong>フィードから削除:</strong>{" "}
 | 
				
			||||||
                  <li><strong>すべてから削除:</strong> フィードとエピソード両方からカテゴリを削除します</li>
 | 
					                    フィードのカテゴリのみを削除します
 | 
				
			||||||
                  <li>削除されたカテゴリは NULL に設定され、分類が解除されます</li>
 | 
					                  </li>
 | 
				
			||||||
 | 
					                  <li>
 | 
				
			||||||
 | 
					                    <strong>エピソードから削除:</strong>{" "}
 | 
				
			||||||
 | 
					                    エピソードのカテゴリのみを削除します
 | 
				
			||||||
 | 
					                  </li>
 | 
				
			||||||
 | 
					                  <li>
 | 
				
			||||||
 | 
					                    <strong>すべてから削除:</strong>{" "}
 | 
				
			||||||
 | 
					                    フィードとエピソード両方からカテゴリを削除します
 | 
				
			||||||
 | 
					                  </li>
 | 
				
			||||||
 | 
					                  <li>
 | 
				
			||||||
 | 
					                    削除されたカテゴリは NULL に設定され、分類が解除されます
 | 
				
			||||||
 | 
					                  </li>
 | 
				
			||||||
                  <li>この操作は元に戻すことができません</li>
 | 
					                  <li>この操作は元に戻すことができません</li>
 | 
				
			||||||
                </ul>
 | 
					                </ul>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,17 +8,17 @@ import { batchScheduler } from "./services/batch-scheduler.js";
 | 
				
			|||||||
import { config } from "./services/config.js";
 | 
					import { config } from "./services/config.js";
 | 
				
			||||||
import { closeBrowser } from "./services/content-extractor.js";
 | 
					import { closeBrowser } from "./services/content-extractor.js";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  deleteCategoryFromBoth,
 | 
				
			||||||
  deleteEpisode,
 | 
					  deleteEpisode,
 | 
				
			||||||
 | 
					  deleteEpisodeCategory,
 | 
				
			||||||
  deleteFeed,
 | 
					  deleteFeed,
 | 
				
			||||||
 | 
					  deleteFeedCategory,
 | 
				
			||||||
  fetchAllEpisodes,
 | 
					  fetchAllEpisodes,
 | 
				
			||||||
  fetchEpisodesWithArticles,
 | 
					  fetchEpisodesWithArticles,
 | 
				
			||||||
  getAllCategories,
 | 
					  getAllCategories,
 | 
				
			||||||
  getAllFeedsIncludingInactive,
 | 
					  getAllFeedsIncludingInactive,
 | 
				
			||||||
  getAllUsedCategories,
 | 
					  getAllUsedCategories,
 | 
				
			||||||
  getCategoryCounts,
 | 
					  getCategoryCounts,
 | 
				
			||||||
  deleteCategoryFromBoth,
 | 
					 | 
				
			||||||
  deleteFeedCategory,
 | 
					 | 
				
			||||||
  deleteEpisodeCategory,
 | 
					 | 
				
			||||||
  getFeedByUrl,
 | 
					  getFeedByUrl,
 | 
				
			||||||
  getFeedRequests,
 | 
					  getFeedRequests,
 | 
				
			||||||
  getFeedsByCategory,
 | 
					  getFeedsByCategory,
 | 
				
			||||||
@@ -375,14 +375,19 @@ app.get("/api/admin/categories/:category/counts", async (c) => {
 | 
				
			|||||||
app.delete("/api/admin/categories/:category", async (c) => {
 | 
					app.delete("/api/admin/categories/:category", async (c) => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const category = decodeURIComponent(c.req.param("category"));
 | 
					    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() === "") {
 | 
					    if (!category || category.trim() === "") {
 | 
				
			||||||
      return c.json({ error: "Category name is required" }, 400);
 | 
					      return c.json({ error: "Category name is required" }, 400);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!target || !["feeds", "episodes", "both"].includes(target)) {
 | 
					    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}`);
 | 
					    console.log(`🗑️  Admin deleting category "${category}" from ${target}`);
 | 
				
			||||||
@@ -396,7 +401,7 @@ app.delete("/api/admin/categories/:category", async (c) => {
 | 
				
			|||||||
        category,
 | 
					        category,
 | 
				
			||||||
        feedChanges: result.feedChanges,
 | 
					        feedChanges: result.feedChanges,
 | 
				
			||||||
        episodeChanges: result.episodeChanges,
 | 
					        episodeChanges: result.episodeChanges,
 | 
				
			||||||
        totalChanges: result.feedChanges + result.episodeChanges
 | 
					        totalChanges: result.feedChanges + result.episodeChanges,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    } else if (target === "feeds") {
 | 
					    } else if (target === "feeds") {
 | 
				
			||||||
      const changes = await deleteFeedCategory(category);
 | 
					      const changes = await deleteFeedCategory(category);
 | 
				
			||||||
@@ -406,7 +411,7 @@ app.delete("/api/admin/categories/:category", async (c) => {
 | 
				
			|||||||
        category,
 | 
					        category,
 | 
				
			||||||
        feedChanges: changes,
 | 
					        feedChanges: changes,
 | 
				
			||||||
        episodeChanges: 0,
 | 
					        episodeChanges: 0,
 | 
				
			||||||
        totalChanges: changes
 | 
					        totalChanges: changes,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    } else if (target === "episodes") {
 | 
					    } else if (target === "episodes") {
 | 
				
			||||||
      const changes = await deleteEpisodeCategory(category);
 | 
					      const changes = await deleteEpisodeCategory(category);
 | 
				
			||||||
@@ -416,7 +421,7 @@ app.delete("/api/admin/categories/:category", async (c) => {
 | 
				
			|||||||
        category,
 | 
					        category,
 | 
				
			||||||
        feedChanges: 0,
 | 
					        feedChanges: 0,
 | 
				
			||||||
        episodeChanges: changes,
 | 
					        episodeChanges: changes,
 | 
				
			||||||
        totalChanges: changes
 | 
					        totalChanges: changes,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { Link, Route, Routes, useLocation } from "react-router-dom";
 | 
					 | 
				
			||||||
import { useEffect, useState } from "react";
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { Link, Route, Routes, useLocation } from "react-router-dom";
 | 
				
			||||||
import EpisodeDetail from "./components/EpisodeDetail";
 | 
					import EpisodeDetail from "./components/EpisodeDetail";
 | 
				
			||||||
import EpisodeList from "./components/EpisodeList";
 | 
					import EpisodeList from "./components/EpisodeList";
 | 
				
			||||||
import FeedDetail from "./components/FeedDetail";
 | 
					import FeedDetail from "./components/FeedDetail";
 | 
				
			||||||
@@ -10,13 +10,16 @@ import RSSEndpoints from "./components/RSSEndpoints";
 | 
				
			|||||||
function App() {
 | 
					function App() {
 | 
				
			||||||
  const location = useLocation();
 | 
					  const location = useLocation();
 | 
				
			||||||
  const [isDarkMode, setIsDarkMode] = useState(() => {
 | 
					  const [isDarkMode, setIsDarkMode] = useState(() => {
 | 
				
			||||||
    const saved = localStorage.getItem('darkMode');
 | 
					    const saved = localStorage.getItem("darkMode");
 | 
				
			||||||
    return saved ? JSON.parse(saved) : false;
 | 
					    return saved ? JSON.parse(saved) : false;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light');
 | 
					    document.documentElement.setAttribute(
 | 
				
			||||||
    localStorage.setItem('darkMode', JSON.stringify(isDarkMode));
 | 
					      "data-theme",
 | 
				
			||||||
 | 
					      isDarkMode ? "dark" : "light",
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    localStorage.setItem("darkMode", JSON.stringify(isDarkMode));
 | 
				
			||||||
  }, [isDarkMode]);
 | 
					  }, [isDarkMode]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const toggleDarkMode = () => {
 | 
					  const toggleDarkMode = () => {
 | 
				
			||||||
@@ -46,7 +49,7 @@ function App() {
 | 
				
			|||||||
                onClick={toggleDarkMode}
 | 
					                onClick={toggleDarkMode}
 | 
				
			||||||
                aria-label="テーマを切り替え"
 | 
					                aria-label="テーマを切り替え"
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                {isDarkMode ? '☀️' : '🌙'}
 | 
					                {isDarkMode ? "☀️" : "🌙"}
 | 
				
			||||||
              </button>
 | 
					              </button>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -90,7 +90,9 @@ function EpisodeList() {
 | 
				
			|||||||
          searchParams.append("category", 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) {
 | 
					        if (!response.ok) {
 | 
				
			||||||
          throw new Error("データベースからの取得に失敗しました");
 | 
					          throw new Error("データベースからの取得に失敗しました");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -402,9 +404,7 @@ function EpisodeList() {
 | 
				
			|||||||
              ))}
 | 
					              ))}
 | 
				
			||||||
            </select>
 | 
					            </select>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
          {isSearching && (
 | 
					          {isSearching && <span className="episode-meta-text">検索中...</span>}
 | 
				
			||||||
            <span className="episode-meta-text">検索中...</span>
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -587,7 +587,9 @@ function EpisodeList() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          <button
 | 
					          <button
 | 
				
			||||||
            className="btn btn-secondary"
 | 
					            className="btn btn-secondary"
 | 
				
			||||||
            onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
 | 
					            onClick={() =>
 | 
				
			||||||
 | 
					              setCurrentPage(Math.min(totalPages, currentPage + 1))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            disabled={currentPage === totalPages}
 | 
					            disabled={currentPage === totalPages}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            次へ
 | 
					            次へ
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -222,7 +222,10 @@ function FeedDetail() {
 | 
				
			|||||||
                    <strong>
 | 
					                    <strong>
 | 
				
			||||||
                      <Link
 | 
					                      <Link
 | 
				
			||||||
                        to={`/episode/${episode.id}`}
 | 
					                        to={`/episode/${episode.id}`}
 | 
				
			||||||
                        style={{ textDecoration: "none", color: "var(--accent-primary)" }}
 | 
					                        style={{
 | 
				
			||||||
 | 
					                          textDecoration: "none",
 | 
				
			||||||
 | 
					                          color: "var(--accent-primary)",
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
                      >
 | 
					                      >
 | 
				
			||||||
                        {episode.title}
 | 
					                        {episode.title}
 | 
				
			||||||
                      </Link>
 | 
					                      </Link>
 | 
				
			||||||
@@ -242,7 +245,10 @@ function FeedDetail() {
 | 
				
			|||||||
                      href={episode.articleLink}
 | 
					                      href={episode.articleLink}
 | 
				
			||||||
                      target="_blank"
 | 
					                      target="_blank"
 | 
				
			||||||
                      rel="noopener noreferrer"
 | 
					                      rel="noopener noreferrer"
 | 
				
			||||||
                      style={{ fontSize: "12px", color: "var(--text-secondary)" }}
 | 
					                      style={{
 | 
				
			||||||
 | 
					                        fontSize: "12px",
 | 
				
			||||||
 | 
					                        color: "var(--text-secondary)",
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                      元記事を見る
 | 
					                      元記事を見る
 | 
				
			||||||
                    </a>
 | 
					                    </a>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,7 +64,7 @@ function FeedList() {
 | 
				
			|||||||
      const data = await response.json();
 | 
					      const data = await response.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Handle paginated response
 | 
					      // Handle paginated response
 | 
				
			||||||
      if (data.feeds && typeof data.total !== 'undefined') {
 | 
					      if (data.feeds && typeof data.total !== "undefined") {
 | 
				
			||||||
        setFeeds(data.feeds);
 | 
					        setFeeds(data.feeds);
 | 
				
			||||||
        setFilteredFeeds(data.feeds);
 | 
					        setFilteredFeeds(data.feeds);
 | 
				
			||||||
        setTotalFeeds(data.total);
 | 
					        setTotalFeeds(data.total);
 | 
				
			||||||
@@ -96,7 +96,6 @@ function FeedList() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const formatDate = (dateString: string) => {
 | 
					  const formatDate = (dateString: string) => {
 | 
				
			||||||
    return new Date(dateString).toLocaleString("ja-JP");
 | 
					    return new Date(dateString).toLocaleString("ja-JP");
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
@@ -197,7 +196,11 @@ function FeedList() {
 | 
				
			|||||||
            <option value={45}>45件表示</option>
 | 
					            <option value={45}>45件表示</option>
 | 
				
			||||||
            <option value={60}>60件表示</option>
 | 
					            <option value={60}>60件表示</option>
 | 
				
			||||||
          </select>
 | 
					          </select>
 | 
				
			||||||
          <button type="button" className="btn btn-secondary" onClick={fetchFeeds}>
 | 
					          <button
 | 
				
			||||||
 | 
					            type="button"
 | 
				
			||||||
 | 
					            className="btn btn-secondary"
 | 
				
			||||||
 | 
					            onClick={fetchFeeds}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
            更新
 | 
					            更新
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@@ -287,7 +290,7 @@ function FeedList() {
 | 
				
			|||||||
                  <button
 | 
					                  <button
 | 
				
			||||||
                    type="button"
 | 
					                    type="button"
 | 
				
			||||||
                    key={pageNum}
 | 
					                    key={pageNum}
 | 
				
			||||||
                    className={`btn ${currentPage === pageNum ? 'btn-primary' : 'btn-secondary'}`}
 | 
					                    className={`btn ${currentPage === pageNum ? "btn-primary" : "btn-secondary"}`}
 | 
				
			||||||
                    onClick={() => setCurrentPage(pageNum)}
 | 
					                    onClick={() => setCurrentPage(pageNum)}
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    {pageNum}
 | 
					                    {pageNum}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								server.ts
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								server.ts
									
									
									
									
									
								
							@@ -515,7 +515,9 @@ app.get("/api/feeds", async (c) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // If pagination parameters are provided, use paginated endpoint
 | 
					    // If pagination parameters are provided, use paginated endpoint
 | 
				
			||||||
    if (page || limit) {
 | 
					    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 pageNum = page ? Number.parseInt(page, 10) : 1;
 | 
				
			||||||
      const limitNum = limit ? Number.parseInt(limit, 10) : 10;
 | 
					      const limitNum = limit ? Number.parseInt(limit, 10) : 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -527,7 +529,11 @@ app.get("/api/feeds", async (c) => {
 | 
				
			|||||||
        return c.json({ error: "Invalid limit (must be between 1-100)" }, 400);
 | 
					        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);
 | 
					      return c.json(result);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // Original behavior for backward compatibility
 | 
					      // Original behavior for backward compatibility
 | 
				
			||||||
@@ -633,7 +639,9 @@ app.get("/api/episodes-with-feed-info", async (c) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // If pagination parameters are provided, use paginated endpoint
 | 
					    // If pagination parameters are provided, use paginated endpoint
 | 
				
			||||||
    if (page || limit) {
 | 
					    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 pageNum = page ? Number.parseInt(page, 10) : 1;
 | 
				
			||||||
      const limitNum = limit ? Number.parseInt(limit, 10) : 20;
 | 
					      const limitNum = limit ? Number.parseInt(limit, 10) : 20;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -645,11 +653,17 @@ app.get("/api/episodes-with-feed-info", async (c) => {
 | 
				
			|||||||
        return c.json({ error: "Invalid limit (must be between 1-100)" }, 400);
 | 
					        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);
 | 
					      return c.json(result);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // Original behavior for backward compatibility
 | 
					      // Original behavior for backward compatibility
 | 
				
			||||||
      const { fetchEpisodesWithFeedInfo } = await import("./services/database.js");
 | 
					      const { fetchEpisodesWithFeedInfo } = await import(
 | 
				
			||||||
 | 
					        "./services/database.js"
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
      const episodes = await fetchEpisodesWithFeedInfo();
 | 
					      const episodes = await fetchEpisodesWithFeedInfo();
 | 
				
			||||||
      return c.json({ episodes });
 | 
					      return c.json({ episodes });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user