Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 33 additions & 23 deletions apps/server/convex/crons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { cronJobs } from "convex/server";
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { decrementStat, getStats, STAT_KEYS } from "./lib/dbStats";
import { createLogger } from "./lib/logger";

const logger = createLogger("Cron");

const crons = cronJobs();

Expand Down Expand Up @@ -74,9 +77,11 @@ export const cleanupSoftDeletedRecords = internalMutation({
// Calculate cutoff date
const cutoffDate = Date.now() - retentionDays * 24 * 60 * 60 * 1000;

console.log(
`[Cron] Cleanup soft-deleted records - Started (retention: ${retentionDays} days, batch size: ${batchSize}, dry run: ${dryRun})`,
);
logger.info("Cleanup soft-deleted records - Started", {
retentionDays,
batchSize,
dryRun,
});

let totalDeleted = 0;

Expand All @@ -90,17 +95,19 @@ export const cleanupSoftDeletedRecords = internalMutation({

for (const chat of chatsToDelete) {
if (dryRun) {
console.log(
`[Cron] Would delete chat: ${chat._id} (deleted at: ${new Date(chat.deletedAt!).toISOString()})`,
);
logger.debug("Would delete chat", {
chatId: chat._id,
deletedAt: new Date(chat.deletedAt!).toISOString(),
});
} else {
await ctx.db.delete(chat._id);
// PERFORMANCE OPTIMIZATION: Update stats counters instead of recalculating
await decrementStat(ctx, STAT_KEYS.CHATS_TOTAL);
await decrementStat(ctx, STAT_KEYS.CHATS_SOFT_DELETED);
console.log(
`[Cron] Hard deleted chat: ${chat._id} (deleted at: ${new Date(chat.deletedAt!).toISOString()})`,
);
logger.debug("Hard deleted chat", {
chatId: chat._id,
deletedAt: new Date(chat.deletedAt!).toISOString(),
});
}
totalDeleted++;
}
Expand All @@ -114,24 +121,27 @@ export const cleanupSoftDeletedRecords = internalMutation({

for (const message of messagesToDelete) {
if (dryRun) {
console.log(
`[Cron] Would delete message: ${message._id} (deleted at: ${new Date(message.deletedAt!).toISOString()})`,
);
logger.debug("Would delete message", {
messageId: message._id,
deletedAt: new Date(message.deletedAt!).toISOString(),
});
} else {
await ctx.db.delete(message._id);
// PERFORMANCE OPTIMIZATION: Update stats counters instead of recalculating
await decrementStat(ctx, STAT_KEYS.MESSAGES_TOTAL);
await decrementStat(ctx, STAT_KEYS.MESSAGES_SOFT_DELETED);
console.log(
`[Cron] Hard deleted message: ${message._id} (deleted at: ${new Date(message.deletedAt!).toISOString()})`,
);
logger.debug("Hard deleted message", {
messageId: message._id,
deletedAt: new Date(message.deletedAt!).toISOString(),
});
}
totalDeleted++;
}

console.log(
`[Cron] Cleanup soft-deleted records - Completed (${totalDeleted} records ${dryRun ? "would be" : ""} deleted)`,
);
logger.info("Cleanup soft-deleted records - Completed", {
totalDeleted,
dryRun,
});

return {
success: true,
Expand All @@ -140,7 +150,7 @@ export const cleanupSoftDeletedRecords = internalMutation({
cutoffDate: new Date(cutoffDate).toISOString(),
};
} catch (error) {
console.error("[Cron] Cleanup soft-deleted records - Failed", error);
logger.error("Cleanup soft-deleted records - Failed", error);
throw error;
}
},
Expand Down Expand Up @@ -174,7 +184,7 @@ export const cleanupSoftDeletedRecords = internalMutation({
export const generateDatabaseStats = internalMutation({
args: {},
handler: async (ctx) => {
console.log("[Cron] Generate database stats - Started");
logger.info("Generate database stats - Started");

try {
// PERFORMANCE OPTIMIZATION: Read from stats counters instead of full table scans
Expand Down Expand Up @@ -208,18 +218,18 @@ export const generateDatabaseStats = internalMutation({
},
};

console.log("[Cron] Database statistics:", JSON.stringify(stats, null, 2));
logger.info("Database statistics", stats);

// TODO: Send alerts if any metrics exceed thresholds

console.log("[Cron] Generate database stats - Completed");
logger.info("Generate database stats - Completed");

return {
success: true,
stats,
};
} catch (error) {
console.error("[Cron] Generate database stats - Failed", error);
logger.error("Generate database stats - Failed", error);
throw error;
}
},
Expand Down
4 changes: 2 additions & 2 deletions apps/server/convex/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ export const streamLLM = httpAction(async (ctx, request) => {
}
} catch (parseError) {
// Log JSON parse errors for debugging OpenRouter protocol issues
console.warn("Failed to parse streaming chunk:", {
logger.warn("Failed to parse streaming chunk", {
data: data.slice(0, 200), // Truncate for logging
error: parseError instanceof Error ? parseError.message : String(parseError),
});
Expand Down Expand Up @@ -874,7 +874,7 @@ export const streamLLM = httpAction(async (ctx, request) => {
headers,
});
} catch (error) {
console.error("Stream error:", error);
logger.error("Stream error", error);
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : "Stream failed",
Expand Down
151 changes: 35 additions & 116 deletions apps/web/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
/**
* App Sidebar - Premium sidebar with smooth collapse animation
*/

import { useState, useEffect } from "react";
import { useNavigate, useParams } from "@tanstack/react-router";
import { useQuery } from "convex/react";
Expand All @@ -21,43 +17,7 @@ import {
SidebarMenuButton,
useSidebar,
} from "./ui/sidebar";

// Icons
const PlusIcon = () => (
<svg className="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4v16m8-8H4" />
</svg>
);

const ChatIcon = () => (
<svg className="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
);

// Sidebar panel icon (clean, modern design)
const SidebarIcon = () => (
<svg className="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="18" rx="2" strokeWidth={1.5} />
<path d="M9 3v18" strokeWidth={1.5} />
</svg>
);

const ChevronRightIcon = () => (
<svg
className="size-4 text-sidebar-foreground/40 transition-transform group-hover:translate-x-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
);
import { PlusIcon, ChatIcon, SidebarIcon, ChevronRightIcon } from "@/components/icons";

// Group chats by time periods
interface ChatItem {
Expand Down Expand Up @@ -103,6 +63,36 @@ function groupChatsByTime(chats: ChatItem[]) {
return { today, last7Days, last30Days, older };
}

interface ChatGroupProps {
label: string;
chats: ChatItem[];
currentChatId?: string;
onChatClick: (chatId: string) => void;
}

function ChatGroup({ label, chats, currentChatId, onChatClick }: ChatGroupProps) {
if (chats.length === 0) return null;

return (
<SidebarGroup>
<SidebarGroupLabel>{label}</SidebarGroupLabel>
<SidebarMenu>
{chats.map((chat) => (
<SidebarMenuItem key={chat._id}>
<SidebarMenuButton
isActive={currentChatId === chat._id}
onClick={() => onChatClick(chat._id)}
>
<ChatIcon />
<span className="truncate">{chat.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
);
}

export function AppSidebar() {
const { user } = useAuth();
const { open, isMobile, setOpen } = useSidebar();
Expand Down Expand Up @@ -249,81 +239,10 @@ export function AppSidebar() {
</div>
) : (
<>
{grouped.today.length > 0 && (
<SidebarGroup>
<SidebarGroupLabel>Today</SidebarGroupLabel>
<SidebarMenu>
{grouped.today.map((chat) => (
<SidebarMenuItem key={chat._id}>
<SidebarMenuButton
isActive={currentChatId === chat._id}
onClick={() => handleChatClick(chat._id)}
>
<ChatIcon />
<span className="truncate">{chat.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
)}

{grouped.last7Days.length > 0 && (
<SidebarGroup>
<SidebarGroupLabel>Last 7 days</SidebarGroupLabel>
<SidebarMenu>
{grouped.last7Days.map((chat) => (
<SidebarMenuItem key={chat._id}>
<SidebarMenuButton
isActive={currentChatId === chat._id}
onClick={() => handleChatClick(chat._id)}
>
<ChatIcon />
<span className="truncate">{chat.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
)}

{grouped.last30Days.length > 0 && (
<SidebarGroup>
<SidebarGroupLabel>Last 30 days</SidebarGroupLabel>
<SidebarMenu>
{grouped.last30Days.map((chat) => (
<SidebarMenuItem key={chat._id}>
<SidebarMenuButton
isActive={currentChatId === chat._id}
onClick={() => handleChatClick(chat._id)}
>
<ChatIcon />
<span className="truncate">{chat.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
)}

{grouped.older.length > 0 && (
<SidebarGroup>
<SidebarGroupLabel>Older</SidebarGroupLabel>
<SidebarMenu>
{grouped.older.map((chat) => (
<SidebarMenuItem key={chat._id}>
<SidebarMenuButton
isActive={currentChatId === chat._id}
onClick={() => handleChatClick(chat._id)}
>
<ChatIcon />
<span className="truncate">{chat.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
)}
<ChatGroup label="Today" chats={grouped.today} currentChatId={currentChatId} onChatClick={handleChatClick} />
<ChatGroup label="Last 7 days" chats={grouped.last7Days} currentChatId={currentChatId} onChatClick={handleChatClick} />
<ChatGroup label="Last 30 days" chats={grouped.last30Days} currentChatId={currentChatId} onChatClick={handleChatClick} />
<ChatGroup label="Older" chats={grouped.older} currentChatId={currentChatId} onChatClick={handleChatClick} />
</>
)}
</SidebarContent>
Expand Down
Loading
Loading