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>
|
||||
|
@ -14,12 +14,16 @@ import {
|
||||
fetchEpisodesWithArticles,
|
||||
getAllCategories,
|
||||
getAllFeedsIncludingInactive,
|
||||
getAllSettings,
|
||||
getFeedByUrl,
|
||||
getFeedRequests,
|
||||
getFeedsByCategory,
|
||||
getFeedsGroupedByCategory,
|
||||
getSetting,
|
||||
getSettingsForAdminUI,
|
||||
toggleFeedActive,
|
||||
updateFeedRequestStatus,
|
||||
updateSetting,
|
||||
} from "./services/database.js";
|
||||
|
||||
// Validate configuration on startup
|
||||
@ -92,6 +96,74 @@ app.get("/api/admin/env", async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Settings management API endpoints
|
||||
app.get("/api/admin/settings", async (c) => {
|
||||
try {
|
||||
const settings = await getSettingsForAdminUI();
|
||||
return c.json(settings);
|
||||
} catch (error) {
|
||||
console.error("Error fetching settings:", error);
|
||||
return c.json({ error: "Failed to fetch settings" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/admin/settings/:key", async (c) => {
|
||||
try {
|
||||
const key = c.req.param("key");
|
||||
const setting = await getSetting(key);
|
||||
|
||||
if (!setting) {
|
||||
return c.json({ error: "Setting not found" }, 404);
|
||||
}
|
||||
|
||||
// Mask credential values for security
|
||||
if (setting.isCredential && setting.value) {
|
||||
setting.value = "••••••••";
|
||||
}
|
||||
|
||||
return c.json(setting);
|
||||
} catch (error) {
|
||||
console.error("Error fetching setting:", error);
|
||||
return c.json({ error: "Failed to fetch setting" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/admin/settings/:key", async (c) => {
|
||||
try {
|
||||
const key = c.req.param("key");
|
||||
const { value } = await c.req.json<{ value: string }>();
|
||||
|
||||
if (!value && value !== "") {
|
||||
return c.json({ error: "Setting value is required" }, 400);
|
||||
}
|
||||
|
||||
console.log(`🔧 Admin updating setting: ${key}`);
|
||||
|
||||
// Check if setting exists
|
||||
const existingSetting = await getSetting(key);
|
||||
if (!existingSetting) {
|
||||
return c.json({ error: "Setting not found" }, 404);
|
||||
}
|
||||
|
||||
const updated = await updateSetting(key, value);
|
||||
|
||||
if (updated) {
|
||||
return c.json({
|
||||
result: "UPDATED",
|
||||
message: "Setting updated successfully",
|
||||
key,
|
||||
// Don't return the value for credentials
|
||||
value: existingSetting.isCredential ? "••••••••" : value,
|
||||
});
|
||||
} else {
|
||||
return c.json({ error: "Failed to update setting" }, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating setting:", error);
|
||||
return c.json({ error: "Failed to update setting" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Feed management API endpoints
|
||||
app.get("/api/admin/feeds", async (c) => {
|
||||
try {
|
||||
|
12
schema.sql
12
schema.sql
@ -68,6 +68,17 @@ CREATE TABLE IF NOT EXISTS feed_requests (
|
||||
admin_notes TEXT
|
||||
);
|
||||
|
||||
-- Environment variable settings stored in database
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
is_credential BOOLEAN DEFAULT 0,
|
||||
description TEXT,
|
||||
default_value TEXT,
|
||||
required BOOLEAN DEFAULT 0,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_feed_id ON articles(feed_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_pub_date ON articles(pub_date);
|
||||
@ -78,3 +89,4 @@ CREATE INDEX IF NOT EXISTS idx_tts_queue_status ON tts_queue(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tts_queue_created_at ON tts_queue(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_feed_requests_status ON feed_requests(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_feed_requests_created_at ON feed_requests(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_settings_is_credential ON settings(is_credential);
|
||||
|
@ -126,86 +126,21 @@ function initializeDatabase(): Database {
|
||||
db.exec("PRAGMA cache_size = 1000;");
|
||||
db.exec("PRAGMA temp_store = memory;");
|
||||
|
||||
// Ensure schema is set up - use the complete schema
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS feeds (
|
||||
id TEXT PRIMARY KEY,
|
||||
url TEXT NOT NULL UNIQUE,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
category TEXT,
|
||||
last_updated TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
active BOOLEAN DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS articles (
|
||||
id TEXT PRIMARY KEY,
|
||||
feed_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
link TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
content TEXT,
|
||||
pub_date TEXT NOT NULL,
|
||||
discovered_at TEXT NOT NULL,
|
||||
processed BOOLEAN DEFAULT 0,
|
||||
FOREIGN KEY(feed_id) REFERENCES feeds(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS episodes (
|
||||
id TEXT PRIMARY KEY,
|
||||
article_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
audio_path TEXT NOT NULL,
|
||||
duration INTEGER,
|
||||
file_size INTEGER,
|
||||
category TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(article_id) REFERENCES articles(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS processed_feed_items (
|
||||
feed_url TEXT NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
processed_at TEXT NOT NULL,
|
||||
PRIMARY KEY(feed_url, item_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tts_queue (
|
||||
id TEXT PRIMARY KEY,
|
||||
item_id TEXT NOT NULL,
|
||||
script_text TEXT NOT NULL,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
last_attempted_at TEXT,
|
||||
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'failed'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS feed_requests (
|
||||
id TEXT PRIMARY KEY,
|
||||
url TEXT NOT NULL,
|
||||
requested_by TEXT,
|
||||
request_message TEXT,
|
||||
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
|
||||
created_at TEXT NOT NULL,
|
||||
reviewed_at TEXT,
|
||||
reviewed_by TEXT,
|
||||
admin_notes TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_feed_id ON articles(feed_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_pub_date ON articles(pub_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_processed ON articles(processed);
|
||||
CREATE INDEX IF NOT EXISTS idx_episodes_article_id ON episodes(article_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_feeds_active ON feeds(active);
|
||||
CREATE INDEX IF NOT EXISTS idx_tts_queue_status ON tts_queue(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tts_queue_created_at ON tts_queue(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_feed_requests_status ON feed_requests(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_feed_requests_created_at ON feed_requests(created_at);`);
|
||||
// Load and execute schema from file
|
||||
const schemaPath = path.join(config.paths.projectRoot, "schema.sql");
|
||||
if (fs.existsSync(schemaPath)) {
|
||||
const schema = fs.readFileSync(schemaPath, "utf-8");
|
||||
db.exec(schema);
|
||||
} else {
|
||||
throw new Error(`Schema file not found: ${schemaPath}`);
|
||||
}
|
||||
|
||||
// Perform database integrity checks and fixes
|
||||
performDatabaseIntegrityFixes(db);
|
||||
|
||||
// Initialize settings table with default values
|
||||
initializeSettings();
|
||||
|
||||
// ALTER
|
||||
// ALTER TABLE feeds ADD COLUMN category TEXT DEFAULT NULL;
|
||||
// Ensure the category column exists in feeds
|
||||
@ -1666,6 +1601,267 @@ export async function getEpisodeCategoryMigrationStatus(): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
// Settings management functions
|
||||
export interface Setting {
|
||||
key: string;
|
||||
value: string | null;
|
||||
isCredential: boolean;
|
||||
description: string;
|
||||
defaultValue: string;
|
||||
required: boolean;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export async function initializeSettings(): Promise<void> {
|
||||
const defaultSettings: Omit<Setting, "updatedAt">[] = [
|
||||
{
|
||||
key: "OPENAI_API_KEY",
|
||||
value: null,
|
||||
isCredential: true,
|
||||
description: "OpenAI API Key for content generation",
|
||||
defaultValue: "",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "OPENAI_API_ENDPOINT",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "OpenAI API Endpoint URL",
|
||||
defaultValue: "https://api.openai.com/v1",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "OPENAI_MODEL_NAME",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "OpenAI Model Name",
|
||||
defaultValue: "gpt-4o-mini",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "VOICEVOX_HOST",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "VOICEVOX Server Host URL",
|
||||
defaultValue: "http://localhost:50021",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "VOICEVOX_STYLE_ID",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "VOICEVOX Voice Style ID",
|
||||
defaultValue: "0",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "PODCAST_TITLE",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "Podcast Title",
|
||||
defaultValue: "自動生成ポッドキャスト",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "PODCAST_LINK",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "Podcast Link URL",
|
||||
defaultValue: "https://your-domain.com/podcast",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "PODCAST_DESCRIPTION",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "Podcast Description",
|
||||
defaultValue: "RSSフィードから自動生成された音声ポッドキャスト",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "PODCAST_LANGUAGE",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "Podcast Language",
|
||||
defaultValue: "ja",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "PODCAST_AUTHOR",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "Podcast Author",
|
||||
defaultValue: "管理者",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "PODCAST_CATEGORIES",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "Podcast Categories",
|
||||
defaultValue: "Technology",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "PODCAST_TTL",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "Podcast TTL",
|
||||
defaultValue: "60",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "PODCAST_BASE_URL",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "Podcast Base URL",
|
||||
defaultValue: "https://your-domain.com",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "ADMIN_PORT",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "Admin Panel Port",
|
||||
defaultValue: "3001",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "ADMIN_USERNAME",
|
||||
value: null,
|
||||
isCredential: true,
|
||||
description: "Admin Panel Username",
|
||||
defaultValue: "",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "ADMIN_PASSWORD",
|
||||
value: null,
|
||||
isCredential: true,
|
||||
description: "Admin Panel Password",
|
||||
defaultValue: "",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "DISABLE_INITIAL_BATCH",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "Disable Initial Batch Process",
|
||||
defaultValue: "false",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "FEED_URLS_FILE",
|
||||
value: null,
|
||||
isCredential: false,
|
||||
description: "Feed URLs File Path",
|
||||
defaultValue: "feed_urls.txt",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const setting of defaultSettings) {
|
||||
try {
|
||||
const stmt = db.prepare(
|
||||
"INSERT OR IGNORE INTO settings (key, value, is_credential, description, default_value, required, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
stmt.run(
|
||||
setting.key,
|
||||
setting.value,
|
||||
setting.isCredential ? 1 : 0,
|
||||
setting.description,
|
||||
setting.defaultValue,
|
||||
setting.required ? 1 : 0,
|
||||
now
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Error initializing setting ${setting.key}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllSettings(): Promise<Setting[]> {
|
||||
try {
|
||||
const stmt = db.prepare("SELECT * FROM settings ORDER BY key");
|
||||
const rows = stmt.all() as any[];
|
||||
|
||||
return rows.map((row) => ({
|
||||
key: row.key,
|
||||
value: row.value,
|
||||
isCredential: Boolean(row.is_credential),
|
||||
description: row.description,
|
||||
defaultValue: row.default_value,
|
||||
required: Boolean(row.required),
|
||||
updatedAt: row.updated_at,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error getting all settings:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSetting(key: string): Promise<Setting | null> {
|
||||
try {
|
||||
const stmt = db.prepare("SELECT * FROM settings WHERE key = ?");
|
||||
const row = stmt.get(key) as any;
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
key: row.key,
|
||||
value: row.value,
|
||||
isCredential: Boolean(row.is_credential),
|
||||
description: row.description,
|
||||
defaultValue: row.default_value,
|
||||
required: Boolean(row.required),
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error getting setting ${key}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSetting(key: string, value: string): Promise<boolean> {
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
const stmt = db.prepare(
|
||||
"UPDATE settings SET value = ?, updated_at = ? WHERE key = ?"
|
||||
);
|
||||
const result = stmt.run(value, now, key);
|
||||
return result.changes > 0;
|
||||
} catch (error) {
|
||||
console.error(`Error updating setting ${key}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSetting(key: string): Promise<boolean> {
|
||||
try {
|
||||
const stmt = db.prepare("DELETE FROM settings WHERE key = ?");
|
||||
const result = stmt.run(key);
|
||||
return result.changes > 0;
|
||||
} catch (error) {
|
||||
console.error(`Error deleting setting ${key}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSettingsForAdminUI(): Promise<Setting[]> {
|
||||
try {
|
||||
const settings = await getAllSettings();
|
||||
return settings.map((setting) => ({
|
||||
...setting,
|
||||
// Mask credential values for security
|
||||
value: setting.isCredential && setting.value ? "••••••••" : setting.value,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error getting settings for admin UI:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function closeDatabase(): void {
|
||||
db.close();
|
||||
}
|
||||
|
Reference in New Issue
Block a user