diff --git a/CLAUDE.md b/CLAUDE.md index 46ac35d..f6b8d6d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **Admin panel development**: `bun run dev:admin` - **Manual batch process**: `bun run scripts/fetch_and_generate.ts` - **Type checking**: `bunx tsc --noEmit` +- **Format code**: `bun run format` +- **Check format**: `bun run format:check` +- **Lint code**: `bun run lint` +- **Fix lint issues**: `bun run lint:fix` +- **Check all (format + lint)**: `bun run check` +- **Fix all issues**: `bun run check:fix` ## Architecture Overview diff --git a/admin-panel/package.json b/admin-panel/package.json index 157cb5a..edefc1f 100644 --- a/admin-panel/package.json +++ b/admin-panel/package.json @@ -18,4 +18,4 @@ "typescript": "^5.0.2", "vite": "^4.4.5" } -} \ No newline at end of file +} diff --git a/admin-panel/src/App.tsx b/admin-panel/src/App.tsx index 3ce7a6c..1d1ab45 100644 --- a/admin-panel/src/App.tsx +++ b/admin-panel/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from "react"; interface Feed { id: string; @@ -38,7 +38,7 @@ interface FeedRequest { url: string; requestedBy?: string; requestMessage?: string; - status: 'pending' | 'approved' | 'rejected'; + status: "pending" | "approved" | "rejected"; createdAt: string; reviewedAt?: string; reviewedBy?: string; @@ -53,10 +53,16 @@ function App() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); - const [newFeedUrl, setNewFeedUrl] = useState(''); - const [requestFilter, setRequestFilter] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all'); - const [approvalNotes, setApprovalNotes] = useState<{[key: string]: string}>({}); - const [activeTab, setActiveTab] = useState<'dashboard' | 'feeds' | 'env' | 'batch' | 'requests'>('dashboard'); + const [newFeedUrl, setNewFeedUrl] = useState(""); + const [requestFilter, setRequestFilter] = useState< + "all" | "pending" | "approved" | "rejected" + >("all"); + const [approvalNotes, setApprovalNotes] = useState<{ [key: string]: string }>( + {}, + ); + const [activeTab, setActiveTab] = useState< + "dashboard" | "feeds" | "env" | "batch" | "requests" + >("dashboard"); useEffect(() => { loadData(); @@ -66,21 +72,21 @@ function App() { setLoading(true); try { const [feedsRes, statsRes, envRes, requestsRes] = await Promise.all([ - fetch('/api/admin/feeds'), - fetch('/api/admin/stats'), - fetch('/api/admin/env'), - fetch('/api/admin/feed-requests') + fetch("/api/admin/feeds"), + fetch("/api/admin/stats"), + fetch("/api/admin/env"), + fetch("/api/admin/feed-requests"), ]); if (!feedsRes.ok || !statsRes.ok || !envRes.ok || !requestsRes.ok) { - throw new Error('Failed to load data'); + throw new Error("Failed to load data"); } const [feedsData, statsData, envData, requestsData] = await Promise.all([ feedsRes.json(), statsRes.json(), envRes.json(), - requestsRes.json() + requestsRes.json(), ]); setFeeds(feedsData); @@ -89,8 +95,8 @@ function App() { setFeedRequests(requestsData); setError(null); } catch (err) { - setError('データの読み込みに失敗しました'); - console.error('Error loading data:', err); + setError("データの読み込みに失敗しました"); + console.error("Error loading data:", err); } finally { setLoading(false); } @@ -101,35 +107,39 @@ function App() { if (!newFeedUrl.trim()) return; try { - const res = await fetch('/api/admin/feeds', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ feedUrl: newFeedUrl }) + const res = await fetch("/api/admin/feeds", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ feedUrl: newFeedUrl }), }); const data = await res.json(); if (res.ok) { setSuccess(data.message); - setNewFeedUrl(''); + setNewFeedUrl(""); loadData(); } else { - setError(data.error || 'フィード追加に失敗しました'); + setError(data.error || "フィード追加に失敗しました"); } } catch (err) { - setError('フィード追加に失敗しました'); - console.error('Error adding feed:', err); + setError("フィード追加に失敗しました"); + console.error("Error adding feed:", err); } }; const deleteFeed = async (feedId: string) => { - if (!confirm('本当にこのフィードを削除しますか?関連するすべての記事とエピソードも削除されます。')) { + if ( + !confirm( + "本当にこのフィードを削除しますか?関連するすべての記事とエピソードも削除されます。", + ) + ) { return; } try { const res = await fetch(`/api/admin/feeds/${feedId}`, { - method: 'DELETE' + method: "DELETE", }); const data = await res.json(); @@ -138,20 +148,20 @@ function App() { setSuccess(data.message); loadData(); } else { - setError(data.error || 'フィード削除に失敗しました'); + setError(data.error || "フィード削除に失敗しました"); } } catch (err) { - setError('フィード削除に失敗しました'); - console.error('Error deleting feed:', err); + setError("フィード削除に失敗しました"); + console.error("Error deleting feed:", err); } }; const toggleFeed = async (feedId: string, active: boolean) => { try { const res = await fetch(`/api/admin/feeds/${feedId}/toggle`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ active }) + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ active }), }); const data = await res.json(); @@ -160,18 +170,18 @@ function App() { setSuccess(data.message); loadData(); } else { - setError(data.error || 'フィードステータス変更に失敗しました'); + setError(data.error || "フィードステータス変更に失敗しました"); } } catch (err) { - setError('フィードステータス変更に失敗しました'); - console.error('Error toggling feed:', err); + setError("フィードステータス変更に失敗しました"); + console.error("Error toggling feed:", err); } }; const triggerBatch = async () => { try { - const res = await fetch('/api/admin/batch/trigger', { - method: 'POST' + const res = await fetch("/api/admin/batch/trigger", { + method: "POST", }); const data = await res.json(); @@ -180,47 +190,54 @@ function App() { setSuccess(data.message); loadData(); // Refresh data to update batch status } else { - setError(data.error || 'バッチ処理開始に失敗しました'); + setError(data.error || "バッチ処理開始に失敗しました"); } } catch (err) { - setError('バッチ処理開始に失敗しました'); - console.error('Error triggering batch:', err); + setError("バッチ処理開始に失敗しました"); + console.error("Error triggering batch:", err); } }; const forceStopBatch = async () => { - if (!confirm('実行中のバッチ処理を強制停止しますか?\n\n進行中の処理が中断され、データの整合性に影響が出る可能性があります。')) { + if ( + !confirm( + "実行中のバッチ処理を強制停止しますか?\n\n進行中の処理が中断され、データの整合性に影響が出る可能性があります。", + ) + ) { return; } try { - const res = await fetch('/api/admin/batch/force-stop', { - method: 'POST' + const res = await fetch("/api/admin/batch/force-stop", { + method: "POST", }); const data = await res.json(); if (res.ok) { - if (data.result === 'STOPPED') { + if (data.result === "STOPPED") { setSuccess(data.message); - } else if (data.result === 'NO_PROCESS') { + } else if (data.result === "NO_PROCESS") { setSuccess(data.message); } loadData(); // Refresh data to update batch status } else { - setError(data.error || 'バッチ処理強制停止に失敗しました'); + setError(data.error || "バッチ処理強制停止に失敗しました"); } } catch (err) { - setError('バッチ処理強制停止に失敗しました'); - console.error('Error force stopping batch:', err); + setError("バッチ処理強制停止に失敗しました"); + console.error("Error force stopping batch:", err); } }; const toggleBatchScheduler = async (enable: boolean) => { try { - const res = await fetch(`/api/admin/batch/${enable ? 'enable' : 'disable'}`, { - method: 'POST' - }); + const res = await fetch( + `/api/admin/batch/${enable ? "enable" : "disable"}`, + { + method: "POST", + }, + ); const data = await res.json(); @@ -228,61 +245,61 @@ function App() { setSuccess(data.message); loadData(); // Refresh data to update batch status } else { - setError(data.error || 'バッチスケジューラーの状態変更に失敗しました'); + setError(data.error || "バッチスケジューラーの状態変更に失敗しました"); } } catch (err) { - setError('バッチスケジューラーの状態変更に失敗しました'); - console.error('Error toggling batch scheduler:', err); + setError("バッチスケジューラーの状態変更に失敗しました"); + console.error("Error toggling batch scheduler:", err); } }; const approveFeedRequest = async (requestId: string, notes: string) => { try { const res = await fetch(`/api/admin/feed-requests/${requestId}/approve`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ adminNotes: notes }) + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ adminNotes: notes }), }); const data = await res.json(); if (res.ok) { - setSuccess(data.message || 'フィードリクエストを承認しました'); - setApprovalNotes({ ...approvalNotes, [requestId]: '' }); + setSuccess(data.message || "フィードリクエストを承認しました"); + setApprovalNotes({ ...approvalNotes, [requestId]: "" }); loadData(); } else { - setError(data.error || 'フィードリクエストの承認に失敗しました'); + setError(data.error || "フィードリクエストの承認に失敗しました"); } } catch (err) { - setError('フィードリクエストの承認に失敗しました'); - console.error('Error approving feed request:', err); + setError("フィードリクエストの承認に失敗しました"); + console.error("Error approving feed request:", err); } }; const rejectFeedRequest = async (requestId: string, notes: string) => { - if (!confirm('このフィードリクエストを拒否しますか?')) { + if (!confirm("このフィードリクエストを拒否しますか?")) { return; } try { const res = await fetch(`/api/admin/feed-requests/${requestId}/reject`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ adminNotes: notes }) + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ adminNotes: notes }), }); const data = await res.json(); if (res.ok) { - setSuccess(data.message || 'フィードリクエストを拒否しました'); - setApprovalNotes({ ...approvalNotes, [requestId]: '' }); + setSuccess(data.message || "フィードリクエストを拒否しました"); + setApprovalNotes({ ...approvalNotes, [requestId]: "" }); loadData(); } else { - setError(data.error || 'フィードリクエストの拒否に失敗しました'); + setError(data.error || "フィードリクエストの拒否に失敗しました"); } } catch (err) { - setError('フィードリクエストの拒否に失敗しました'); - console.error('Error rejecting feed request:', err); + setError("フィードリクエストの拒否に失敗しました"); + console.error("Error rejecting feed request:", err); } }; @@ -290,20 +307,26 @@ function App() { setApprovalNotes({ ...approvalNotes, [requestId]: notes }); }; - const filteredRequests = feedRequests.filter(request => { - if (requestFilter === 'all') return true; + const filteredRequests = feedRequests.filter((request) => { + if (requestFilter === "all") return true; return request.status === requestFilter; }); if (loading) { - return
読み込み中...
; + return ( +
+
読み込み中...
+
+ ); } return (

管理者パネル

-

RSS Podcast Manager - 管理者用インターフェース

+

+ RSS Podcast Manager - 管理者用インターフェース +

{error &&
{error}
} @@ -311,34 +334,34 @@ function App() {
-
- {activeTab === 'dashboard' && ( + {activeTab === "dashboard" && ( <>
@@ -370,44 +393,62 @@ function App() {
保留中リクエスト
-
- {stats?.batchScheduler?.enabled ? 'ON' : 'OFF'} +
+ {stats?.batchScheduler?.enabled ? "ON" : "OFF"}
バッチスケジューラー
-
- {stats?.batchScheduler?.canForceStop && ( - )} -
-
+

管理者パネルポート: {stats?.adminPort}

-

認証: {stats?.authEnabled ? '有効' : '無効'}

-

最終更新: {stats?.lastUpdated ? new Date(stats.lastUpdated).toLocaleString('ja-JP') : '不明'}

+

認証: {stats?.authEnabled ? "有効" : "無効"}

+

+ 最終更新:{" "} + {stats?.lastUpdated + ? new Date(stats.lastUpdated).toLocaleString("ja-JP") + : "不明"} +

)} - {activeTab === 'feeds' && ( + {activeTab === "feeds" && ( <>
@@ -427,10 +468,16 @@ function App() { -
+

フィード一覧 ({feeds.length}件)

{feeds.length === 0 ? ( -

+

フィードが登録されていません

) : ( @@ -438,18 +485,20 @@ function App() { {feeds.map((feed) => (
  • -

    {feed.title || 'タイトル未設定'}

    +

    {feed.title || "タイトル未設定"}

    {feed.url}
    - - {feed.active ? 'アクティブ' : '非アクティブ'} + + {feed.active ? "アクティブ" : "非アクティブ"}
    -
    -

    +

    スケジューラーを無効化すると、定期的なバッチ処理が停止します。手動実行は引き続き可能です。

  • -
    +

    手動実行

    -
    - {stats?.batchScheduler?.canForceStop && ( - )}
    -

    +

    スケジューラーの状態に関係なく、バッチ処理を手動で実行できます。実行中の場合は強制停止も可能です。

    -
    +

    バッチ処理について

    -
      +
      • 定期バッチ処理は6時間ごとに実行されます
      • 新しいRSS記事の取得、要約生成、音声合成を行います
      • -
      • スケジューラーが無効の場合、定期実行は停止しますが手動実行は可能です
      • -
      • バッチ処理の実行中は重複実行を防ぐため、新たな実行はスキップされます
      • -
      • 強制停止: 実行中のバッチ処理を緊急停止できます(データ整合性に注意)
      • +
      • + スケジューラーが無効の場合、定期実行は停止しますが手動実行は可能です +
      • +
      • + バッチ処理の実行中は重複実行を防ぐため、新たな実行はスキップされます +
      • +
      • + 強制停止:{" "} + 実行中のバッチ処理を緊急停止できます(データ整合性に注意) +
    )} - {activeTab === 'requests' && ( + {activeTab === "requests" && ( <>

    フィードリクエスト管理

    -

    +

    ユーザーから送信されたフィード追加リクエストを承認・拒否できます。

    -
    -
    - - - -
    {filteredRequests.length === 0 ? ( -

    - {requestFilter === 'all' ? 'フィードリクエストがありません' : `${requestFilter === 'pending' ? '保留中' : requestFilter === 'approved' ? '承認済み' : '拒否済み'}のリクエストがありません`} +

    + {requestFilter === "all" + ? "フィードリクエストがありません" + : `${requestFilter === "pending" ? "保留中" : requestFilter === "approved" ? "承認済み" : "拒否済み"}のリクエストがありません`}

    ) : (
    {filteredRequests.map((request) => ( -
    +
    -

    {request.url}

    +

    + {request.url} +

    {request.requestMessage && ( -

    +

    メッセージ: {request.requestMessage}

    )} -
    - 申請者: {request.requestedBy || '匿名'} - | - 申請日: {new Date(request.createdAt).toLocaleString('ja-JP')} +
    + 申請者: {request.requestedBy || "匿名"} + | + + 申請日:{" "} + {new Date(request.createdAt).toLocaleString( + "ja-JP", + )} + {request.reviewedAt && ( <> - | - 審査日: {new Date(request.reviewedAt).toLocaleString('ja-JP')} + | + + 審査日:{" "} + {new Date(request.reviewedAt).toLocaleString( + "ja-JP", + )} + )}
    - - {request.status === 'pending' ? '保留中' : request.status === 'approved' ? '承認済み' : '拒否済み'} + + {request.status === "pending" + ? "保留中" + : request.status === "approved" + ? "承認済み" + : "拒否済み"} {request.adminNotes && ( -
    +
    管理者メモ: {request.adminNotes}
    )}
    - {request.status === 'pending' && ( -
    + {request.status === "pending" && ( +