Update admin panel and server configuration

This commit is contained in:
2025-06-08 21:59:24 +09:00
parent b7f3ca6a27
commit 00ae265314
4 changed files with 584 additions and 79 deletions

View File

@ -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>