Update admin panel and server configuration
This commit is contained in:
@ -64,10 +64,21 @@ interface Episode {
|
||||
feedCategory?: string;
|
||||
}
|
||||
|
||||
interface Setting {
|
||||
key: string;
|
||||
value: string | null;
|
||||
isCredential: boolean;
|
||||
description: string;
|
||||
defaultValue: string;
|
||||
required: boolean;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [feeds, setFeeds] = useState<Feed[]>([]);
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [envVars, setEnvVars] = useState<EnvVars>({});
|
||||
const [settings, setSettings] = useState<Setting[]>([]);
|
||||
const [feedRequests, setFeedRequests] = useState<FeedRequest[]>([]);
|
||||
const [episodes, setEpisodes] = useState<Episode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -80,8 +91,9 @@ function App() {
|
||||
const [approvalNotes, setApprovalNotes] = useState<{ [key: string]: string }>(
|
||||
{},
|
||||
);
|
||||
const [editingSettings, setEditingSettings] = useState<{ [key: string]: string }>({});
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"dashboard" | "feeds" | "episodes" | "env" | "batch" | "requests"
|
||||
"dashboard" | "feeds" | "episodes" | "env" | "settings" | "batch" | "requests"
|
||||
>("dashboard");
|
||||
|
||||
useEffect(() => {
|
||||
@ -91,11 +103,12 @@ function App() {
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [feedsRes, statsRes, envRes, requestsRes, episodesRes] =
|
||||
const [feedsRes, statsRes, envRes, settingsRes, requestsRes, episodesRes] =
|
||||
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"),
|
||||
]);
|
||||
@ -104,17 +117,19 @@ function App() {
|
||||
!feedsRes.ok ||
|
||||
!statsRes.ok ||
|
||||
!envRes.ok ||
|
||||
!settingsRes.ok ||
|
||||
!requestsRes.ok ||
|
||||
!episodesRes.ok
|
||||
) {
|
||||
throw new Error("Failed to load data");
|
||||
}
|
||||
|
||||
const [feedsData, statsData, envData, requestsData, episodesData] =
|
||||
const [feedsData, statsData, envData, settingsData, requestsData, episodesData] =
|
||||
await Promise.all([
|
||||
feedsRes.json(),
|
||||
statsRes.json(),
|
||||
envRes.json(),
|
||||
settingsRes.json(),
|
||||
requestsRes.json(),
|
||||
episodesRes.json(),
|
||||
]);
|
||||
@ -122,6 +137,7 @@ function App() {
|
||||
setFeeds(feedsData);
|
||||
setStats(statsData);
|
||||
setEnvVars(envData);
|
||||
setSettings(settingsData);
|
||||
setFeedRequests(requestsData);
|
||||
setEpisodes(episodesData);
|
||||
setError(null);
|
||||
@ -338,6 +354,43 @@ function App() {
|
||||
setApprovalNotes({ ...approvalNotes, [requestId]: notes });
|
||||
};
|
||||
|
||||
const updateSetting = async (key: string, value: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/settings/${key}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ value }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
setSuccess(data.message);
|
||||
setEditingSettings({ ...editingSettings, [key]: "" });
|
||||
loadData();
|
||||
} else {
|
||||
setError(data.error || "設定更新に失敗しました");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("設定更新に失敗しました");
|
||||
console.error("Error updating setting:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const startEditingSetting = (key: string, currentValue: string | null) => {
|
||||
setEditingSettings({ ...editingSettings, [key]: currentValue || "" });
|
||||
};
|
||||
|
||||
const cancelEditingSetting = (key: string) => {
|
||||
const newEditing = { ...editingSettings };
|
||||
delete newEditing[key];
|
||||
setEditingSettings(newEditing);
|
||||
};
|
||||
|
||||
const updateEditingValue = (key: string, value: string) => {
|
||||
setEditingSettings({ ...editingSettings, [key]: value });
|
||||
};
|
||||
|
||||
const deleteEpisode = async (episodeId: string, episodeTitle: string) => {
|
||||
if (
|
||||
!confirm(
|
||||
@ -424,6 +477,12 @@ function App() {
|
||||
>
|
||||
フィード承認
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${activeTab === "settings" ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => setActiveTab("settings")}
|
||||
>
|
||||
設定管理
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${activeTab === "env" ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => setActiveTab("env")}
|
||||
@ -1087,6 +1146,172 @@ function App() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === "settings" && (
|
||||
<>
|
||||
<h3>設定管理</h3>
|
||||
<p style={{ marginBottom: "20px", color: "#7f8c8d" }}>
|
||||
システム設定を管理できます。設定はデータベースに保存され、アプリケーション全体に適用されます。
|
||||
</p>
|
||||
|
||||
<div style={{ marginBottom: "20px" }}>
|
||||
<div className="stats-grid" style={{ gridTemplateColumns: "repeat(4, 1fr)" }}>
|
||||
<div className="stat-card">
|
||||
<div className="value">{settings.length}</div>
|
||||
<div className="label">総設定数</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value">
|
||||
{settings.filter(s => s.value !== null && s.value !== "").length}
|
||||
</div>
|
||||
<div className="label">設定済み</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value">
|
||||
{settings.filter(s => s.required).length}
|
||||
</div>
|
||||
<div className="label">必須設定</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value">
|
||||
{settings.filter(s => s.isCredential).length}
|
||||
</div>
|
||||
<div className="label">認証情報</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-list">
|
||||
{settings.map((setting) => (
|
||||
<div key={setting.key} className="setting-item" style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "16px",
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: "4px",
|
||||
marginBottom: "12px",
|
||||
backgroundColor: setting.required && (!setting.value || setting.value === "") ? "#fff5f5" : "#fff"
|
||||
}}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "4px" }}>
|
||||
<h4 style={{ margin: 0, fontSize: "16px" }}>{setting.key}</h4>
|
||||
{setting.required && (
|
||||
<span style={{
|
||||
padding: "2px 6px",
|
||||
background: "#dc3545",
|
||||
color: "white",
|
||||
borderRadius: "4px",
|
||||
fontSize: "10px"
|
||||
}}>必須</span>
|
||||
)}
|
||||
{setting.isCredential && (
|
||||
<span style={{
|
||||
padding: "2px 6px",
|
||||
background: "#ffc107",
|
||||
color: "black",
|
||||
borderRadius: "4px",
|
||||
fontSize: "10px"
|
||||
}}>認証情報</span>
|
||||
)}
|
||||
</div>
|
||||
<p style={{ margin: "0 0 8px 0", fontSize: "14px", color: "#666" }}>
|
||||
{setting.description}
|
||||
</p>
|
||||
<div style={{ fontSize: "12px", color: "#999" }}>
|
||||
デフォルト値: {setting.defaultValue || "なし"} |
|
||||
最終更新: {new Date(setting.updatedAt).toLocaleString("ja-JP")}
|
||||
</div>
|
||||
|
||||
{editingSettings[setting.key] !== undefined ? (
|
||||
<div style={{ marginTop: "8px", display: "flex", gap: "8px", alignItems: "center" }}>
|
||||
<input
|
||||
type={setting.isCredential ? "password" : "text"}
|
||||
value={editingSettings[setting.key]}
|
||||
onChange={(e) => updateEditingValue(setting.key, e.target.value)}
|
||||
placeholder={setting.defaultValue || "値を入力..."}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "6px 10px",
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: "4px",
|
||||
fontSize: "14px"
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={() => updateSetting(setting.key, editingSettings[setting.key])}
|
||||
style={{ fontSize: "12px", padding: "6px 12px" }}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => cancelEditingSetting(setting.key)}
|
||||
style={{ fontSize: "12px", padding: "6px 12px" }}
|
||||
>
|
||||
キャンセル
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ marginTop: "8px", display: "flex", 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 === "" ? (
|
||||
<span style={{ color: "#dc3545", fontStyle: "italic" }}>未設定</span>
|
||||
) : setting.isCredential ? (
|
||||
<span style={{ color: "#666" }}>••••••••</span>
|
||||
) : (
|
||||
setting.value
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => startEditingSetting(setting.key, setting.value)}
|
||||
style={{ fontSize: "12px", padding: "6px 12px" }}
|
||||
>
|
||||
編集
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: "20px",
|
||||
padding: "16px",
|
||||
background: "#f8f9fa",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<h4>設定について</h4>
|
||||
<ul
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
color: "#6c757d",
|
||||
marginTop: "8px",
|
||||
paddingLeft: "20px",
|
||||
}}
|
||||
>
|
||||
<li><strong>必須設定:</strong> アプリケーションの動作に必要な設定です</li>
|
||||
<li><strong>認証情報:</strong> セキュリティ上重要な情報で、表示時にマスクされます</li>
|
||||
<li>設定の変更は即座にアプリケーションに反映されます</li>
|
||||
<li>デフォルト値が設定されている項目は、空にするとデフォルト値が使用されます</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === "env" && (
|
||||
<>
|
||||
<h3>環境変数設定</h3>
|
||||
|
Reference in New Issue
Block a user