Apply formatting
This commit is contained in:
@ -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<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(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 <div className="container"><div className="loading">読み込み中...</div></div>;
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">読み込み中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>管理者パネル</h1>
|
||||
<p className="subtitle">RSS Podcast Manager - 管理者用インターフェース</p>
|
||||
<p className="subtitle">
|
||||
RSS Podcast Manager - 管理者用インターフェース
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <div className="error">{error}</div>}
|
||||
@ -311,34 +334,34 @@ function App() {
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<nav style={{ display: 'flex', gap: '16px' }}>
|
||||
<button
|
||||
className={`btn ${activeTab === 'dashboard' ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setActiveTab('dashboard')}
|
||||
<nav style={{ display: "flex", gap: "16px" }}>
|
||||
<button
|
||||
className={`btn ${activeTab === "dashboard" ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => setActiveTab("dashboard")}
|
||||
>
|
||||
ダッシュボード
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${activeTab === 'feeds' ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setActiveTab('feeds')}
|
||||
<button
|
||||
className={`btn ${activeTab === "feeds" ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => setActiveTab("feeds")}
|
||||
>
|
||||
フィード管理
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${activeTab === 'batch' ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setActiveTab('batch')}
|
||||
<button
|
||||
className={`btn ${activeTab === "batch" ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => setActiveTab("batch")}
|
||||
>
|
||||
バッチ管理
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${activeTab === 'requests' ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setActiveTab('requests')}
|
||||
<button
|
||||
className={`btn ${activeTab === "requests" ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => setActiveTab("requests")}
|
||||
>
|
||||
フィード承認
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${activeTab === 'env' ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setActiveTab('env')}
|
||||
<button
|
||||
className={`btn ${activeTab === "env" ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => setActiveTab("env")}
|
||||
>
|
||||
環境変数
|
||||
</button>
|
||||
@ -346,7 +369,7 @@ function App() {
|
||||
</div>
|
||||
|
||||
<div className="card-content">
|
||||
{activeTab === 'dashboard' && (
|
||||
{activeTab === "dashboard" && (
|
||||
<>
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
@ -370,44 +393,62 @@ function App() {
|
||||
<div className="label">保留中リクエスト</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value" style={{ color: stats?.batchScheduler?.enabled ? '#28a745' : '#dc3545' }}>
|
||||
{stats?.batchScheduler?.enabled ? 'ON' : 'OFF'}
|
||||
<div
|
||||
className="value"
|
||||
style={{
|
||||
color: stats?.batchScheduler?.enabled
|
||||
? "#28a745"
|
||||
: "#dc3545",
|
||||
}}
|
||||
>
|
||||
{stats?.batchScheduler?.enabled ? "ON" : "OFF"}
|
||||
</div>
|
||||
<div className="label">バッチスケジューラー</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
<div style={{ marginBottom: "20px" }}>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={triggerBatch}
|
||||
disabled={stats?.batchScheduler?.isRunning}
|
||||
>
|
||||
{stats?.batchScheduler?.isRunning ? 'バッチ処理実行中...' : 'バッチ処理を手動実行'}
|
||||
{stats?.batchScheduler?.isRunning
|
||||
? "バッチ処理実行中..."
|
||||
: "バッチ処理を手動実行"}
|
||||
</button>
|
||||
{stats?.batchScheduler?.canForceStop && (
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={forceStopBatch}
|
||||
style={{ marginLeft: '8px' }}
|
||||
style={{ marginLeft: "8px" }}
|
||||
>
|
||||
強制停止
|
||||
</button>
|
||||
)}
|
||||
<button className="btn btn-primary" onClick={loadData} style={{ marginLeft: '8px' }}>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={loadData}
|
||||
style={{ marginLeft: "8px" }}
|
||||
>
|
||||
データを再読み込み
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
|
||||
<div style={{ fontSize: "14px", color: "#7f8c8d" }}>
|
||||
<p>管理者パネルポート: {stats?.adminPort}</p>
|
||||
<p>認証: {stats?.authEnabled ? '有効' : '無効'}</p>
|
||||
<p>最終更新: {stats?.lastUpdated ? new Date(stats.lastUpdated).toLocaleString('ja-JP') : '不明'}</p>
|
||||
<p>認証: {stats?.authEnabled ? "有効" : "無効"}</p>
|
||||
<p>
|
||||
最終更新:{" "}
|
||||
{stats?.lastUpdated
|
||||
? new Date(stats.lastUpdated).toLocaleString("ja-JP")
|
||||
: "不明"}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'feeds' && (
|
||||
{activeTab === "feeds" && (
|
||||
<>
|
||||
<form onSubmit={addFeed} className="add-feed-form">
|
||||
<div className="form-group">
|
||||
@ -427,10 +468,16 @@ function App() {
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div style={{ marginTop: '24px' }}>
|
||||
<div style={{ marginTop: "24px" }}>
|
||||
<h3>フィード一覧 ({feeds.length}件)</h3>
|
||||
{feeds.length === 0 ? (
|
||||
<p style={{ color: '#7f8c8d', textAlign: 'center', padding: '20px' }}>
|
||||
<p
|
||||
style={{
|
||||
color: "#7f8c8d",
|
||||
textAlign: "center",
|
||||
padding: "20px",
|
||||
}}
|
||||
>
|
||||
フィードが登録されていません
|
||||
</p>
|
||||
) : (
|
||||
@ -438,18 +485,20 @@ function App() {
|
||||
{feeds.map((feed) => (
|
||||
<li key={feed.id} className="feed-item">
|
||||
<div className="feed-info">
|
||||
<h3>{feed.title || 'タイトル未設定'}</h3>
|
||||
<h3>{feed.title || "タイトル未設定"}</h3>
|
||||
<div className="url">{feed.url}</div>
|
||||
<span className={`status ${feed.active ? 'active' : 'inactive'}`}>
|
||||
{feed.active ? 'アクティブ' : '非アクティブ'}
|
||||
<span
|
||||
className={`status ${feed.active ? "active" : "inactive"}`}
|
||||
>
|
||||
{feed.active ? "アクティブ" : "非アクティブ"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="feed-actions">
|
||||
<button
|
||||
className={`btn ${feed.active ? 'btn-warning' : 'btn-success'}`}
|
||||
className={`btn ${feed.active ? "btn-warning" : "btn-success"}`}
|
||||
onClick={() => toggleFeed(feed.id, !feed.active)}
|
||||
>
|
||||
{feed.active ? '無効化' : '有効化'}
|
||||
{feed.active ? "無効化" : "有効化"}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
@ -466,55 +515,75 @@ function App() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'batch' && (
|
||||
{activeTab === "batch" && (
|
||||
<>
|
||||
<h3>バッチ処理管理</h3>
|
||||
<p style={{ marginBottom: '20px', color: '#7f8c8d' }}>
|
||||
<p style={{ marginBottom: "20px", color: "#7f8c8d" }}>
|
||||
定期バッチ処理の状態管理と手動実行を行えます。
|
||||
</p>
|
||||
|
||||
<div className="stats-grid" style={{ marginBottom: '24px' }}>
|
||||
<div className="stats-grid" style={{ marginBottom: "24px" }}>
|
||||
<div className="stat-card">
|
||||
<div className="value" style={{ color: stats?.batchScheduler?.enabled ? '#28a745' : '#dc3545' }}>
|
||||
{stats?.batchScheduler?.enabled ? '有効' : '無効'}
|
||||
<div
|
||||
className="value"
|
||||
style={{
|
||||
color: stats?.batchScheduler?.enabled
|
||||
? "#28a745"
|
||||
: "#dc3545",
|
||||
}}
|
||||
>
|
||||
{stats?.batchScheduler?.enabled ? "有効" : "無効"}
|
||||
</div>
|
||||
<div className="label">スケジューラー状態</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value" style={{ color: stats?.batchScheduler?.isRunning ? '#ffc107' : '#6c757d' }}>
|
||||
{stats?.batchScheduler?.isRunning ? '実行中' : '待機中'}
|
||||
<div
|
||||
className="value"
|
||||
style={{
|
||||
color: stats?.batchScheduler?.isRunning
|
||||
? "#ffc107"
|
||||
: "#6c757d",
|
||||
}}
|
||||
>
|
||||
{stats?.batchScheduler?.isRunning ? "実行中" : "待機中"}
|
||||
</div>
|
||||
<div className="label">実行状態</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value" style={{ fontSize: '12px' }}>
|
||||
{stats?.batchScheduler?.lastRun
|
||||
? new Date(stats.batchScheduler.lastRun).toLocaleString('ja-JP')
|
||||
: '未実行'}
|
||||
<div className="value" style={{ fontSize: "12px" }}>
|
||||
{stats?.batchScheduler?.lastRun
|
||||
? new Date(stats.batchScheduler.lastRun).toLocaleString(
|
||||
"ja-JP",
|
||||
)
|
||||
: "未実行"}
|
||||
</div>
|
||||
<div className="label">前回実行</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value" style={{ fontSize: '12px' }}>
|
||||
{stats?.batchScheduler?.nextRun
|
||||
? new Date(stats.batchScheduler.nextRun).toLocaleString('ja-JP')
|
||||
: '未予定'}
|
||||
<div className="value" style={{ fontSize: "12px" }}>
|
||||
{stats?.batchScheduler?.nextRun
|
||||
? new Date(stats.batchScheduler.nextRun).toLocaleString(
|
||||
"ja-JP",
|
||||
)
|
||||
: "未予定"}
|
||||
</div>
|
||||
<div className="label">次回実行</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{ marginBottom: "24px" }}>
|
||||
<h4>スケジューラー制御</h4>
|
||||
<div style={{ display: 'flex', gap: '12px', marginTop: '12px' }}>
|
||||
<button
|
||||
<div
|
||||
style={{ display: "flex", gap: "12px", marginTop: "12px" }}
|
||||
>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={() => toggleBatchScheduler(true)}
|
||||
disabled={stats?.batchScheduler?.enabled}
|
||||
>
|
||||
スケジューラーを有効化
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
onClick={() => toggleBatchScheduler(false)}
|
||||
disabled={!stats?.batchScheduler?.enabled}
|
||||
@ -522,147 +591,248 @@ function App() {
|
||||
スケジューラーを無効化
|
||||
</button>
|
||||
</div>
|
||||
<p style={{ fontSize: '14px', color: '#6c757d', marginTop: '8px' }}>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
color: "#6c757d",
|
||||
marginTop: "8px",
|
||||
}}
|
||||
>
|
||||
スケジューラーを無効化すると、定期的なバッチ処理が停止します。手動実行は引き続き可能です。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{ marginBottom: "24px" }}>
|
||||
<h4>手動実行</h4>
|
||||
<div style={{ display: 'flex', gap: '12px', marginTop: '12px' }}>
|
||||
<button
|
||||
<div
|
||||
style={{ display: "flex", gap: "12px", marginTop: "12px" }}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={triggerBatch}
|
||||
disabled={stats?.batchScheduler?.isRunning}
|
||||
>
|
||||
{stats?.batchScheduler?.isRunning ? 'バッチ処理実行中...' : 'バッチ処理を手動実行'}
|
||||
{stats?.batchScheduler?.isRunning
|
||||
? "バッチ処理実行中..."
|
||||
: "バッチ処理を手動実行"}
|
||||
</button>
|
||||
{stats?.batchScheduler?.canForceStop && (
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={forceStopBatch}
|
||||
>
|
||||
<button className="btn btn-danger" onClick={forceStopBatch}>
|
||||
強制停止
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p style={{ fontSize: '14px', color: '#6c757d', marginTop: '8px' }}>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
color: "#6c757d",
|
||||
marginTop: "8px",
|
||||
}}
|
||||
>
|
||||
スケジューラーの状態に関係なく、バッチ処理を手動で実行できます。実行中の場合は強制停止も可能です。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '16px', background: '#f8f9fa', borderRadius: '4px' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
background: "#f8f9fa",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<h4>バッチ処理について</h4>
|
||||
<ul style={{ fontSize: '14px', color: '#6c757d', marginTop: '8px', paddingLeft: '20px' }}>
|
||||
<ul
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
color: "#6c757d",
|
||||
marginTop: "8px",
|
||||
paddingLeft: "20px",
|
||||
}}
|
||||
>
|
||||
<li>定期バッチ処理は6時間ごとに実行されます</li>
|
||||
<li>新しいRSS記事の取得、要約生成、音声合成を行います</li>
|
||||
<li>スケジューラーが無効の場合、定期実行は停止しますが手動実行は可能です</li>
|
||||
<li>バッチ処理の実行中は重複実行を防ぐため、新たな実行はスキップされます</li>
|
||||
<li><strong>強制停止:</strong> 実行中のバッチ処理を緊急停止できます(データ整合性に注意)</li>
|
||||
<li>
|
||||
スケジューラーが無効の場合、定期実行は停止しますが手動実行は可能です
|
||||
</li>
|
||||
<li>
|
||||
バッチ処理の実行中は重複実行を防ぐため、新たな実行はスキップされます
|
||||
</li>
|
||||
<li>
|
||||
<strong>強制停止:</strong>{" "}
|
||||
実行中のバッチ処理を緊急停止できます(データ整合性に注意)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'requests' && (
|
||||
{activeTab === "requests" && (
|
||||
<>
|
||||
<h3>フィードリクエスト管理</h3>
|
||||
<p style={{ marginBottom: '20px', color: '#7f8c8d' }}>
|
||||
<p style={{ marginBottom: "20px", color: "#7f8c8d" }}>
|
||||
ユーザーから送信されたフィード追加リクエストを承認・拒否できます。
|
||||
</p>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
className={`btn ${requestFilter === 'all' ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setRequestFilter('all')}
|
||||
<div style={{ marginBottom: "20px" }}>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<button
|
||||
className={`btn ${requestFilter === "all" ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => setRequestFilter("all")}
|
||||
>
|
||||
すべて ({feedRequests.length})
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${requestFilter === 'pending' ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setRequestFilter('pending')}
|
||||
<button
|
||||
className={`btn ${requestFilter === "pending" ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => setRequestFilter("pending")}
|
||||
>
|
||||
保留中 ({feedRequests.filter(r => r.status === 'pending').length})
|
||||
保留中 (
|
||||
{feedRequests.filter((r) => r.status === "pending").length})
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${requestFilter === 'approved' ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setRequestFilter('approved')}
|
||||
<button
|
||||
className={`btn ${requestFilter === "approved" ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => setRequestFilter("approved")}
|
||||
>
|
||||
承認済み ({feedRequests.filter(r => r.status === 'approved').length})
|
||||
承認済み (
|
||||
{feedRequests.filter((r) => r.status === "approved").length}
|
||||
)
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${requestFilter === 'rejected' ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setRequestFilter('rejected')}
|
||||
<button
|
||||
className={`btn ${requestFilter === "rejected" ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => setRequestFilter("rejected")}
|
||||
>
|
||||
拒否済み ({feedRequests.filter(r => r.status === 'rejected').length})
|
||||
拒否済み (
|
||||
{feedRequests.filter((r) => r.status === "rejected").length}
|
||||
)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredRequests.length === 0 ? (
|
||||
<p style={{ color: '#7f8c8d', textAlign: 'center', padding: '20px' }}>
|
||||
{requestFilter === 'all' ? 'フィードリクエストがありません' : `${requestFilter === 'pending' ? '保留中' : requestFilter === 'approved' ? '承認済み' : '拒否済み'}のリクエストがありません`}
|
||||
<p
|
||||
style={{
|
||||
color: "#7f8c8d",
|
||||
textAlign: "center",
|
||||
padding: "20px",
|
||||
}}
|
||||
>
|
||||
{requestFilter === "all"
|
||||
? "フィードリクエストがありません"
|
||||
: `${requestFilter === "pending" ? "保留中" : requestFilter === "approved" ? "承認済み" : "拒否済み"}のリクエストがありません`}
|
||||
</p>
|
||||
) : (
|
||||
<div className="requests-list">
|
||||
{filteredRequests.map((request) => (
|
||||
<div key={request.id} className="feed-item" style={{ marginBottom: '16px' }}>
|
||||
<div
|
||||
key={request.id}
|
||||
className="feed-item"
|
||||
style={{ marginBottom: "16px" }}
|
||||
>
|
||||
<div className="feed-info">
|
||||
<h4 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>{request.url}</h4>
|
||||
<h4 style={{ margin: "0 0 8px 0", fontSize: "16px" }}>
|
||||
{request.url}
|
||||
</h4>
|
||||
{request.requestMessage && (
|
||||
<p style={{ margin: '0 0 8px 0', fontSize: '14px', color: '#666' }}>
|
||||
<p
|
||||
style={{
|
||||
margin: "0 0 8px 0",
|
||||
fontSize: "14px",
|
||||
color: "#666",
|
||||
}}
|
||||
>
|
||||
メッセージ: {request.requestMessage}
|
||||
</p>
|
||||
)}
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
<span>申請者: {request.requestedBy || '匿名'}</span>
|
||||
<span style={{ margin: '0 8px' }}>|</span>
|
||||
<span>申請日: {new Date(request.createdAt).toLocaleString('ja-JP')}</span>
|
||||
<div style={{ fontSize: "12px", color: "#999" }}>
|
||||
<span>申請者: {request.requestedBy || "匿名"}</span>
|
||||
<span style={{ margin: "0 8px" }}>|</span>
|
||||
<span>
|
||||
申請日:{" "}
|
||||
{new Date(request.createdAt).toLocaleString(
|
||||
"ja-JP",
|
||||
)}
|
||||
</span>
|
||||
{request.reviewedAt && (
|
||||
<>
|
||||
<span style={{ margin: '0 8px' }}>|</span>
|
||||
<span>審査日: {new Date(request.reviewedAt).toLocaleString('ja-JP')}</span>
|
||||
<span style={{ margin: "0 8px" }}>|</span>
|
||||
<span>
|
||||
審査日:{" "}
|
||||
{new Date(request.reviewedAt).toLocaleString(
|
||||
"ja-JP",
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<span className={`status ${request.status === 'approved' ? 'active' : request.status === 'rejected' ? 'inactive' : ''}`}>
|
||||
{request.status === 'pending' ? '保留中' : request.status === 'approved' ? '承認済み' : '拒否済み'}
|
||||
<span
|
||||
className={`status ${request.status === "approved" ? "active" : request.status === "rejected" ? "inactive" : ""}`}
|
||||
>
|
||||
{request.status === "pending"
|
||||
? "保留中"
|
||||
: request.status === "approved"
|
||||
? "承認済み"
|
||||
: "拒否済み"}
|
||||
</span>
|
||||
{request.adminNotes && (
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#f8f9fa', borderRadius: '4px', fontSize: '14px' }}>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "8px",
|
||||
padding: "8px",
|
||||
background: "#f8f9fa",
|
||||
borderRadius: "4px",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
<strong>管理者メモ:</strong> {request.adminNotes}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{request.status === 'pending' && (
|
||||
<div className="feed-actions" style={{ flexDirection: 'column', gap: '8px', minWidth: '200px' }}>
|
||||
{request.status === "pending" && (
|
||||
<div
|
||||
className="feed-actions"
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
minWidth: "200px",
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
placeholder="管理者メモ(任意)"
|
||||
value={approvalNotes[request.id] || ''}
|
||||
onChange={(e) => updateApprovalNotes(request.id, e.target.value)}
|
||||
value={approvalNotes[request.id] || ""}
|
||||
onChange={(e) =>
|
||||
updateApprovalNotes(request.id, e.target.value)
|
||||
}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: '60px',
|
||||
padding: '8px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
resize: 'vertical'
|
||||
width: "100%",
|
||||
minHeight: "60px",
|
||||
padding: "8px",
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: "4px",
|
||||
fontSize: "12px",
|
||||
resize: "vertical",
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={() => approveFeedRequest(request.id, approvalNotes[request.id] || '')}
|
||||
style={{ fontSize: '12px', padding: '6px 12px' }}
|
||||
onClick={() =>
|
||||
approveFeedRequest(
|
||||
request.id,
|
||||
approvalNotes[request.id] || "",
|
||||
)
|
||||
}
|
||||
style={{ fontSize: "12px", padding: "6px 12px" }}
|
||||
>
|
||||
承認
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={() => rejectFeedRequest(request.id, approvalNotes[request.id] || '')}
|
||||
style={{ fontSize: '12px', padding: '6px 12px' }}
|
||||
onClick={() =>
|
||||
rejectFeedRequest(
|
||||
request.id,
|
||||
approvalNotes[request.id] || "",
|
||||
)
|
||||
}
|
||||
style={{ fontSize: "12px", padding: "6px 12px" }}
|
||||
>
|
||||
拒否
|
||||
</button>
|
||||
@ -676,27 +846,40 @@ function App() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'env' && (
|
||||
{activeTab === "env" && (
|
||||
<>
|
||||
<h3>環境変数設定</h3>
|
||||
<p style={{ marginBottom: '20px', color: '#7f8c8d' }}>
|
||||
<p style={{ marginBottom: "20px", color: "#7f8c8d" }}>
|
||||
現在の環境変数設定を表示しています。機密情報は***SET***と表示されます。
|
||||
</p>
|
||||
|
||||
|
||||
<ul className="env-list">
|
||||
{Object.entries(envVars).map(([key, value]) => (
|
||||
<li key={key} className="env-item">
|
||||
<div className="env-key">{key}</div>
|
||||
<div className="env-value">
|
||||
{value === undefined ? '未設定' : value}
|
||||
{value === undefined ? "未設定" : value}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div style={{ marginTop: '20px', padding: '16px', background: '#f8f9fa', borderRadius: '4px' }}>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "20px",
|
||||
padding: "16px",
|
||||
background: "#f8f9fa",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<h4>環境変数の設定方法</h4>
|
||||
<p style={{ fontSize: '14px', color: '#6c757d', marginTop: '8px' }}>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
color: "#6c757d",
|
||||
marginTop: "8px",
|
||||
}}
|
||||
>
|
||||
環境変数を変更するには、.envファイルを編集するか、システムの環境変数を設定してください。
|
||||
変更後はサーバーの再起動が必要です。
|
||||
</p>
|
||||
@ -709,4 +892,4 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
@ -5,8 +5,8 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@ -24,7 +24,7 @@ body {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ body {
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -147,7 +147,7 @@ body {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@ -275,4 +275,4 @@ body {
|
||||
.add-feed-form .form-group {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
);
|
||||
|
Reference in New Issue
Block a user