685 lines
21 KiB
TypeScript
685 lines
21 KiB
TypeScript
import { Router } from "express";
|
|
import { db } from "../../db";
|
|
import {
|
|
workspacePages, pageBlocks, pageLinks, dashboardWidgets,
|
|
quickNotes, activityFeed, userFavorites, commandHistory,
|
|
insertWorkspacePageSchema, insertPageBlockSchema, insertDashboardWidgetSchema,
|
|
insertQuickNoteSchema, insertActivityFeedSchema, insertUserFavoriteSchema,
|
|
pcClients, pcProjects, pcTasks, conversations, knowledgeBase
|
|
} from "@shared/schema";
|
|
import { eq, desc, and, or, ilike, sql } from "drizzle-orm";
|
|
|
|
const router = Router();
|
|
|
|
const requireAuth = (req: any, res: any, next: any) => {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Not authenticated" });
|
|
}
|
|
next();
|
|
};
|
|
|
|
// ========== UNIVERSAL SEARCH ==========
|
|
router.get("/search", requireAuth, async (req, res) => {
|
|
try {
|
|
const { q, modules, limit = 20 } = req.query;
|
|
const userId = req.user!.id;
|
|
const searchQuery = `%${q}%`;
|
|
const limitNum = Math.min(parseInt(limit as string) || 20, 50);
|
|
|
|
const results: any = { pages: [], clients: [], projects: [], tasks: [], conversations: [], knowledge: [] };
|
|
|
|
const moduleFilter = modules ? (modules as string).split(',') : ['all'];
|
|
const searchAll = moduleFilter.includes('all');
|
|
|
|
if (searchAll || moduleFilter.includes('pages')) {
|
|
const pages = await db.select()
|
|
.from(workspacePages)
|
|
.where(and(
|
|
eq(workspacePages.userId, userId),
|
|
eq(workspacePages.isArchived, 0),
|
|
ilike(workspacePages.title, searchQuery)
|
|
))
|
|
.limit(limitNum);
|
|
results.pages = pages.map(p => ({ ...p, _type: 'page', _module: 'workspace' }));
|
|
}
|
|
|
|
if (searchAll || moduleFilter.includes('clients')) {
|
|
const clients = await db.select()
|
|
.from(pcClients)
|
|
.where(or(
|
|
ilike(pcClients.name, searchQuery),
|
|
ilike(pcClients.email, searchQuery),
|
|
ilike(pcClients.phone, searchQuery)
|
|
))
|
|
.limit(limitNum);
|
|
results.clients = clients.map(c => ({ ...c, _type: 'client', _module: 'compass' }));
|
|
}
|
|
|
|
if (searchAll || moduleFilter.includes('projects')) {
|
|
const projects = await db.select()
|
|
.from(pcProjects)
|
|
.where(or(
|
|
ilike(pcProjects.name, searchQuery),
|
|
ilike(pcProjects.description, searchQuery)
|
|
))
|
|
.limit(limitNum);
|
|
results.projects = projects.map(p => ({ ...p, _type: 'project', _module: 'compass' }));
|
|
}
|
|
|
|
if (searchAll || moduleFilter.includes('tasks')) {
|
|
const tasks = await db.select()
|
|
.from(pcTasks)
|
|
.where(or(
|
|
ilike(pcTasks.title, searchQuery),
|
|
ilike(pcTasks.description, searchQuery)
|
|
))
|
|
.limit(limitNum);
|
|
results.tasks = tasks.map(t => ({ ...t, _type: 'task', _module: 'compass' }));
|
|
}
|
|
|
|
if (searchAll || moduleFilter.includes('conversations')) {
|
|
const convs = await db.select()
|
|
.from(conversations)
|
|
.where(and(
|
|
eq(conversations.userId, userId),
|
|
ilike(conversations.title, searchQuery)
|
|
))
|
|
.limit(limitNum);
|
|
results.conversations = convs.map(c => ({ ...c, _type: 'conversation', _module: 'agent' }));
|
|
}
|
|
|
|
if (searchAll || moduleFilter.includes('knowledge')) {
|
|
const knowledge = await db.select()
|
|
.from(knowledgeBase)
|
|
.where(or(
|
|
ilike(knowledgeBase.title, searchQuery),
|
|
ilike(knowledgeBase.content, searchQuery)
|
|
))
|
|
.limit(limitNum);
|
|
results.knowledge = knowledge.map(k => ({ ...k, _type: 'knowledge', _module: 'agent' }));
|
|
}
|
|
|
|
const allResults = [
|
|
...results.pages,
|
|
...results.clients,
|
|
...results.projects,
|
|
...results.tasks,
|
|
...results.conversations,
|
|
...results.knowledge
|
|
];
|
|
|
|
res.json({ results: allResults, grouped: results });
|
|
} catch (error) {
|
|
console.error("Search error:", error);
|
|
res.status(500).json({ error: "Search failed" });
|
|
}
|
|
});
|
|
|
|
// ========== WORKSPACE PAGES ==========
|
|
router.get("/pages", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const { parentId, archived } = req.query;
|
|
|
|
let whereClause = and(
|
|
eq(workspacePages.userId, userId),
|
|
eq(workspacePages.isArchived, archived === 'true' ? 1 : 0)
|
|
);
|
|
|
|
if (parentId) {
|
|
whereClause = and(whereClause, eq(workspacePages.parentId, parseInt(parentId as string)));
|
|
} else {
|
|
whereClause = and(whereClause, sql`${workspacePages.parentId} IS NULL`);
|
|
}
|
|
|
|
const pages = await db.select()
|
|
.from(workspacePages)
|
|
.where(whereClause)
|
|
.orderBy(workspacePages.orderIndex);
|
|
|
|
res.json(pages);
|
|
} catch (error) {
|
|
console.error("Error fetching pages:", error);
|
|
res.status(500).json({ error: "Failed to fetch pages" });
|
|
}
|
|
});
|
|
|
|
router.get("/pages/favorites", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const pages = await db.select()
|
|
.from(workspacePages)
|
|
.where(and(
|
|
eq(workspacePages.userId, userId),
|
|
eq(workspacePages.isFavorite, 1),
|
|
eq(workspacePages.isArchived, 0)
|
|
))
|
|
.orderBy(workspacePages.title);
|
|
res.json(pages);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to fetch favorite pages" });
|
|
}
|
|
});
|
|
|
|
router.get("/pages/:id", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const [page] = await db.select()
|
|
.from(workspacePages)
|
|
.where(and(
|
|
eq(workspacePages.id, parseInt(req.params.id)),
|
|
or(eq(workspacePages.userId, userId), eq(workspacePages.isPublic, 1))
|
|
));
|
|
|
|
if (!page) {
|
|
return res.status(404).json({ error: "Page not found" });
|
|
}
|
|
|
|
res.json(page);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to fetch page" });
|
|
}
|
|
});
|
|
|
|
router.post("/pages", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const data = insertWorkspacePageSchema.parse({ ...req.body, userId });
|
|
|
|
const [page] = await db.insert(workspacePages).values(data).returning();
|
|
|
|
await db.insert(activityFeed).values({
|
|
userId,
|
|
actorId: userId,
|
|
type: 'created',
|
|
module: 'workspace',
|
|
entityType: 'page',
|
|
entityId: page.id.toString(),
|
|
entityTitle: page.title,
|
|
description: `Criou a página "${page.title}"`
|
|
});
|
|
|
|
res.status(201).json(page);
|
|
} catch (error) {
|
|
console.error("Error creating page:", error);
|
|
res.status(500).json({ error: "Failed to create page" });
|
|
}
|
|
});
|
|
|
|
router.patch("/pages/:id", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const pageId = parseInt(req.params.id);
|
|
|
|
const [existing] = await db.select()
|
|
.from(workspacePages)
|
|
.where(and(eq(workspacePages.id, pageId), eq(workspacePages.userId, userId)));
|
|
|
|
if (!existing) {
|
|
return res.status(404).json({ error: "Page not found" });
|
|
}
|
|
|
|
const updateData = { ...req.body, updatedAt: new Date() };
|
|
delete updateData.id;
|
|
delete updateData.userId;
|
|
delete updateData.createdAt;
|
|
|
|
const [updated] = await db.update(workspacePages)
|
|
.set(updateData)
|
|
.where(eq(workspacePages.id, pageId))
|
|
.returning();
|
|
|
|
res.json(updated);
|
|
} catch (error) {
|
|
console.error("Error updating page:", error);
|
|
res.status(500).json({ error: "Failed to update page" });
|
|
}
|
|
});
|
|
|
|
router.delete("/pages/:id", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const pageId = parseInt(req.params.id);
|
|
|
|
const [deleted] = await db.delete(workspacePages)
|
|
.where(and(eq(workspacePages.id, pageId), eq(workspacePages.userId, userId)))
|
|
.returning();
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ error: "Page not found" });
|
|
}
|
|
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to delete page" });
|
|
}
|
|
});
|
|
|
|
// ========== PAGE BLOCKS ==========
|
|
router.get("/pages/:pageId/blocks", requireAuth, async (req, res) => {
|
|
try {
|
|
const pageId = parseInt(req.params.pageId);
|
|
const blocks = await db.select()
|
|
.from(pageBlocks)
|
|
.where(eq(pageBlocks.pageId, pageId))
|
|
.orderBy(pageBlocks.orderIndex);
|
|
res.json(blocks);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to fetch blocks" });
|
|
}
|
|
});
|
|
|
|
router.post("/pages/:pageId/blocks", requireAuth, async (req, res) => {
|
|
try {
|
|
const pageId = parseInt(req.params.pageId);
|
|
const data = insertPageBlockSchema.parse({ ...req.body, pageId });
|
|
|
|
const [block] = await db.insert(pageBlocks).values(data).returning();
|
|
res.status(201).json(block);
|
|
} catch (error) {
|
|
console.error("Error creating block:", error);
|
|
res.status(500).json({ error: "Failed to create block" });
|
|
}
|
|
});
|
|
|
|
router.patch("/blocks/:id", requireAuth, async (req, res) => {
|
|
try {
|
|
const blockId = parseInt(req.params.id);
|
|
const updateData = { ...req.body, updatedAt: new Date() };
|
|
delete updateData.id;
|
|
delete updateData.pageId;
|
|
delete updateData.createdAt;
|
|
|
|
const [updated] = await db.update(pageBlocks)
|
|
.set(updateData)
|
|
.where(eq(pageBlocks.id, blockId))
|
|
.returning();
|
|
|
|
if (!updated) {
|
|
return res.status(404).json({ error: "Block not found" });
|
|
}
|
|
|
|
res.json(updated);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to update block" });
|
|
}
|
|
});
|
|
|
|
router.delete("/blocks/:id", requireAuth, async (req, res) => {
|
|
try {
|
|
const blockId = parseInt(req.params.id);
|
|
const [deleted] = await db.delete(pageBlocks)
|
|
.where(eq(pageBlocks.id, blockId))
|
|
.returning();
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ error: "Block not found" });
|
|
}
|
|
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to delete block" });
|
|
}
|
|
});
|
|
|
|
router.post("/pages/:pageId/blocks/reorder", requireAuth, async (req, res) => {
|
|
try {
|
|
const { blocks } = req.body;
|
|
|
|
for (const { id, orderIndex } of blocks) {
|
|
await db.update(pageBlocks)
|
|
.set({ orderIndex, updatedAt: new Date() })
|
|
.where(eq(pageBlocks.id, id));
|
|
}
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to reorder blocks" });
|
|
}
|
|
});
|
|
|
|
// ========== DASHBOARD WIDGETS ==========
|
|
router.get("/widgets", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const widgets = await db.select()
|
|
.from(dashboardWidgets)
|
|
.where(eq(dashboardWidgets.userId, userId));
|
|
res.json(widgets);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to fetch widgets" });
|
|
}
|
|
});
|
|
|
|
router.post("/widgets", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const data = insertDashboardWidgetSchema.parse({ ...req.body, userId });
|
|
|
|
const [widget] = await db.insert(dashboardWidgets).values(data).returning();
|
|
res.status(201).json(widget);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to create widget" });
|
|
}
|
|
});
|
|
|
|
router.patch("/widgets/:id", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const widgetId = parseInt(req.params.id);
|
|
|
|
const updateData = { ...req.body };
|
|
delete updateData.id;
|
|
delete updateData.userId;
|
|
delete updateData.createdAt;
|
|
|
|
const [updated] = await db.update(dashboardWidgets)
|
|
.set(updateData)
|
|
.where(and(eq(dashboardWidgets.id, widgetId), eq(dashboardWidgets.userId, userId)))
|
|
.returning();
|
|
|
|
if (!updated) {
|
|
return res.status(404).json({ error: "Widget not found" });
|
|
}
|
|
|
|
res.json(updated);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to update widget" });
|
|
}
|
|
});
|
|
|
|
router.delete("/widgets/:id", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const widgetId = parseInt(req.params.id);
|
|
|
|
const [deleted] = await db.delete(dashboardWidgets)
|
|
.where(and(eq(dashboardWidgets.id, widgetId), eq(dashboardWidgets.userId, userId)))
|
|
.returning();
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ error: "Widget not found" });
|
|
}
|
|
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to delete widget" });
|
|
}
|
|
});
|
|
|
|
router.post("/widgets/defaults", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
|
|
const existingWidgets = await db.select().from(dashboardWidgets).where(eq(dashboardWidgets.userId, userId));
|
|
|
|
if (existingWidgets.length > 0) {
|
|
return res.json({ message: "Widgets already exist", widgets: existingWidgets });
|
|
}
|
|
|
|
const defaultWidgets = [
|
|
{ userId, type: 'tasks', title: 'Tarefas Pendentes', position: JSON.stringify({ x: 0, y: 0, w: 2, h: 2 }), isVisible: 1 },
|
|
{ userId, type: 'recent_activity', title: 'Atividades Recentes', position: JSON.stringify({ x: 2, y: 0, w: 2, h: 2 }), isVisible: 1 },
|
|
{ userId, type: 'quick_notes', title: 'Notas Rápidas', position: JSON.stringify({ x: 0, y: 2, w: 2, h: 1 }), isVisible: 1 },
|
|
{ userId, type: 'favorites', title: 'Favoritos', position: JSON.stringify({ x: 2, y: 2, w: 2, h: 1 }), isVisible: 1 },
|
|
];
|
|
|
|
const widgets = await db.insert(dashboardWidgets).values(defaultWidgets).returning();
|
|
res.status(201).json(widgets);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to create default widgets" });
|
|
}
|
|
});
|
|
|
|
// ========== QUICK NOTES ==========
|
|
router.get("/notes", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const notes = await db.select()
|
|
.from(quickNotes)
|
|
.where(eq(quickNotes.userId, userId))
|
|
.orderBy(desc(quickNotes.isPinned), desc(quickNotes.updatedAt));
|
|
res.json(notes);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to fetch notes" });
|
|
}
|
|
});
|
|
|
|
router.post("/notes", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const data = insertQuickNoteSchema.parse({ ...req.body, userId });
|
|
|
|
const [note] = await db.insert(quickNotes).values(data).returning();
|
|
res.status(201).json(note);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to create note" });
|
|
}
|
|
});
|
|
|
|
router.patch("/notes/:id", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const noteId = parseInt(req.params.id);
|
|
|
|
const updateData = { ...req.body, updatedAt: new Date() };
|
|
delete updateData.id;
|
|
delete updateData.userId;
|
|
delete updateData.createdAt;
|
|
|
|
const [updated] = await db.update(quickNotes)
|
|
.set(updateData)
|
|
.where(and(eq(quickNotes.id, noteId), eq(quickNotes.userId, userId)))
|
|
.returning();
|
|
|
|
if (!updated) {
|
|
return res.status(404).json({ error: "Note not found" });
|
|
}
|
|
|
|
res.json(updated);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to update note" });
|
|
}
|
|
});
|
|
|
|
router.delete("/notes/:id", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const noteId = parseInt(req.params.id);
|
|
|
|
const [deleted] = await db.delete(quickNotes)
|
|
.where(and(eq(quickNotes.id, noteId), eq(quickNotes.userId, userId)))
|
|
.returning();
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ error: "Note not found" });
|
|
}
|
|
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to delete note" });
|
|
}
|
|
});
|
|
|
|
// ========== ACTIVITY FEED / INBOX ==========
|
|
router.get("/activity", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const { unreadOnly, module, limit = 50 } = req.query;
|
|
const limitNum = Math.min(parseInt(limit as string) || 50, 100);
|
|
|
|
let whereClause: any = eq(activityFeed.userId, userId);
|
|
|
|
if (unreadOnly === 'true') {
|
|
whereClause = and(whereClause, eq(activityFeed.isRead, 0))!;
|
|
}
|
|
|
|
if (module) {
|
|
whereClause = and(whereClause, eq(activityFeed.module, module as string))!;
|
|
}
|
|
|
|
const activities = await db.select()
|
|
.from(activityFeed)
|
|
.where(whereClause as any)
|
|
.orderBy(desc(activityFeed.createdAt))
|
|
.limit(limitNum);
|
|
|
|
res.json(activities);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to fetch activities" });
|
|
}
|
|
});
|
|
|
|
router.get("/activity/unread-count", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const result = await db.select({ count: sql<number>`count(*)` })
|
|
.from(activityFeed)
|
|
.where(and(eq(activityFeed.userId, userId), eq(activityFeed.isRead, 0)));
|
|
|
|
res.json({ count: result[0]?.count || 0 });
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to fetch unread count" });
|
|
}
|
|
});
|
|
|
|
router.patch("/activity/:id/read", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const activityId = parseInt(req.params.id);
|
|
|
|
const [updated] = await db.update(activityFeed)
|
|
.set({ isRead: 1 })
|
|
.where(and(eq(activityFeed.id, activityId), eq(activityFeed.userId, userId)))
|
|
.returning();
|
|
|
|
res.json(updated || { success: true });
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to mark as read" });
|
|
}
|
|
});
|
|
|
|
router.post("/activity/mark-all-read", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
|
|
await db.update(activityFeed)
|
|
.set({ isRead: 1 })
|
|
.where(and(eq(activityFeed.userId, userId), eq(activityFeed.isRead, 0)));
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to mark all as read" });
|
|
}
|
|
});
|
|
|
|
// ========== FAVORITES ==========
|
|
router.get("/favorites", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const favorites = await db.select()
|
|
.from(userFavorites)
|
|
.where(eq(userFavorites.userId, userId))
|
|
.orderBy(userFavorites.orderIndex);
|
|
res.json(favorites);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to fetch favorites" });
|
|
}
|
|
});
|
|
|
|
router.post("/favorites", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const data = insertUserFavoriteSchema.parse({ ...req.body, userId });
|
|
|
|
const [favorite] = await db.insert(userFavorites).values(data).returning();
|
|
res.status(201).json(favorite);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to add favorite" });
|
|
}
|
|
});
|
|
|
|
router.delete("/favorites/:id", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const favoriteId = parseInt(req.params.id);
|
|
|
|
const [deleted] = await db.delete(userFavorites)
|
|
.where(and(eq(userFavorites.id, favoriteId), eq(userFavorites.userId, userId)))
|
|
.returning();
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ error: "Favorite not found" });
|
|
}
|
|
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to remove favorite" });
|
|
}
|
|
});
|
|
|
|
// ========== COMMAND HISTORY ==========
|
|
router.get("/commands/recent", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const commands = await db.select()
|
|
.from(commandHistory)
|
|
.where(eq(commandHistory.userId, userId))
|
|
.orderBy(desc(commandHistory.frequency), desc(commandHistory.lastUsedAt))
|
|
.limit(20);
|
|
res.json(commands);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to fetch command history" });
|
|
}
|
|
});
|
|
|
|
router.post("/commands/track", requireAuth, async (req, res) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const { command } = req.body;
|
|
|
|
const [existing] = await db.select()
|
|
.from(commandHistory)
|
|
.where(and(eq(commandHistory.userId, userId), eq(commandHistory.command, command)));
|
|
|
|
if (existing) {
|
|
const [updated] = await db.update(commandHistory)
|
|
.set({ frequency: (existing.frequency || 0) + 1, lastUsedAt: new Date() })
|
|
.where(eq(commandHistory.id, existing.id))
|
|
.returning();
|
|
res.json(updated);
|
|
} else {
|
|
const [created] = await db.insert(commandHistory)
|
|
.values({ userId, command })
|
|
.returning();
|
|
res.status(201).json(created);
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to track command" });
|
|
}
|
|
});
|
|
|
|
// ========== PAGE BACKLINKS ==========
|
|
router.get("/pages/:id/backlinks", requireAuth, async (req, res) => {
|
|
try {
|
|
const pageId = parseInt(req.params.id);
|
|
|
|
const backlinks = await db.select({
|
|
id: pageLinks.id,
|
|
sourcePageId: pageLinks.sourcePageId,
|
|
sourcePageTitle: workspacePages.title,
|
|
sourcePageIcon: workspacePages.icon,
|
|
createdAt: pageLinks.createdAt
|
|
})
|
|
.from(pageLinks)
|
|
.innerJoin(workspacePages, eq(pageLinks.sourcePageId, workspacePages.id))
|
|
.where(eq(pageLinks.targetPageId, pageId));
|
|
|
|
res.json(backlinks);
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to fetch backlinks" });
|
|
}
|
|
});
|
|
|
|
export default router;
|