feat: Complete AI Chat Agent, Admin Settings (MCP/AI), and Translations
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-12-15 21:52:36 +01:00
parent 71d5c825c5
commit 9736864425
24 changed files with 1679 additions and 401 deletions
+167 -25
View File
@@ -5,11 +5,13 @@ import { insertLabelSchema, insertTaskSchema, insertNoteSchema, insertXpEventSch
import { z } from "zod";
import { EmailService } from "./email.js";
import { mcpServer } from "./mcp";
import { AiService } from "./ai.js";
import { AiService, DEFAULT_SYSTEM_PROMPT } from "./ai.js";
import { RecurrenceService } from "./services/recurrence.js";
import { GamificationService } from "./gamification.js";
const emailService = new EmailService(storage);
const aiService = new AiService(storage);
const recurrenceService = new RecurrenceService(storage);
const gamificationService = new GamificationService(storage);
import { setupAuth, hashPassword, comparePassword } from "./auth.js";
@@ -59,13 +61,25 @@ export async function registerRoutes(app: Express): Promise<Server> {
req.login(adminUser, (err) => {
if (err) return res.status(500).json({ error: "Login failed after setup" });
return res.json(adminUser);
req.session.save(() => {
return res.json(adminUser);
});
});
} catch (e) {
res.status(500).json({ error: "Failed to create admin user" });
}
});
// Check if AI is configured (for UI empty states)
app.get("/api/ai/status", async (req, res) => {
if (!req.isAuthenticated()) return res.sendStatus(401);
const key = await storage.getSystemSettings("ai_api_key");
const provider = await storage.getSystemSettings("ai_provider");
// Consider configured if we have a key OR if provider is 'ollama'
const isConfigured = (provider === 'ollama') || (!!key && key.length > 0);
res.json({ configured: isConfigured, provider });
});
// --- Password Reset Routes ---
app.post("/api/auth/forgot-password", async (req, res) => {
try {
@@ -256,16 +270,37 @@ export async function registerRoutes(app: Express): Promise<Server> {
settings[key] = val || ""; // Return empty string if undefined for inputs
}
}
// Inject System Prompt with Default Fallback
const storedPrompt = await storage.getSystemSettings("ai_system_prompt");
settings["ai_system_prompt"] = storedPrompt || DEFAULT_SYSTEM_PROMPT;
res.json(settings);
});
app.post("/api/admin/settings", isAdmin, async (req, res) => {
const updates = req.body;
const currentPrompt = await storage.getSystemSettings("ai_system_prompt");
// Check for System Prompt changes & Log
if (updates.ai_system_prompt && updates.ai_system_prompt !== currentPrompt) {
await storage.createAuditLog({
userId: (req.user as User).id,
action: "UPDATE",
entityType: "SYSTEM_SETTINGS",
entityId: "ai_system_prompt",
details: { message: "AI System Prompt updated" },
source: "ADMIN"
});
}
const keys = [
"registration_enabled",
"smtp_host", "smtp_port", "smtp_user", "smtp_pass", "smtp_from", "smtp_secure",
"ai_provider", "ai_api_key", "ai_model", "ai_base_url",
"ai_provider", "ai_api_key", "ai_model", "ai_base_url", "ai_system_prompt",
"mcp_enabled", "mcp_port"
];
for (const key of keys) {
if (req.body[key] !== undefined) {
await storage.setSystemSettings(key, String(req.body[key]));
@@ -508,28 +543,36 @@ export async function registerRoutes(app: Express): Promise<Server> {
if (conv.userId !== user.id) return res.sendStatus(403);
// Store User Message
await storage.addMessage({
const userMessage = await storage.addMessage({
conversationId,
role: 'user',
content
});
// Fetch history for context
const dbMessages = await storage.getMessages(conversationId);
// Convert to format expected by AiService (role, content)
const history = dbMessages.map(m => ({ role: m.role as 'user' | 'assistant' | 'system', content: m.content }));
// Send immediate response to client (Session Aware: request completes)
res.json(userMessage);
// Build User Context (Tasks etc)
const tasks = await storage.getTasksForUser(user.id);
const activeTasks = tasks.filter(t => t.status !== 'done');
const completedTasks = tasks.filter(t => t.status === 'done');
// Background Processing (Fire & Forget)
(async () => {
try {
// Fetch history for context
const dbMessages = await storage.getMessages(conversationId);
// Convert to format expected by AiService (role, content)
const history = dbMessages.map(m => ({ role: m.role as 'user' | 'assistant' | 'system', content: m.content }));
const context = `
// Build User Context (Tasks etc)
const tasks = await storage.getTasksForUser(user.id);
const activeTasks = tasks.filter(t => t.status !== 'done');
const completedTasks = tasks.filter(t => t.status === 'done');
const currentClientTime = clientTime || new Date().toLocaleString();
const context = `
User Context:
- User ID: ${user.id}
- Username: ${user.username}
- XP: ${user.xp} (Level ${user.level})
- Current Date/Time: ${clientTime || new Date().toLocaleString()}
- Current Date/Time: ${currentClientTime}
Task Summary:
- Total Active Tasks: ${activeTasks.length}
@@ -542,23 +585,54 @@ Recent Active Tasks:
${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
`;
// Call AI Service
const responseContent = await aiService.chat(history, user, context);
// Call AI Service
const responseContent = await aiService.chat(history, user, context);
// Store AI Response
const botMessage = await storage.addMessage({
conversationId,
role: 'assistant',
content: responseContent
});
// Store AI Response
await storage.addMessage({
conversationId,
role: 'assistant',
content: responseContent
});
} catch (bgError) {
console.error("Background AI Processing Error:", bgError);
// Optional: Add a system message saying it failed?
}
})();
res.json(botMessage);
} catch (e: any) {
console.error("AI Route Error:", e);
res.status(500).json({ error: e.message || "Failed to generate AI response" });
}
});
// Analyze completed task for follow-up
app.post("/api/ai/analyze-completion", async (req, res) => {
if (!req.isAuthenticated()) return res.sendStatus(401);
const user = req.user as User;
if (!user.aiEnabled) return res.json({ needed: false }); // Silently fail/return false
try {
const { taskId } = req.body;
const task = await storage.getTask(taskId);
if (!task) return res.status(404).json({ error: "Task not found" });
// Build context
const context = `
User Context:
- User ID: ${user.id}
- Username: ${user.username}
- Current Date/Time: ${new Date().toLocaleString()}
`;
const analysis = await aiService.analyzeTaskCompletion(task, context);
res.json(analysis);
} catch (e: any) {
console.error("Analysis Error:", e);
res.status(500).json({ error: "Failed to analyze task" });
}
});
// Edit message and regenerate (Regenerate Response)
app.put("/api/ai/chat/:messageId", async (req, res) => {
if (!req.isAuthenticated()) return res.sendStatus(401);
@@ -1402,12 +1476,80 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
app.patch("/api/goals/:id", async (req, res) => {
if (!req.isAuthenticated()) return res.sendStatus(401);
try {
const updated = await storage.updateGoal(req.params.id, req.body);
const updated = await storage.updateTask(req.params.id, req.body);
// Check for Recurrence if task is marked done
if (updated && updated.status === 'done' && updated.isRecurring && req.body.status === 'done') {
// Fire and forget, or await? Await to ensure it happens.
console.log(`[Recurrence] Checking recurrence for task ${updated.id}`);
try {
const nextTask = await recurrenceService.handleTaskCompletion(updated);
if (nextTask) {
console.log(`[Recurrence] Created next task: ${nextTask.id} (${nextTask.title})`);
}
} catch (err) {
console.error("[Recurrence] Error creating next task:", err);
}
}
res.json(updated);
} catch (e) {
res.status(500).json({ error: "Failed to update goal" });
res.status(500).json({ error: "Failed to update task" });
}
});
// Export User Data
app.post("/api/user/export", async (req, res) => {
if (!req.isAuthenticated()) return res.sendStatus(401);
try {
const user = req.user as User;
const { includeTasks, includeLabels, includeSettings } = req.body;
const exportData: any = {
exportedAt: new Date(),
user: { username: user.username, email: user.email }
};
if (includeTasks) {
exportData.tasks = await storage.getTasksForUser(user.id);
}
if (includeLabels) {
exportData.labels = await storage.getLabels(user.id);
}
if (includeSettings && user.role === 'admin') {
// Only admins can request system settings dump, though arguably they might mean "User Settings"
// The prompt said "system settings". I will include system settings if they are admin.
// But maybe they meant user preferences? The prompt specifically said "system settings".
const allSettings: Record<string, string> = {};
const keys = [
"registration_enabled", "smtp_host", "smtp_port", "smtp_user", "smtp_from",
"ai_provider", "ai_model", "ai_base_url", "ai_system_prompt", "mcp_enabled"
];
for (const key of keys) {
const val = await storage.getSystemSettings(key);
if (val) allSettings[key] = val;
}
exportData.systemSettings = allSettings;
}
await storage.createAuditLog({
userId: user.id,
action: "EXPORT_DATA",
entityType: "USER_DATA",
entityId: user.id.toString(),
source: "USER",
details: { includeTasks, includeLabels, includeSettings }
});
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Disposition', `attachment; filename="taskflow_export_${user.username}.json"`);
res.json(exportData);
} catch (e) {
console.error("Export Failed:", e);
res.status(500).json({ error: "Failed to export data" });
}
}); // End of routes
return httpServer;
}