Apply formatting
This commit is contained in:
302
admin-server.ts
302
admin-server.ts
@ -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}`);
|
||||
},
|
||||
);
|
||||
);
|
||||
|
Reference in New Issue
Block a user