Apply formatting

This commit is contained in:
2025-06-08 15:21:58 +09:00
parent b5ff912fcb
commit a728ebb66c
28 changed files with 1809 additions and 1137 deletions

View File

@ -3,7 +3,7 @@ import { serve } from "@hono/node-server";
import { basicAuth } from "hono/basic-auth";
import path from "path";
import { config, validateConfig } from "./services/config.js";
import {
import {
getAllFeedsIncludingInactive,
deleteFeed,
toggleFeedActive,
@ -31,10 +31,13 @@ const app = new Hono();
// Basic Authentication middleware (if credentials are provided)
if (config.admin.username && config.admin.password) {
app.use("*", basicAuth({
username: config.admin.username,
password: config.admin.password,
}));
app.use(
"*",
basicAuth({
username: config.admin.username,
password: config.admin.password,
}),
);
console.log("🔐 Admin panel authentication enabled");
} else {
console.log("⚠️ Admin panel running without authentication");
@ -45,14 +48,16 @@ app.get("/api/admin/env", async (c) => {
try {
const envVars = {
// OpenAI Configuration
OPENAI_API_KEY: import.meta.env["OPENAI_API_KEY"] ? "***SET***" : undefined,
OPENAI_API_KEY: import.meta.env["OPENAI_API_KEY"]
? "***SET***"
: undefined,
OPENAI_API_ENDPOINT: import.meta.env["OPENAI_API_ENDPOINT"],
OPENAI_MODEL_NAME: import.meta.env["OPENAI_MODEL_NAME"],
// VOICEVOX Configuration
VOICEVOX_HOST: import.meta.env["VOICEVOX_HOST"],
VOICEVOX_STYLE_ID: import.meta.env["VOICEVOX_STYLE_ID"],
// Podcast Configuration
PODCAST_TITLE: import.meta.env["PODCAST_TITLE"],
PODCAST_LINK: import.meta.env["PODCAST_LINK"],
@ -62,16 +67,20 @@ app.get("/api/admin/env", async (c) => {
PODCAST_CATEGORIES: import.meta.env["PODCAST_CATEGORIES"],
PODCAST_TTL: import.meta.env["PODCAST_TTL"],
PODCAST_BASE_URL: import.meta.env["PODCAST_BASE_URL"],
// Admin Configuration
ADMIN_PORT: import.meta.env["ADMIN_PORT"],
ADMIN_USERNAME: import.meta.env["ADMIN_USERNAME"] ? "***SET***" : undefined,
ADMIN_PASSWORD: import.meta.env["ADMIN_PASSWORD"] ? "***SET***" : undefined,
ADMIN_USERNAME: import.meta.env["ADMIN_USERNAME"]
? "***SET***"
: undefined,
ADMIN_PASSWORD: import.meta.env["ADMIN_PASSWORD"]
? "***SET***"
: undefined,
// File Configuration
FEED_URLS_FILE: import.meta.env["FEED_URLS_FILE"],
};
return c.json(envVars);
} catch (error) {
console.error("Error fetching environment variables:", error);
@ -93,30 +102,34 @@ app.get("/api/admin/feeds", async (c) => {
app.post("/api/admin/feeds", async (c) => {
try {
const { feedUrl } = await c.req.json<{ feedUrl: string }>();
if (!feedUrl || typeof feedUrl !== "string" || !feedUrl.startsWith('http')) {
if (
!feedUrl ||
typeof feedUrl !== "string" ||
!feedUrl.startsWith("http")
) {
return c.json({ error: "Valid feed URL is required" }, 400);
}
console.log(" Admin adding new feed URL:", feedUrl);
// Check if feed already exists
const existingFeed = await getFeedByUrl(feedUrl);
if (existingFeed) {
return c.json({
result: "EXISTS",
return c.json({
result: "EXISTS",
message: "Feed URL already exists",
feed: existingFeed
feed: existingFeed,
});
}
// Add new feed
await addNewFeedUrl(feedUrl);
return c.json({
result: "CREATED",
return c.json({
result: "CREATED",
message: "Feed URL added successfully",
feedUrl
feedUrl,
});
} catch (error) {
console.error("Error adding feed:", error);
@ -127,20 +140,20 @@ app.post("/api/admin/feeds", async (c) => {
app.delete("/api/admin/feeds/:id", async (c) => {
try {
const feedId = c.req.param("id");
if (!feedId || feedId.trim() === "") {
return c.json({ error: "Feed ID is required" }, 400);
}
console.log("🗑️ Admin deleting feed ID:", feedId);
const deleted = await deleteFeed(feedId);
if (deleted) {
return c.json({
result: "DELETED",
return c.json({
result: "DELETED",
message: "Feed deleted successfully",
feedId
feedId,
});
} else {
return c.json({ error: "Feed not found" }, 404);
@ -155,25 +168,27 @@ app.patch("/api/admin/feeds/:id/toggle", async (c) => {
try {
const feedId = c.req.param("id");
const { active } = await c.req.json<{ active: boolean }>();
if (!feedId || feedId.trim() === "") {
return c.json({ error: "Feed ID is required" }, 400);
}
if (typeof active !== "boolean") {
return c.json({ error: "Active status must be a boolean" }, 400);
}
console.log(`🔄 Admin toggling feed ${feedId} to ${active ? "active" : "inactive"}`);
console.log(
`🔄 Admin toggling feed ${feedId} to ${active ? "active" : "inactive"}`,
);
const updated = await toggleFeedActive(feedId, active);
if (updated) {
return c.json({
result: "UPDATED",
return c.json({
result: "UPDATED",
message: `Feed ${active ? "activated" : "deactivated"} successfully`,
feedId,
active
active,
});
} else {
return c.json({ error: "Feed not found" }, 404);
@ -209,62 +224,85 @@ app.get("/api/admin/episodes/simple", async (c) => {
app.get("/api/admin/db-diagnostic", async (c) => {
try {
const db = new Database(config.paths.dbPath);
// 1. Check episodes table
const episodeCount = db.prepare("SELECT COUNT(*) as count FROM episodes").get() as any;
const episodeCount = db
.prepare("SELECT COUNT(*) as count FROM episodes")
.get() as any;
// 2. Check articles table
const articleCount = db.prepare("SELECT COUNT(*) as count FROM articles").get() as any;
const articleCount = db
.prepare("SELECT COUNT(*) as count FROM articles")
.get() as any;
// 3. Check feeds table
const feedCount = db.prepare("SELECT COUNT(*) as count FROM feeds").get() as any;
const activeFeedCount = db.prepare("SELECT COUNT(*) as count FROM feeds WHERE active = 1").get() as any;
const inactiveFeedCount = db.prepare("SELECT COUNT(*) as count FROM feeds WHERE active = 0 OR active IS NULL").get() as any;
const feedCount = db
.prepare("SELECT COUNT(*) as count FROM feeds")
.get() as any;
const activeFeedCount = db
.prepare("SELECT COUNT(*) as count FROM feeds WHERE active = 1")
.get() as any;
const inactiveFeedCount = db
.prepare(
"SELECT COUNT(*) as count FROM feeds WHERE active = 0 OR active IS NULL",
)
.get() as any;
// 4. Check orphaned episodes
const orphanedEpisodes = db.prepare(`
const orphanedEpisodes = db
.prepare(`
SELECT e.id, e.title, e.article_id
FROM episodes e
LEFT JOIN articles a ON e.article_id = a.id
WHERE a.id IS NULL
`).all() as any[];
`)
.all() as any[];
// 5. Check orphaned articles
const orphanedArticles = db.prepare(`
const orphanedArticles = db
.prepare(`
SELECT a.id, a.title, a.feed_id
FROM articles a
LEFT JOIN feeds f ON a.feed_id = f.id
WHERE f.id IS NULL
`).all() as any[];
`)
.all() as any[];
// 6. Check episodes with articles but feeds are inactive
const episodesInactiveFeeds = db.prepare(`
const episodesInactiveFeeds = db
.prepare(`
SELECT e.id, e.title, f.active, f.title as feed_title
FROM episodes e
JOIN articles a ON e.article_id = a.id
JOIN feeds f ON a.feed_id = f.id
WHERE f.active = 0 OR f.active IS NULL
`).all() as any[];
`)
.all() as any[];
// 7. Test the JOIN query
const joinResult = db.prepare(`
const joinResult = db
.prepare(`
SELECT COUNT(*) as count
FROM episodes e
JOIN articles a ON e.article_id = a.id
JOIN feeds f ON a.feed_id = f.id
WHERE f.active = 1
`).get() as any;
`)
.get() as any;
// 8. Sample feed details
const sampleFeeds = db.prepare(`
const sampleFeeds = db
.prepare(`
SELECT id, title, url, active, created_at
FROM feeds
ORDER BY created_at DESC
LIMIT 5
`).all() as any[];
`)
.all() as any[];
// 9. Sample episode-article-feed chain
const sampleChain = db.prepare(`
const sampleChain = db
.prepare(`
SELECT
e.id as episode_id, e.title as episode_title,
a.id as article_id, a.title as article_title,
@ -274,10 +312,11 @@ app.get("/api/admin/db-diagnostic", async (c) => {
LEFT JOIN feeds f ON a.feed_id = f.id
ORDER BY e.created_at DESC
LIMIT 5
`).all() as any[];
`)
.all() as any[];
db.close();
const diagnosticResult = {
counts: {
episodes: episodeCount.count,
@ -305,11 +344,14 @@ app.get("/api/admin/db-diagnostic", async (c) => {
},
timestamp: new Date().toISOString(),
};
return c.json(diagnosticResult);
} catch (error) {
console.error("Error running database diagnostic:", error);
return c.json({ error: "Failed to run database diagnostic", details: error.message }, 500);
return c.json(
{ error: "Failed to run database diagnostic", details: error.message },
500,
);
}
});
@ -330,37 +372,37 @@ app.patch("/api/admin/feed-requests/:id/approve", async (c) => {
const requestId = c.req.param("id");
const body = await c.req.json();
const { adminNotes } = body;
// First get the request to get the URL
const requests = await getFeedRequests();
const request = requests.find(r => r.id === requestId);
const request = requests.find((r) => r.id === requestId);
if (!request) {
return c.json({ error: "Feed request not found" }, 404);
}
if (request.status !== 'pending') {
if (request.status !== "pending") {
return c.json({ error: "Feed request already processed" }, 400);
}
// Add the feed
await addNewFeedUrl(request.url);
// Update request status
const updated = await updateFeedRequestStatus(
requestId,
'approved',
'admin',
adminNotes
requestId,
"approved",
"admin",
adminNotes,
);
if (!updated) {
return c.json({ error: "Failed to update request status" }, 500);
}
return c.json({
success: true,
message: "Feed request approved and feed added successfully"
return c.json({
success: true,
message: "Feed request approved and feed added successfully",
});
} catch (error) {
console.error("Error approving feed request:", error);
@ -373,21 +415,21 @@ app.patch("/api/admin/feed-requests/:id/reject", async (c) => {
const requestId = c.req.param("id");
const body = await c.req.json();
const { adminNotes } = body;
const updated = await updateFeedRequestStatus(
requestId,
'rejected',
'admin',
adminNotes
requestId,
"rejected",
"admin",
adminNotes,
);
if (!updated) {
return c.json({ error: "Feed request not found" }, 404);
}
return c.json({
success: true,
message: "Feed request rejected successfully"
return c.json({
success: true,
message: "Feed request rejected successfully",
});
} catch (error) {
console.error("Error rejecting feed request:", error);
@ -409,10 +451,10 @@ app.get("/api/admin/batch/status", async (c) => {
app.post("/api/admin/batch/enable", async (c) => {
try {
batchScheduler.enable();
return c.json({
success: true,
return c.json({
success: true,
message: "Batch scheduler enabled successfully",
status: batchScheduler.getStatus()
status: batchScheduler.getStatus(),
});
} catch (error) {
console.error("Error enabling batch scheduler:", error);
@ -423,10 +465,10 @@ app.post("/api/admin/batch/enable", async (c) => {
app.post("/api/admin/batch/disable", async (c) => {
try {
batchScheduler.disable();
return c.json({
success: true,
return c.json({
success: true,
message: "Batch scheduler disabled successfully",
status: batchScheduler.getStatus()
status: batchScheduler.getStatus(),
});
} catch (error) {
console.error("Error disabling batch scheduler:", error);
@ -441,13 +483,14 @@ app.get("/api/admin/stats", async (c) => {
const episodes = await fetchAllEpisodes();
const feedRequests = await getFeedRequests();
const batchStatus = batchScheduler.getStatus();
const stats = {
totalFeeds: feeds.length,
activeFeeds: feeds.filter(f => f.active).length,
inactiveFeeds: feeds.filter(f => !f.active).length,
activeFeeds: feeds.filter((f) => f.active).length,
inactiveFeeds: feeds.filter((f) => !f.active).length,
totalEpisodes: episodes.length,
pendingRequests: feedRequests.filter(r => r.status === 'pending').length,
pendingRequests: feedRequests.filter((r) => r.status === "pending")
.length,
totalRequests: feedRequests.length,
batchScheduler: {
enabled: batchStatus.enabled,
@ -459,7 +502,7 @@ app.get("/api/admin/stats", async (c) => {
adminPort: config.admin.port,
authEnabled: !!(config.admin.username && config.admin.password),
};
return c.json(stats);
} catch (error) {
console.error("Error fetching admin stats:", error);
@ -470,16 +513,16 @@ app.get("/api/admin/stats", async (c) => {
app.post("/api/admin/batch/trigger", async (c) => {
try {
console.log("🚀 Manual batch process triggered via admin panel");
// Use the batch scheduler's manual trigger method
batchScheduler.triggerManualRun().catch(error => {
batchScheduler.triggerManualRun().catch((error) => {
console.error("❌ Manual admin batch process failed:", error);
});
return c.json({
return c.json({
result: "TRIGGERED",
message: "Batch process started in background",
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error("Error triggering admin batch process:", error);
@ -490,21 +533,24 @@ app.post("/api/admin/batch/trigger", async (c) => {
app.post("/api/admin/batch/force-stop", async (c) => {
try {
console.log("🛑 Force stop batch process requested via admin panel");
const stopped = batchScheduler.forceStop();
if (stopped) {
return c.json({
return c.json({
result: "STOPPED",
message: "Batch process force stop signal sent",
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
} else {
return c.json({
result: "NO_PROCESS",
message: "No batch process is currently running",
timestamp: new Date().toISOString()
}, 200);
return c.json(
{
result: "NO_PROCESS",
message: "No batch process is currently running",
timestamp: new Date().toISOString(),
},
200,
);
}
} catch (error) {
console.error("Error force stopping batch process:", error);
@ -517,7 +563,7 @@ app.get("/assets/*", async (c) => {
try {
const filePath = path.join(config.paths.adminBuildDir, c.req.path);
const file = Bun.file(filePath);
if (await file.exists()) {
const contentType = filePath.endsWith(".js")
? "application/javascript"
@ -539,12 +585,12 @@ async function serveAdminIndex(c: any) {
try {
const indexPath = path.join(config.paths.adminBuildDir, "index.html");
const file = Bun.file(indexPath);
if (await file.exists()) {
const blob = await file.arrayBuffer();
return c.body(blob, 200, { "Content-Type": "text/html; charset=utf-8" });
}
// Fallback to simple HTML if admin panel is not built
return c.html(`
<!DOCTYPE html>
@ -606,7 +652,9 @@ serve(
},
(info) => {
console.log(`🔧 Admin panel running on http://localhost:${info.port}`);
console.log(`📊 Admin authentication: ${config.admin.username && config.admin.password ? "enabled" : "disabled"}`);
console.log(
`📊 Admin authentication: ${config.admin.username && config.admin.password ? "enabled" : "disabled"}`,
);
console.log(`🗄️ Database: ${config.paths.dbPath}`);
},
);
);