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

@ -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();
}