+
diff --git a/apps/dashboard/app/(main)/links/[id]/_components/clicks-chart.tsx b/apps/dashboard/app/(main)/links/[id]/_components/clicks-chart.tsx
index fad080553..ccc6892c8 100644
--- a/apps/dashboard/app/(main)/links/[id]/_components/clicks-chart.tsx
+++ b/apps/dashboard/app/(main)/links/[id]/_components/clicks-chart.tsx
@@ -1,12 +1,12 @@
"use client";
-import { ChartLineIcon } from "@phosphor-icons/react/dist/ssr/ChartLine";
+import { ChartLineIcon } from "@phosphor-icons/react";
import { Chart } from "@/components/ui/composables/chart";
import dayjs from "@/lib/dayjs";
export interface ChartDataPoint {
- date: string;
clicks: number;
+ date: string;
}
interface ClicksChartProps {
@@ -28,10 +28,7 @@ export function ClicksChart({
>
-
+
No click data available
diff --git a/apps/dashboard/app/(main)/links/[id]/_components/link-stats-columns.tsx b/apps/dashboard/app/(main)/links/[id]/_components/link-stats-columns.tsx
index 88f2c53e0..aff61a283 100644
--- a/apps/dashboard/app/(main)/links/[id]/_components/link-stats-columns.tsx
+++ b/apps/dashboard/app/(main)/links/[id]/_components/link-stats-columns.tsx
@@ -1,25 +1,26 @@
"use client";
-import { MapPinIcon } from "@phosphor-icons/react/dist/ssr/MapPin";
+import { MapPinIcon } from "@phosphor-icons/react";
import type { CellContext, ColumnDef } from "@tanstack/react-table";
import { DeviceTypeCell } from "@/components/analytics";
import { ReferrerSourceCell } from "@/components/atomic/ReferrerSourceCell";
import { CountryFlag } from "@/components/icon";
import { PercentageBadge } from "@/components/ui/percentage-badge";
+import { formatNumber } from "@/lib/formatters";
export interface SourceEntry {
- name: string;
clicks: number;
+ domain?: string;
+ name: string;
percentage: number;
referrer?: string;
- domain?: string;
}
export interface GeoEntry {
- name: string;
+ clicks: number;
country_code: string;
country_name: string;
- clicks: number;
+ name: string;
percentage: number;
}
@@ -41,16 +42,6 @@ function extractDomain(referrer: string | undefined): string | undefined {
}
}
-function formatNumber(value: number): string {
- if (value == null || Number.isNaN(value)) {
- return "0";
- }
- return Intl.NumberFormat(undefined, {
- notation: "compact",
- maximumFractionDigits: 1,
- }).format(value);
-}
-
export function createReferrerColumns(): ColumnDef[] {
return [
{
diff --git a/apps/dashboard/app/(main)/links/[id]/_components/link-stats-content.tsx b/apps/dashboard/app/(main)/links/[id]/_components/link-stats-content.tsx
index 3da1196c1..52efeaf79 100644
--- a/apps/dashboard/app/(main)/links/[id]/_components/link-stats-content.tsx
+++ b/apps/dashboard/app/(main)/links/[id]/_components/link-stats-content.tsx
@@ -1,9 +1,9 @@
"use client";
-import { CursorClickIcon } from "@phosphor-icons/react/dist/ssr/CursorClick";
-import { GlobeIcon } from "@phosphor-icons/react/dist/ssr/Globe";
-import { LinkIcon } from "@phosphor-icons/react/dist/ssr/Link";
-import { UsersIcon } from "@phosphor-icons/react/dist/ssr/Users";
+import { CursorClickIcon } from "@phosphor-icons/react";
+import { GlobeIcon } from "@phosphor-icons/react";
+import { LinkIcon } from "@phosphor-icons/react";
+import { UsersIcon } from "@phosphor-icons/react";
import { useParams, useRouter } from "next/navigation";
import { useMemo } from "react";
import { StatCard } from "@/components/analytics";
@@ -14,7 +14,7 @@ import { useDateFilters } from "@/hooks/use-date-filters";
import { useLink, useLinkStats } from "@/hooks/use-links";
import { useMediaQuery } from "@/hooks/use-media-query";
import dayjs from "@/lib/dayjs";
-import { formatMetricNumber } from "@/lib/formatters";
+import { formatNumber } from "@/lib/formatters";
import { type ChartDataPoint, ClicksChart } from "./clicks-chart";
import {
createDeviceColumns,
@@ -29,16 +29,6 @@ interface MiniChartDataPoint {
value: number;
}
-function formatNumber(value: number): string {
- if (value == null || Number.isNaN(value)) {
- return "0";
- }
- return Intl.NumberFormat(undefined, {
- notation: "compact",
- maximumFractionDigits: 1,
- }).format(value);
-}
-
function StatsLoadingSkeleton() {
return (
@@ -251,7 +241,7 @@ export function LinkStatsContent() {
isLoading={isLoading}
showChart={true}
title="Total Clicks"
- value={formatMetricNumber(stats?.totalClicks ?? 0)}
+ value={formatNumber(stats?.totalClicks ?? 0)}
/>
;
- title: string;
badge?: number | boolean;
+ children: React.ReactNode;
+ icon: React.ComponentType<{ size?: number; weight?: "duotone" | "fill" }>;
isExpanded: boolean;
onToggleAction: () => void;
- children: React.ReactNode;
+ title: string;
}
export function CollapsibleSection({
diff --git a/apps/dashboard/app/(main)/links/_components/expiration-picker.tsx b/apps/dashboard/app/(main)/links/_components/expiration-picker.tsx
index ec64ffcc5..7a137b46e 100644
--- a/apps/dashboard/app/(main)/links/_components/expiration-picker.tsx
+++ b/apps/dashboard/app/(main)/links/_components/expiration-picker.tsx
@@ -1,12 +1,10 @@
"use client";
-import {
- CalendarIcon,
- CheckIcon,
- ClockIcon,
- InfinityIcon,
- XIcon,
-} from "@phosphor-icons/react";
+import { CalendarIcon } from "@phosphor-icons/react";
+import { CheckIcon } from "@phosphor-icons/react";
+import { ClockIcon } from "@phosphor-icons/react";
+import { InfinityIcon } from "@phosphor-icons/react";
+import { XIcon } from "@phosphor-icons/react";
import { useCallback, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
@@ -19,9 +17,9 @@ import dayjs from "@/lib/dayjs";
import { cn } from "@/lib/utils";
interface ExpirationPreset {
+ getDate: () => Date;
label: string;
value: string;
- getDate: () => Date;
}
const EXPIRATION_PRESETS: ExpirationPreset[] = [
@@ -73,9 +71,9 @@ function formatPresetPreview(preset: ExpirationPreset): string {
}
interface ExpirationPickerProps {
- value?: string;
- onChange: (value: string) => void;
className?: string;
+ onChange: (value: string) => void;
+ value?: string;
}
export function ExpirationPicker({
diff --git a/apps/dashboard/app/(main)/links/_components/link-form-fields.tsx b/apps/dashboard/app/(main)/links/_components/link-form-fields.tsx
index 76e9e8198..6b6931bb8 100644
--- a/apps/dashboard/app/(main)/links/_components/link-form-fields.tsx
+++ b/apps/dashboard/app/(main)/links/_components/link-form-fields.tsx
@@ -1,13 +1,11 @@
"use client";
-import {
- AndroidLogoIcon,
- AppleLogoIcon,
- CalendarIcon,
- DeviceMobileIcon,
- ImageIcon,
- LinkSimpleIcon,
-} from "@phosphor-icons/react";
+import { AndroidLogoIcon } from "@phosphor-icons/react";
+import { AppleLogoIcon } from "@phosphor-icons/react";
+import { CalendarIcon } from "@phosphor-icons/react";
+import { DeviceMobileIcon } from "@phosphor-icons/react";
+import { ImageIcon } from "@phosphor-icons/react";
+import { LinkSimpleIcon } from "@phosphor-icons/react";
import type { UseFormReturn } from "react-hook-form";
import {
FormControl,
@@ -29,22 +27,22 @@ import { type OgData, OgPreview } from "./og-preview";
import { UtmBuilder, type UtmParams } from "./utm-builder";
interface LinkFormFieldsProps {
- form: UseFormReturn;
- isEditMode: boolean;
+ deviceTargetingCount: number;
expandedSection: ExpandedSection;
- onToggleSectionAction: (section: ExpandedSection) => void;
- slugValue: string | undefined;
+ form: UseFormReturn;
fullTargetUrl: string;
- utmParams: UtmParams;
- onUtmParamsChangeAction: (params: UtmParams) => void;
+ hasCustomSocial: boolean;
+ hasExpiration: boolean;
+ isEditMode: boolean;
ogData: OgData;
onOgDataChangeAction: (data: OgData) => void;
- useCustomOg: boolean;
+ onToggleSectionAction: (section: ExpandedSection) => void;
onUseCustomOgChangeAction: (useCustom: boolean) => void;
- hasExpiration: boolean;
- deviceTargetingCount: number;
+ onUtmParamsChangeAction: (params: UtmParams) => void;
+ slugValue: string | undefined;
+ useCustomOg: boolean;
+ utmParams: UtmParams;
utmParamsCount: number;
- hasCustomSocial: boolean;
}
export function LinkFormFields({
diff --git a/apps/dashboard/app/(main)/links/_components/link-item.tsx b/apps/dashboard/app/(main)/links/_components/link-item.tsx
index d481b18a6..2ddff2adc 100644
--- a/apps/dashboard/app/(main)/links/_components/link-item.tsx
+++ b/apps/dashboard/app/(main)/links/_components/link-item.tsx
@@ -1,14 +1,12 @@
"use client";
-import {
- ClockCountdownIcon,
- CopyIcon,
- DotsThreeIcon,
- PencilSimpleIcon,
- QrCodeIcon,
- TrashIcon,
-} from "@phosphor-icons/react";
-import { LinkIcon } from "@phosphor-icons/react/dist/ssr/Link";
+import { ClockCountdownIcon } from "@phosphor-icons/react";
+import { CopyIcon } from "@phosphor-icons/react";
+import { DotsThreeIcon } from "@phosphor-icons/react";
+import { PencilSimpleIcon } from "@phosphor-icons/react";
+import { QrCodeIcon } from "@phosphor-icons/react";
+import { TrashIcon } from "@phosphor-icons/react";
+import { LinkIcon } from "@phosphor-icons/react";
import NextLink from "next/link";
import { toast } from "sonner";
import { FaviconImage } from "@/components/analytics/favicon-image";
@@ -220,10 +218,10 @@ function LinkRow({
interface LinksListProps {
links: Link[];
- onEdit: (link: Link) => void;
+ onCreateLink: () => void;
onDelete: (linkId: string) => void;
+ onEdit: (link: Link) => void;
onShowQr: (link: Link) => void;
- onCreateLink: () => void;
}
export function LinksList({
diff --git a/apps/dashboard/app/(main)/links/_components/link-qr-code.tsx b/apps/dashboard/app/(main)/links/_components/link-qr-code.tsx
index 1d7579c41..d6d3f3e54 100644
--- a/apps/dashboard/app/(main)/links/_components/link-qr-code.tsx
+++ b/apps/dashboard/app/(main)/links/_components/link-qr-code.tsx
@@ -1,11 +1,9 @@
"use client";
-import {
- CopyIcon,
- DownloadSimpleIcon,
- ImageIcon,
- XIcon,
-} from "@phosphor-icons/react";
+import { CopyIcon } from "@phosphor-icons/react";
+import { DownloadSimpleIcon } from "@phosphor-icons/react";
+import { ImageIcon } from "@phosphor-icons/react";
+import { XIcon } from "@phosphor-icons/react";
import { useCallback, useRef, useState } from "react";
import { QRCode } from "react-qrcode-logo";
import { toast } from "sonner";
@@ -34,10 +32,10 @@ const QR_COLORS = [
];
interface LinkQrCodeProps {
- slug: string;
+ className?: string;
name: string;
showControls?: boolean;
- className?: string;
+ slug: string;
}
export function LinkQrCode({
diff --git a/apps/dashboard/app/(main)/links/_components/link-sheet.tsx b/apps/dashboard/app/(main)/links/_components/link-sheet.tsx
index ae85858fa..c8263419f 100644
--- a/apps/dashboard/app/(main)/links/_components/link-sheet.tsx
+++ b/apps/dashboard/app/(main)/links/_components/link-sheet.tsx
@@ -1,12 +1,10 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import {
- CircleNotchIcon,
- CopyIcon,
- LinkSimpleIcon,
- QrCodeIcon,
-} from "@phosphor-icons/react";
+import { CircleNotchIcon } from "@phosphor-icons/react";
+import { CopyIcon } from "@phosphor-icons/react";
+import { LinkSimpleIcon } from "@phosphor-icons/react";
+import { QrCodeIcon } from "@phosphor-icons/react";
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
import { type SubmitHandler, useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -54,10 +52,10 @@ const DEFAULT_OG_DATA: OgData = {
};
interface LinkSheetProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
link?: Link | null;
+ onOpenChange: (open: boolean) => void;
onSave?: (link: Link) => void;
+ open: boolean;
}
function LinkSheetInner({ open, onOpenChange, link, onSave }: LinkSheetProps) {
diff --git a/apps/dashboard/app/(main)/links/_components/link-utils.ts b/apps/dashboard/app/(main)/links/_components/link-utils.ts
index 9c452f63e..ab7c34c12 100644
--- a/apps/dashboard/app/(main)/links/_components/link-utils.ts
+++ b/apps/dashboard/app/(main)/links/_components/link-utils.ts
@@ -73,25 +73,25 @@ interface BuildPayloadInput {
androidUrl?: string;
externalId?: string;
};
- utmParams: UtmParams;
ogData: OgData;
useCustomOg: boolean;
+ utmParams: UtmParams;
}
interface LinkPayload {
- name: string;
- targetUrl: string;
- slug: string | undefined;
+ androidUrl: string | undefined;
+ expiredRedirectUrl: string | undefined;
expiresAtDate: Date | undefined;
expiresAtString: string | undefined;
- expiredRedirectUrl: string | undefined;
- ogTitle: string | undefined;
+ externalId: string | undefined;
+ iosUrl: string | undefined;
+ name: string;
ogDescription: string | undefined;
ogImageUrl: string | undefined;
+ ogTitle: string | undefined;
ogVideoUrl: string | undefined;
- iosUrl: string | undefined;
- androidUrl: string | undefined;
- externalId: string | undefined;
+ slug: string | undefined;
+ targetUrl: string;
}
export function buildLinkPayload({
diff --git a/apps/dashboard/app/(main)/links/_components/links-search-bar.tsx b/apps/dashboard/app/(main)/links/_components/links-search-bar.tsx
index 09d688f7c..b07d1cb57 100644
--- a/apps/dashboard/app/(main)/links/_components/links-search-bar.tsx
+++ b/apps/dashboard/app/(main)/links/_components/links-search-bar.tsx
@@ -1,11 +1,9 @@
"use client";
-import {
- FunnelIcon,
- MagnifyingGlassIcon,
- SortAscendingIcon,
- XIcon,
-} from "@phosphor-icons/react";
+import { FunnelIcon } from "@phosphor-icons/react";
+import { MagnifyingGlassIcon } from "@phosphor-icons/react";
+import { SortAscendingIcon } from "@phosphor-icons/react";
+import { XIcon } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -28,10 +26,10 @@ const SORT_LABELS: Record = {
};
interface LinksSearchBarProps {
- searchQuery: string;
onSearchQueryChangeAction: (query: string) => void;
- sortBy: SortOption;
onSortByChangeAction: (sort: SortOption) => void;
+ searchQuery: string;
+ sortBy: SortOption;
}
export function LinksSearchBar({
diff --git a/apps/dashboard/app/(main)/links/_components/og-preview.tsx b/apps/dashboard/app/(main)/links/_components/og-preview.tsx
index e411b3caa..a28225b6a 100644
--- a/apps/dashboard/app/(main)/links/_components/og-preview.tsx
+++ b/apps/dashboard/app/(main)/links/_components/og-preview.tsx
@@ -1,15 +1,13 @@
"use client";
-import {
- ArrowCounterClockwiseIcon,
- ArrowsClockwiseIcon,
- CheckCircleIcon,
- CircleNotchIcon,
- XIcon as CloseIcon,
- ImageIcon,
- VideoIcon,
- WarningCircleIcon,
-} from "@phosphor-icons/react";
+import { ArrowCounterClockwiseIcon } from "@phosphor-icons/react";
+import { ArrowsClockwiseIcon } from "@phosphor-icons/react";
+import { CheckCircleIcon } from "@phosphor-icons/react";
+import { CircleNotchIcon } from "@phosphor-icons/react";
+import { XIcon as CloseIcon } from "@phosphor-icons/react";
+import { ImageIcon } from "@phosphor-icons/react";
+import { VideoIcon } from "@phosphor-icons/react";
+import { WarningCircleIcon } from "@phosphor-icons/react";
import { useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -24,9 +22,9 @@ import {
} from "./use-og-metadata";
export interface OgData {
- ogTitle: string;
ogDescription: string;
ogImageUrl: string;
+ ogTitle: string;
ogVideoUrl: string;
}
@@ -34,11 +32,11 @@ const TITLE_MAX = 120;
const DESCRIPTION_MAX = 240;
interface OgPreviewProps {
- targetUrl: string;
- value: OgData;
onChange: (data: OgData) => void;
- useCustomOg: boolean;
onUseCustomOgChange: (useCustom: boolean) => void;
+ targetUrl: string;
+ useCustomOg: boolean;
+ value: OgData;
}
export function OgPreview({
diff --git a/apps/dashboard/app/(main)/links/_components/qr-code-dialog.tsx b/apps/dashboard/app/(main)/links/_components/qr-code-dialog.tsx
index 60714e6e6..3b41d273e 100644
--- a/apps/dashboard/app/(main)/links/_components/qr-code-dialog.tsx
+++ b/apps/dashboard/app/(main)/links/_components/qr-code-dialog.tsx
@@ -12,8 +12,8 @@ import { LinkQrCode } from "./link-qr-code";
interface QrCodeDialogProps {
link: Link | null;
- open: boolean;
onOpenChange: (open: boolean) => void;
+ open: boolean;
}
export function QrCodeDialog({ link, open, onOpenChange }: QrCodeDialogProps) {
diff --git a/apps/dashboard/app/(main)/links/_components/use-og-metadata.ts b/apps/dashboard/app/(main)/links/_components/use-og-metadata.ts
index 6b383f2bd..831350053 100644
--- a/apps/dashboard/app/(main)/links/_components/use-og-metadata.ts
+++ b/apps/dashboard/app/(main)/links/_components/use-og-metadata.ts
@@ -5,9 +5,9 @@ import { useQuery } from "@tanstack/react-query";
import { useCallback, useEffect, useState } from "react";
export interface FetchedOgData {
- title: string;
description: string;
image: string;
+ title: string;
}
const TRUSTED_IMAGE_HOSTS = new Set(["cdn.databuddy.cc", "api.dicebear.com"]);
diff --git a/apps/dashboard/app/(main)/links/_components/utm-builder.tsx b/apps/dashboard/app/(main)/links/_components/utm-builder.tsx
index dc8909736..b7c6bd464 100644
--- a/apps/dashboard/app/(main)/links/_components/utm-builder.tsx
+++ b/apps/dashboard/app/(main)/links/_components/utm-builder.tsx
@@ -7,17 +7,17 @@ import { Label } from "@/components/ui/label";
const PROTOCOL_REGEX = /^https?:\/\//;
export interface UtmParams {
- utm_source: string;
- utm_medium: string;
utm_campaign: string;
utm_content: string;
+ utm_medium: string;
+ utm_source: string;
utm_term: string;
}
interface UtmBuilderProps {
- value: UtmParams;
- onChange: (params: UtmParams) => void;
baseUrl?: string;
+ onChange: (params: UtmParams) => void;
+ value: UtmParams;
}
const UTM_FIELDS = [
diff --git a/apps/dashboard/app/(main)/links/page.tsx b/apps/dashboard/app/(main)/links/page.tsx
index 881b31871..99f009e5a 100644
--- a/apps/dashboard/app/(main)/links/page.tsx
+++ b/apps/dashboard/app/(main)/links/page.tsx
@@ -5,8 +5,8 @@ import {
MagnifyingGlassIcon,
PlusIcon,
TrendDownIcon,
-} from "@phosphor-icons/react/dist/ssr";
-import { LinkIcon } from "@phosphor-icons/react/dist/ssr/Link";
+} from "@phosphor-icons/react";
+import { LinkIcon } from "@phosphor-icons/react";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import { PageHeader } from "@/app/(main)/websites/_components/page-header";
diff --git a/apps/dashboard/app/(main)/llm/_components/llm-kpis.tsx b/apps/dashboard/app/(main)/llm/_components/llm-kpis.tsx
deleted file mode 100644
index 4e039f71e..000000000
--- a/apps/dashboard/app/(main)/llm/_components/llm-kpis.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-"use client";
-
-import { ChartLineUpIcon } from "@phosphor-icons/react/dist/ssr/ChartLineUp";
-import { ClockIcon } from "@phosphor-icons/react/dist/ssr/Clock";
-import { CpuIcon } from "@phosphor-icons/react/dist/ssr/Cpu";
-import { CurrencyDollarIcon } from "@phosphor-icons/react/dist/ssr/CurrencyDollar";
-import { LightningIcon } from "@phosphor-icons/react/dist/ssr/Lightning";
-import { WarningCircleIcon } from "@phosphor-icons/react/dist/ssr/WarningCircle";
-import { WrenchIcon } from "@phosphor-icons/react/dist/ssr/Wrench";
-import { StatCard } from "@/components/analytics/stat-card";
-import {
- formatCurrency,
- formatDuration,
- formatNumber,
- formatPercentage,
- type LLMKpiData,
-} from "./llm-types";
-
-interface LLMKpisProps {
- kpis: LLMKpiData | null;
- isLoading: boolean;
- chartData: {
- cost: Array<{ date: string; value: number }>;
- calls: Array<{ date: string; value: number }>;
- tokens: Array<{ date: string; value: number }>;
- latency: Array<{ date: string; value: number }>;
- };
-}
-
-export function LLMPrimaryKpis({ kpis, isLoading, chartData }: LLMKpisProps) {
- return (
-
-
-
-
-
-
- );
-}
-
-export function LLMSecondaryKpis({
- kpis,
- isLoading,
-}: Omit) {
- return (
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/dashboard/app/(main)/llm/_components/llm-page-context.tsx b/apps/dashboard/app/(main)/llm/_components/llm-page-context.tsx
deleted file mode 100644
index b92582397..000000000
--- a/apps/dashboard/app/(main)/llm/_components/llm-page-context.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-"use client";
-
-import {
- createContext,
- useCallback,
- useContext,
- useMemo,
- useRef,
- useState,
-} from "react";
-import { useOrganizationsContext } from "@/components/providers/organizations-provider";
-import { useWebsitesLight } from "@/hooks/use-websites";
-import dayjs from "@/lib/dayjs";
-
-type RefreshFn = () => void;
-
-interface LLMPageContextValue {
- selectedWebsiteId: string | null;
- setSelectedWebsiteId: (id: string | null) => void;
- selectedWebsite: { id: string; name: string; domain: string } | undefined;
- websites: Array<{ id: string; name: string; domain: string }>;
- isLoadingWebsites: boolean;
- queryOptions: { websiteId?: string; organizationId?: string };
- hasQueryId: boolean;
- dateRange: {
- start_date: string;
- end_date: string;
- granularity: "daily";
- };
- isLoadingOrg: boolean;
- registerRefresh: (fn: RefreshFn) => () => void;
- refresh: () => void;
- isFetching: boolean;
- setIsFetching: (fetching: boolean) => void;
-}
-
-const LLMPageContext = createContext(null);
-
-export const DEFAULT_DATE_RANGE = {
- start_date: dayjs().subtract(30, "day").format("YYYY-MM-DD"),
- end_date: dayjs().format("YYYY-MM-DD"),
- granularity: "daily" as const,
-};
-
-export function LLMPageProvider({ children }: { children: React.ReactNode }) {
- const {
- activeOrganization,
- activeOrganizationId,
- isLoading: isLoadingOrg,
- } = useOrganizationsContext();
- const { websites, isLoading: isLoadingWebsites } = useWebsitesLight();
- const [selectedWebsiteId, setSelectedWebsiteId] = useState(
- null
- );
- const [isFetching, setIsFetching] = useState(false);
- const refreshFnsRef = useRef>(new Set());
-
- const registerRefresh = useCallback((fn: RefreshFn) => {
- refreshFnsRef.current.add(fn);
- return () => {
- refreshFnsRef.current.delete(fn);
- };
- }, []);
-
- const refresh = useCallback(() => {
- for (const fn of refreshFnsRef.current) {
- fn();
- }
- }, []);
-
- const queryOptions = useMemo(() => {
- if (selectedWebsiteId) {
- return { websiteId: selectedWebsiteId };
- }
- return {};
- }, [selectedWebsiteId]);
-
- const hasQueryId = !!(
- selectedWebsiteId ||
- activeOrganization?.id ||
- activeOrganizationId
- );
- const selectedWebsite = websites.find((w) => w.id === selectedWebsiteId);
-
- const value = useMemo(
- () => ({
- selectedWebsiteId,
- setSelectedWebsiteId,
- selectedWebsite,
- websites,
- isLoadingWebsites,
- queryOptions,
- hasQueryId,
- dateRange: DEFAULT_DATE_RANGE,
- isLoadingOrg,
- registerRefresh,
- refresh,
- isFetching,
- setIsFetching,
- }),
- [
- selectedWebsiteId,
- selectedWebsite,
- websites,
- isLoadingWebsites,
- queryOptions,
- hasQueryId,
- isLoadingOrg,
- registerRefresh,
- refresh,
- isFetching,
- ]
- );
-
- return (
- {children}
- );
-}
-
-export function useLLMPageContext() {
- const context = useContext(LLMPageContext);
- if (!context) {
- throw new Error("useLLMPageContext must be used within LLMPageProvider");
- }
- return context;
-}
diff --git a/apps/dashboard/app/(main)/llm/_components/llm-page-header.tsx b/apps/dashboard/app/(main)/llm/_components/llm-page-header.tsx
deleted file mode 100644
index 31b52ca8e..000000000
--- a/apps/dashboard/app/(main)/llm/_components/llm-page-header.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-"use client";
-
-import { ArrowClockwiseIcon } from "@phosphor-icons/react/dist/ssr/ArrowClockwise";
-import { CaretDownIcon } from "@phosphor-icons/react/dist/ssr/CaretDown";
-import { RobotIcon } from "@phosphor-icons/react/dist/ssr/Robot";
-import { PageHeader } from "@/app/(main)/websites/_components/page-header";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Skeleton } from "@/components/ui/skeleton";
-import { useLLMPageContext } from "./llm-page-context";
-
-export function LLMPageHeader() {
- const {
- setSelectedWebsiteId,
- selectedWebsite,
- websites,
- isLoadingWebsites,
- refresh,
- isFetching,
- hasQueryId,
- } = useLLMPageContext();
-
- return (
- }
- right={
-
-
-
-
-
-
- setSelectedWebsiteId(null)}>
- All Websites
-
- {websites.map((website) => (
- setSelectedWebsiteId(website.id)}
- >
- {website.name || website.domain}
-
- ))}
-
-
-
-
- }
- title="LLM Analytics"
- />
- );
-}
diff --git a/apps/dashboard/app/(main)/llm/_components/llm-tables.tsx b/apps/dashboard/app/(main)/llm/_components/llm-tables.tsx
deleted file mode 100644
index 1d4b43bb1..000000000
--- a/apps/dashboard/app/(main)/llm/_components/llm-tables.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-"use client";
-
-import { RobotIcon } from "@phosphor-icons/react/dist/ssr/Robot";
-import { WrenchIcon } from "@phosphor-icons/react/dist/ssr/Wrench";
-import type { ColumnDef } from "@tanstack/react-table";
-import { useMemo } from "react";
-import { DataTable } from "@/components/table/data-table";
-import { PercentageBadge } from "@/components/ui/percentage-badge";
-import { TruncatedText } from "@/components/ui/truncated-text";
-import {
- formatCurrency,
- formatDuration,
- formatNumber,
- type LLMModelData,
- type LLMToolData,
-} from "./llm-types";
-
-// Helper components
-function createMetricDisplay(
- value: number,
- label: string,
- format = formatNumber
-) {
- return (
-
-
{format(value)}
-
{label}
-
- );
-}
-
-function createToolIndicator() {
- return ;
-}
-
-function createModelIndicator() {
- return ;
-}
-
-// Tool columns
-function createToolColumns(
- totalCalls: number
-): ColumnDef[] {
- return [
- {
- id: "name",
- accessorKey: "name",
- header: "Tool Name",
- cell: ({ getValue }) => {
- const name = getValue() as string;
- return (
-
- {createToolIndicator()}
-
-
-
- );
- },
- },
- {
- id: "calls",
- accessorKey: "calls",
- header: "Calls",
- cell: ({ getValue }) =>
- createMetricDisplay(getValue() as number, "total"),
- },
- {
- id: "percentage",
- accessorKey: "calls",
- header: "Share",
- cell: ({ getValue }) => {
- const calls = getValue() as number;
- const percentage = totalCalls > 0 ? (calls / totalCalls) * 100 : 0;
- return ;
- },
- },
- ];
-}
-
-// Model columns
-function createModelColumns(
- totalCost: number
-): ColumnDef[] {
- return [
- {
- id: "name",
- accessorKey: "name",
- header: "Model",
- cell: ({ row }) => {
- const model = row.original;
- return (
-
- {createModelIndicator()}
-
-
-
-
- {model.provider}
-
-
-
- );
- },
- },
- {
- id: "calls",
- accessorKey: "calls",
- header: "Requests",
- cell: ({ getValue }) =>
- createMetricDisplay(getValue() as number, "total"),
- },
- {
- id: "total_tokens",
- accessorKey: "total_tokens",
- header: "Tokens",
- cell: ({ getValue }) => createMetricDisplay(getValue() as number, "used"),
- },
- {
- id: "avg_duration_ms",
- accessorKey: "avg_duration_ms",
- header: "Latency",
- cell: ({ getValue }) =>
- createMetricDisplay(getValue() as number, "avg", formatDuration),
- },
- {
- id: "total_cost",
- accessorKey: "total_cost",
- header: "Cost",
- cell: ({ getValue }) =>
- createMetricDisplay(getValue() as number, "total", formatCurrency),
- },
- {
- id: "percentage",
- accessorKey: "total_cost",
- header: "Share",
- cell: ({ getValue }) => {
- const cost = getValue() as number;
- const percentage = totalCost > 0 ? (cost / totalCost) * 100 : 0;
- return ;
- },
- },
- ];
-}
-
-interface LLMTablesProps {
- tools: LLMToolData[];
- models: LLMModelData[];
- isLoading: boolean;
-}
-
-export function LLMTables({ tools, models, isLoading }: LLMTablesProps) {
- // Calculate totals for percentage
- const totalToolCalls = useMemo(
- () => tools.reduce((sum, t) => sum + (t.calls || 0), 0),
- [tools]
- );
- const totalModelCost = useMemo(
- () => models.reduce((sum, m) => sum + (m.total_cost || 0), 0),
- [models]
- );
-
- // Transform data for DataTable (needs `name` field)
- const toolsTableData = useMemo(
- () => tools.map((t) => ({ ...t, name: t.tool_name })),
- [tools]
- );
- const modelsTableData = useMemo(
- () => models.map((m) => ({ ...m, name: m.model })),
- [models]
- );
-
- const toolColumns = useMemo(
- () => createToolColumns(totalToolCalls),
- [totalToolCalls]
- );
- const modelColumns = useMemo(
- () => createModelColumns(totalModelCost),
- [totalModelCost]
- );
-
- return (
-
-
-
-
- );
-}
diff --git a/apps/dashboard/app/(main)/llm/_components/llm-time-series-chart.tsx b/apps/dashboard/app/(main)/llm/_components/llm-time-series-chart.tsx
deleted file mode 100644
index 0b859c575..000000000
--- a/apps/dashboard/app/(main)/llm/_components/llm-time-series-chart.tsx
+++ /dev/null
@@ -1,342 +0,0 @@
-"use client";
-
-import { ChartLineIcon } from "@phosphor-icons/react/dist/ssr/ChartLine";
-import { useCallback, useState } from "react";
-import { SkeletonChart } from "@/components/charts/skeleton-chart";
-import { Chart } from "@/components/ui/composables/chart";
-import {
- chartAxisTickDefault,
- chartAxisYWidthDefault,
- chartCartesianGridDefault,
- chartRechartsInteractiveLegendLabelClassName,
- chartRechartsLegendIconSize,
- chartRechartsLegendInteractiveWrapperStyle,
- chartSurfaceBorderlessClassName,
-} from "@/lib/chart-presentation";
-import {
- formatCurrency,
- formatDuration,
- formatNumber,
- type LLMTimeSeriesData,
-} from "./llm-types";
-
-const {
- Area,
- CartesianGrid,
- ComposedChart,
- Legend,
- ResponsiveContainer,
- Tooltip,
- XAxis,
- YAxis,
-} = Chart.Recharts;
-
-interface MetricConfig {
- key: string;
- label: string;
- color: string;
- gradient: string;
- formatValue: (value: number) => string;
-}
-
-const METRICS: MetricConfig[] = [
- {
- key: "total_cost",
- label: "Cost",
- color: "var(--chart-1)",
- gradient: "llm-cost",
- formatValue: formatCurrency,
- },
- {
- key: "total_calls",
- label: "Requests",
- color: "#3b82f6",
- gradient: "llm-calls",
- formatValue: formatNumber,
- },
- {
- key: "total_tokens",
- label: "Tokens",
- color: "#10b981",
- gradient: "llm-tokens",
- formatValue: formatNumber,
- },
- {
- key: "avg_duration_ms",
- label: "Latency",
- color: "#f59e0b",
- gradient: "llm-latency",
- formatValue: formatDuration,
- },
-];
-
-interface TooltipPayloadEntry {
- dataKey: string;
- value: number;
- color: string;
- payload: Record;
-}
-
-interface TooltipProps {
- active?: boolean;
- payload?: TooltipPayloadEntry[];
- label?: string;
-}
-
-function CustomTooltip({ active, payload, label }: TooltipProps) {
- if (!(active && payload?.length)) {
- return null;
- }
-
- return (
-
-
-
- {payload.map((entry) => {
- const metric = METRICS.find((m) => m.key === entry.dataKey);
- if (!metric || entry.value === undefined || entry.value === null) {
- return null;
- }
-
- return (
-
-
-
- {metric.formatValue(entry.value)}
-
-
- );
- })}
-
-
- );
-}
-
-interface LLMTimeSeriesChartProps {
- data: LLMTimeSeriesData[];
- isLoading: boolean;
- height?: number;
-}
-
-export function LLMTimeSeriesChart({
- data,
- isLoading,
- height = 350,
-}: LLMTimeSeriesChartProps) {
- const [hiddenMetrics, setHiddenMetrics] = useState>(
- {}
- );
- const [hasAnimated, setHasAnimated] = useState(false);
-
- const toggleMetric = useCallback((key: string) => {
- setHiddenMetrics((prev) => ({
- ...prev,
- [key]: !prev[key],
- }));
- }, []);
-
- if (isLoading) {
- return (
-
-
-
-
- Usage Over Time
-
-
- Daily cost, requests, and performance trends
-
-
-
-
-
-
-
- );
- }
-
- if (data.length === 0) {
- return (
-
-
-
-
- Usage Over Time
-
-
- Daily cost, requests, and performance trends
-
-
-
-
-
-
-
-
-
- No data available
-
-
- Your LLM analytics data will appear here as AI calls are made
-
-
-
-
- );
- }
-
- return (
-
-
-
-
- Usage Over Time
-
-
- Daily cost, requests, and performance trends
-
-
-
-
-
14 ? 800 : undefined }}
- >
-
-
-
- 5 ? 60 : 20,
- }}
- >
-
- {METRICS.map((metric) => (
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
- }
- cursor={Chart.tooltipCursorLine}
- />
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/dashboard/app/(main)/llm/_components/llm-types.ts b/apps/dashboard/app/(main)/llm/_components/llm-types.ts
deleted file mode 100644
index d9623f95d..000000000
--- a/apps/dashboard/app/(main)/llm/_components/llm-types.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-export interface LLMKpiData {
- total_calls: number;
- total_cost: number;
- total_tokens: number;
- total_input_tokens: number;
- total_output_tokens: number;
- avg_duration_ms: number;
- p75_duration_ms: number;
- error_count: number;
- error_rate: number;
- cache_hit_rate: number;
- tool_use_rate: number;
- web_search_rate: number;
-}
-
-export interface LLMTimeSeriesData {
- date: string;
- total_calls: number;
- total_cost: number;
- total_tokens: number;
- avg_duration_ms: number;
- p75_duration_ms: number;
-}
-
-export interface LLMModelData {
- name: string;
- model: string;
- provider: string;
- calls: number;
- total_cost: number;
- total_tokens: number;
- avg_duration_ms: number;
- p75_duration_ms: number;
- error_rate: number;
-}
-
-export interface LLMToolData {
- name: string;
- tool_name: string;
- calls: number;
-}
-
-export interface LLMErrorSeriesData {
- date: string;
- error_count: number;
- error_rate: number;
-}
-
-export interface LLMErrorBreakdownData {
- name: string;
- error_name: string;
- sample_message: string;
- error_count: number;
-}
-
-export interface LLMHttpStatusData {
- name: string;
- http_status: number;
- calls: number;
-}
-
-export interface LLMRecentErrorData {
- name: string;
- timestamp: string;
- error_name: string;
- error_message: string;
- error_stack?: string;
- model: string;
- provider: string;
- http_status?: number;
- duration_ms: number;
-}
-
-export function formatCurrency(value: number | null | undefined): string {
- if (value === null || value === undefined) {
- return "$0.00";
- }
- if (value < 0.01 && value > 0) {
- return `$${value.toFixed(4)}`;
- }
- return `$${value.toFixed(2)}`;
-}
-
-export function formatNumber(value: number | null | undefined): string {
- if (value === null || value === undefined) {
- return "0";
- }
- return Intl.NumberFormat(undefined, {
- notation: "compact",
- maximumFractionDigits: 1,
- }).format(value);
-}
-
-export function formatDuration(ms: number | null | undefined): string {
- if (ms === null || ms === undefined || ms === 0) {
- return "0ms";
- }
- if (ms < 1000) {
- return `${Math.round(ms)}ms`;
- }
- return `${(ms / 1000).toFixed(1)}s`;
-}
-
-export function formatPercentage(value: number | null | undefined): string {
- if (value === null || value === undefined) {
- return "0%";
- }
- return `${(value * 100).toFixed(1)}%`;
-}
diff --git a/apps/dashboard/app/(main)/llm/errors/_components/llm-error-detail-modal.tsx b/apps/dashboard/app/(main)/llm/errors/_components/llm-error-detail-modal.tsx
deleted file mode 100644
index 53ee170f9..000000000
--- a/apps/dashboard/app/(main)/llm/errors/_components/llm-error-detail-modal.tsx
+++ /dev/null
@@ -1,412 +0,0 @@
-"use client";
-
-import {
- CheckIcon,
- ClockIcon,
- CodeIcon,
- CopyIcon,
- RobotIcon,
- StackIcon,
- TimerIcon,
- WarningIcon,
-} from "@phosphor-icons/react";
-import { useState } from "react";
-import { toast } from "sonner";
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from "@/components/ui/accordion";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import {
- Sheet,
- SheetBody,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { fromNow } from "@/lib/time";
-import type { LLMRecentErrorData } from "../../_components/llm-types";
-import { formatDuration } from "../../_components/llm-types";
-
-interface LLMErrorDetailModalProps {
- error: LLMRecentErrorData;
- isOpen: boolean;
- onCloseAction: () => void;
-}
-
-type CopiedSection = "name" | "message" | "stack" | "all" | null;
-
-function formatDateTimeSeconds(timestamp: string): string {
- const date = new Date(timestamp);
- return date.toLocaleString(undefined, {
- year: "numeric",
- month: "short",
- day: "numeric",
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- });
-}
-
-function getHttpStatusSeverity(
- status?: number
-): "high" | "medium" | "low" | null {
- if (!status) {
- return null;
- }
- if (status >= 500) {
- return "high";
- }
- if (status >= 400) {
- return "medium";
- }
- return "low";
-}
-
-function getSeverityColor(severity: "high" | "medium" | "low"): string {
- const colors = {
- high: "bg-destructive/10 text-destructive border-destructive/20",
- medium: "bg-chart-2/10 text-chart-2 border-chart-2/20",
- low: "bg-chart-3/10 text-chart-3 border-chart-3/20",
- };
- return colors[severity];
-}
-
-const CopyButton = ({
- text,
- section,
- copiedSection,
- onCopy,
- ariaLabel,
-}: {
- text: string;
- section: CopiedSection;
- copiedSection: CopiedSection;
- onCopy: (text: string, section: CopiedSection) => void;
- ariaLabel?: string;
-}) => {
- const isCopied = copiedSection === section;
-
- return (
-
- );
-};
-
-export function LLMErrorDetailModal({
- error,
- isOpen,
- onCloseAction,
-}: LLMErrorDetailModalProps) {
- const [copiedSection, setCopiedSection] = useState(null);
-
- if (!error) {
- return null;
- }
-
- const copyToClipboard = async (text: string, section: CopiedSection) => {
- try {
- await navigator.clipboard.writeText(text);
- setCopiedSection(section);
- toast.success("Copied to clipboard");
- setTimeout(() => setCopiedSection(null), 2000);
- } catch (err) {
- toast.error("Failed to copy", {
- description: err instanceof Error ? err.message : "Unknown error",
- });
- }
- };
-
- const relativeTimeStr = fromNow(error.timestamp);
- const httpSeverity = getHttpStatusSeverity(error.http_status);
-
- const fullErrorInfo = `LLM Error: ${error.error_name}
-${error.error_message ? `\nMessage:\n${error.error_message}` : ""}
-${error.error_stack ? `\nStack Trace:\n${error.error_stack}` : ""}
-
-Context:
-• Model: ${error.model}
-• Provider: ${error.provider}
-• HTTP Status: ${error.http_status ?? "N/A"}
-• Latency: ${formatDuration(error.duration_ms)}
-• Time: ${formatDateTimeSeconds(error.timestamp)}`;
-
- const contextRows = [
- {
- key: "model",
- label: "Model",
- value: error.model,
- icon: (
-
- ),
- },
- {
- key: "provider",
- label: "Provider",
- value: error.provider,
- icon: (
-
- ),
- },
- {
- key: "http_status",
- label: "HTTP Status",
- value: error.http_status ? String(error.http_status) : "—",
- icon: (
-
- ),
- },
- {
- key: "latency",
- label: "Latency",
- value: formatDuration(error.duration_ms),
- icon: (
-
- ),
- },
- ];
-
- return (
-
-
-
-
-
-
-
-
-
-
- {error.error_name}
-
- {httpSeverity && (
-
- {error.http_status}
-
- )}
-
-
-
- {relativeTimeStr}
- •
-
- {formatDateTimeSeconds(error.timestamp)}
-
-
-
-
-
-
-
- {/* Quick Actions */}
-
-
- Quick actions
-
-
-
- {error.error_message && (
-
- )}
- {error.error_stack && (
-
- )}
-
-
-
- {/* Error Name */}
-
-
-
-
-
- Error Name
-
-
-
-
-
-
- {error.error_name}
-
-
-
-
- {/* Error Message */}
- {error.error_message && (
-
-
-
-
-
- Error Message
-
-
-
-
-
-
- {error.error_message}
-
-
-
- )}
-
- {/* Stack Trace */}
- {error.error_stack && (
-
-
-
-
-
-
-
- Stack Trace
-
-
-
-
-
-
- {error.error_stack}
-
-
-
-
-
-
-
-
-
- )}
-
- {/* Context */}
-
- Context
-
- {contextRows.map((row, index) => (
-
0 ? "border-t" : ""}`}
- key={row.key}
- >
-
- {row.icon}
-
-
- {row.label}
-
-
- {row.value}
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/dashboard/app/(main)/llm/errors/page.tsx b/apps/dashboard/app/(main)/llm/errors/page.tsx
deleted file mode 100644
index a86f5821e..000000000
--- a/apps/dashboard/app/(main)/llm/errors/page.tsx
+++ /dev/null
@@ -1,455 +0,0 @@
-"use client";
-
-import { ClockIcon, RobotIcon, WarningIcon } from "@phosphor-icons/react";
-import type { ColumnDef } from "@tanstack/react-table";
-import { useCallback, useMemo, useState } from "react";
-import { SimpleMetricsChart } from "@/components/charts/simple-metrics-chart";
-import { DataTable, type TabConfig } from "@/components/table/data-table";
-import { Badge } from "@/components/ui/badge";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
-import { useBatchDynamicQuery } from "@/hooks/use-dynamic-query";
-import dayjs from "@/lib/dayjs";
-import { useLLMPageContext } from "../_components/llm-page-context";
-import {
- formatDuration,
- formatNumber,
- type LLMErrorBreakdownData,
- type LLMErrorSeriesData,
- type LLMHttpStatusData,
- type LLMRecentErrorData,
-} from "../_components/llm-types";
-import { LLMErrorDetailModal } from "./_components/llm-error-detail-modal";
-
-function getRelativeTime(timestamp: string): string {
- const date = dayjs(timestamp);
- if (!date.isValid()) {
- return "";
- }
- return date.fromNow();
-}
-
-function formatDateTimeSeconds(timestamp: string): string {
- const date = new Date(timestamp);
- return date.toLocaleString(undefined, {
- month: "short",
- day: "numeric",
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- });
-}
-
-function getHttpStatusSeverity(
- status?: number
-): "high" | "medium" | "low" | null {
- if (!status) {
- return null;
- }
- if (status >= 500) {
- return "high";
- }
- if (status >= 400) {
- return "medium";
- }
- return "low";
-}
-
-function getSeverityColor(severity: "high" | "medium" | "low"): string {
- const colors = {
- high: "bg-destructive/10 text-destructive border-destructive/20",
- medium: "bg-chart-2/10 text-chart-2 border-chart-2/20",
- low: "bg-chart-3/10 text-chart-3 border-chart-3/20",
- };
- return colors[severity];
-}
-
-const SeverityDot = ({ severity }: { severity: "high" | "medium" | "low" }) => {
- const colors = {
- high: "bg-destructive",
- medium: "bg-chart-2",
- low: "bg-chart-3",
- };
- return (
-
- );
-};
-
-const errorBreakdownColumns: ColumnDef<
- LLMErrorBreakdownData & { name: string }
->[] = [
- {
- id: "error_name",
- accessorKey: "error_name",
- header: "Error",
- cell: ({ row }) => (
-
-
-
- {row.original.error_name}
-
-
- ),
- },
- {
- id: "error_count",
- accessorKey: "error_count",
- header: "Count",
- cell: ({ row }) => (
-
- {formatNumber(row.original.error_count)}
-
- ),
- },
- {
- id: "sample_message",
- accessorKey: "sample_message",
- header: "Sample Message",
- cell: ({ row }) => (
-
-
-
- {row.original.sample_message || "—"}
-
-
- {row.original.sample_message && (
-
-
- {row.original.sample_message}
-
-
- )}
-
- ),
- },
-];
-
-const httpStatusColumns: ColumnDef[] = [
- {
- id: "http_status",
- accessorKey: "http_status",
- header: "Status Code",
- cell: ({ row }) => {
- const status = row.original.http_status;
- const severity = getHttpStatusSeverity(status);
- return (
-
- {severity && }
- {status}
-
- );
- },
- },
- {
- id: "calls",
- accessorKey: "calls",
- header: "Count",
- cell: ({ row }) => (
- {formatNumber(row.original.calls)}
- ),
- },
-];
-
-export default function LLMErrorsPage() {
- const { queryOptions, dateRange, hasQueryId, isLoadingOrg } =
- useLLMPageContext();
-
- const [selectedError, setSelectedError] = useState(
- null
- );
- const [isModalOpen, setIsModalOpen] = useState(false);
-
- const handleViewError = useCallback((error: LLMRecentErrorData) => {
- setSelectedError(error);
- setIsModalOpen(true);
- }, []);
-
- const handleCloseModal = useCallback(() => {
- setIsModalOpen(false);
- setSelectedError(null);
- }, []);
-
- const queries = useMemo(
- () => [
- { id: "llm-error-series", parameters: ["llm_error_rate_time_series"] },
- { id: "llm-errors", parameters: ["llm_error_breakdown"] },
- { id: "llm-status", parameters: ["llm_http_status_breakdown"] },
- { id: "llm-recent-errors", parameters: ["llm_recent_errors"] },
- ],
- []
- );
-
- const { isLoading, getDataForQuery } = useBatchDynamicQuery(
- queryOptions,
- dateRange,
- queries,
- { enabled: hasQueryId }
- );
-
- const errorSeries =
- (getDataForQuery(
- "llm-error-series",
- "llm_error_rate_time_series"
- ) as LLMErrorSeriesData[]) ?? [];
-
- const errorBreakdown =
- (getDataForQuery(
- "llm-errors",
- "llm_error_breakdown"
- ) as LLMErrorBreakdownData[]) ?? [];
-
- const statusBreakdown =
- (getDataForQuery(
- "llm-status",
- "llm_http_status_breakdown"
- ) as LLMHttpStatusData[]) ?? [];
-
- const recentErrors =
- (getDataForQuery(
- "llm-recent-errors",
- "llm_recent_errors"
- ) as LLMRecentErrorData[]) ?? [];
-
- const chartData = useMemo(
- () =>
- errorSeries.map((row) => ({
- date: row.date,
- errors: row.error_count ?? 0,
- rate: (row.error_rate ?? 0) * 100,
- })),
- [errorSeries]
- );
-
- const breakdownTabs = useMemo(() => {
- const tabs: TabConfig<
- (LLMErrorBreakdownData | LLMHttpStatusData) & { name: string }
- >[] = [];
-
- if (errorBreakdown.length > 0) {
- tabs.push({
- id: "errors",
- label: "By Error Type",
- data: errorBreakdown.map((row) => ({ ...row, name: row.error_name })),
- columns: errorBreakdownColumns,
- });
- }
-
- if (statusBreakdown.length > 0) {
- tabs.push({
- id: "http-status",
- label: "By HTTP Status",
- data: statusBreakdown.map((row) => ({
- ...row,
- name: String(row.http_status),
- })),
- columns: httpStatusColumns,
- });
- }
-
- return tabs;
- }, [errorBreakdown, statusBreakdown]);
-
- const recentErrorsData = useMemo(
- () => recentErrors.map((row) => ({ ...row, name: row.error_name })),
- [recentErrors]
- );
-
- const recentErrorColumns: ColumnDef[] =
- useMemo(
- () => [
- {
- id: "severity",
- accessorKey: "http_status",
- header: "",
- size: 32,
- cell: ({ row }) => {
- const severity = getHttpStatusSeverity(row.original.http_status);
- return (
-
- {severity ? (
-
- ) : (
-
- )}
-
- );
- },
- },
- {
- id: "error",
- accessorKey: "error_name",
- header: "Error",
- size: 400,
- cell: ({ row }) => {
- const error = row.original;
-
- return (
-
-
-
-
-
-
- {error.error_name}
-
-
-
- );
- },
- },
- {
- id: "model",
- accessorKey: "model",
- header: "Model",
- size: 160,
- cell: ({ row }) => (
-
-
-
-
-
- {row.original.model}
-
-
-
-
-
- Model: {row.original.model}
- Provider: {row.original.provider}
-
-
-
- ),
- },
- {
- id: "http_status",
- accessorKey: "http_status",
- header: "Status",
- size: 80,
- cell: ({ row }) => {
- const status = row.original.http_status;
- if (!status) {
- return —;
- }
- const severity = getHttpStatusSeverity(status);
- return (
-
- {status}
-
- );
- },
- },
- {
- id: "duration_ms",
- accessorKey: "duration_ms",
- header: "Latency",
- cell: ({ row }) => (
-
- {formatDuration(row.original.duration_ms)}
-
- ),
- },
- {
- id: "timestamp",
- accessorKey: "timestamp",
- header: "Time",
- cell: ({ row }) => {
- const time = row.original.timestamp;
- const relative = getRelativeTime(time);
- const full = formatDateTimeSeconds(time);
-
- return (
-
-
-
-
-
- {relative}
-
-
-
-
- {full}
-
-
- );
- },
- },
- ],
- []
- );
-
- const isPageLoading = isLoadingOrg || isLoading;
-
- return (
-
- formatNumber(v),
- },
- {
- key: "rate",
- label: "Error Rate",
- color: "#f97316",
- formatValue: (v) => {
- const safeValue = v == null || Number.isNaN(v) ? 0 : v;
- return `${safeValue.toFixed(1)}%`;
- },
- },
- ]}
- title="Error Trends"
- />
-
- {breakdownTabs.length > 0 && (
-
- )}
-
- handleViewError(row)}
- title="Recent Errors"
- />
-
- {selectedError && (
-
- )}
-
- );
-}
diff --git a/apps/dashboard/app/(main)/llm/layout.tsx b/apps/dashboard/app/(main)/llm/layout.tsx
deleted file mode 100644
index 4941fa7ae..000000000
--- a/apps/dashboard/app/(main)/llm/layout.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-"use client";
-
-import { BrainIcon, WarningIcon } from "@phosphor-icons/react/dist/ssr";
-import { PageNavigation } from "@/components/layout/page-navigation";
-import { LLMPageProvider } from "./_components/llm-page-context";
-import { LLMPageHeader } from "./_components/llm-page-header";
-
-export default function LlmAnalyticsLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- const basePath = "/llm";
-
- return (
-
-
-
- );
-}
diff --git a/apps/dashboard/app/(main)/llm/page.tsx b/apps/dashboard/app/(main)/llm/page.tsx
deleted file mode 100644
index 5fb74d8d7..000000000
--- a/apps/dashboard/app/(main)/llm/page.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-"use client";
-
-import { useEffect, useMemo } from "react";
-import { useBatchDynamicQuery } from "@/hooks/use-dynamic-query";
-import { LLMPrimaryKpis, LLMSecondaryKpis } from "./_components/llm-kpis";
-import { useLLMPageContext } from "./_components/llm-page-context";
-import { LLMTables } from "./_components/llm-tables";
-import { LLMTimeSeriesChart } from "./_components/llm-time-series-chart";
-import type {
- LLMKpiData,
- LLMModelData,
- LLMTimeSeriesData,
- LLMToolData,
-} from "./_components/llm-types";
-
-export default function LLMAnalyticsPage() {
- const {
- queryOptions,
- dateRange,
- hasQueryId,
- isLoadingOrg,
- registerRefresh,
- setIsFetching,
- } = useLLMPageContext();
-
- const queries = useMemo(
- () => [
- {
- id: "llm-kpis",
- parameters: ["llm_overview_kpis"],
- limit: 1,
- granularity: dateRange.granularity,
- },
- {
- id: "llm-time-series",
- parameters: ["llm_time_series"],
- limit: 365,
- granularity: dateRange.granularity,
- },
- {
- id: "llm-models",
- parameters: ["llm_model_breakdown"],
- limit: 20,
- granularity: dateRange.granularity,
- },
- {
- id: "llm-tools",
- parameters: ["llm_tool_name_breakdown"],
- limit: 20,
- granularity: dateRange.granularity,
- },
- ],
- [dateRange.granularity]
- );
-
- const { isLoading, getDataForQuery, refetch, isFetching } =
- useBatchDynamicQuery(queryOptions, dateRange, queries, {
- enabled: hasQueryId,
- });
-
- useEffect(() => {
- return registerRefresh(refetch);
- }, [registerRefresh, refetch]);
-
- useEffect(() => {
- setIsFetching(isFetching);
- }, [isFetching, setIsFetching]);
-
- const kpis =
- (getDataForQuery("llm-kpis", "llm_overview_kpis") as LLMKpiData[])?.[0] ||
- null;
- const timeSeries =
- (getDataForQuery(
- "llm-time-series",
- "llm_time_series"
- ) as LLMTimeSeriesData[]) || [];
- const models =
- (getDataForQuery("llm-models", "llm_model_breakdown") as LLMModelData[]) ||
- [];
- const tools =
- (getDataForQuery(
- "llm-tools",
- "llm_tool_name_breakdown"
- ) as LLMToolData[]) || [];
-
- const chartData = useMemo(
- () => ({
- cost: timeSeries.map((d) => ({
- date: d.date,
- value: d.total_cost || 0,
- })),
- calls: timeSeries.map((d) => ({
- date: d.date,
- value: d.total_calls || 0,
- })),
- tokens: timeSeries.map((d) => ({
- date: d.date,
- value: d.total_tokens || 0,
- })),
- latency: timeSeries.map((d) => ({
- date: d.date,
- value: d.avg_duration_ms || 0,
- })),
- }),
- [timeSeries]
- );
-
- const isPageLoading = isLoadingOrg || isLoading;
-
- return (
-
- {/* KPIs */}
-
-
- {/* Time Series Chart */}
-
-
- {/* Tools & Models Tables */}
-
-
- );
-}
diff --git a/apps/dashboard/app/(main)/monitors/[id]/page.tsx b/apps/dashboard/app/(main)/monitors/[id]/page.tsx
index 83f8b5323..f922b3f00 100644
--- a/apps/dashboard/app/(main)/monitors/[id]/page.tsx
+++ b/apps/dashboard/app/(main)/monitors/[id]/page.tsx
@@ -1,5 +1,21 @@
"use client";
+import { ArrowClockwiseIcon } from "@phosphor-icons/react";
+import { ArrowLeftIcon } from "@phosphor-icons/react";
+import { ArrowSquareOutIcon } from "@phosphor-icons/react";
+import { GlobeIcon } from "@phosphor-icons/react";
+import { HeartbeatIcon } from "@phosphor-icons/react";
+import { LightningIcon } from "@phosphor-icons/react";
+import { PauseIcon } from "@phosphor-icons/react";
+import { PencilIcon } from "@phosphor-icons/react";
+import { PlayIcon } from "@phosphor-icons/react";
+import { TrashIcon } from "@phosphor-icons/react";
+import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
+import dynamic from "next/dynamic";
+import Link from "next/link";
+import { useParams, useRouter } from "next/navigation";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { toast } from "sonner";
import { MonitorDetailLoading } from "@/app/(main)/monitors/_components/monitor-detail-loading";
import { PageHeader } from "@/app/(main)/websites/_components/page-header";
import { FaviconImage } from "@/components/analytics/favicon-image";
@@ -25,23 +41,6 @@ import { fromNow, localDayjs } from "@/lib/time";
import { LatencyChartChunkPlaceholder } from "@/lib/uptime/latency-chart-chunk-placeholder";
import { UptimeHeatmap } from "@/lib/uptime/uptime-heatmap";
import { cn } from "@/lib/utils";
-import {
- ArrowClockwiseIcon,
- ArrowLeftIcon,
- ArrowSquareOutIcon,
- GlobeIcon,
- HeartbeatIcon,
- PauseIcon,
- PencilIcon,
- PlayIcon,
- TrashIcon,
-} from "@phosphor-icons/react";
-import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
-import dynamic from "next/dynamic";
-import Link from "next/link";
-import { useParams, useRouter } from "next/navigation";
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { toast } from "sonner";
import {
RecentActivity,
type RecentActivityCheck,
@@ -73,22 +72,24 @@ const granularityLabels: Record = {
};
interface ScheduleData {
- id: string;
- organizationId: string;
- websiteId: string | null;
- url: string;
- name: string | null;
- granularity: string;
+ cacheBust: boolean;
cron: string;
+ granularity: string;
+ id: string;
isPaused: boolean;
isPublic: boolean;
- qstashStatus: string;
jsonParsingConfig?: { enabled: boolean } | null;
+ name: string | null;
+ organizationId: string;
+ qstashStatus: string;
+ timeout: number | null;
+ url: string;
website?: {
id: string;
name: string | null;
domain: string;
} | null;
+ websiteId: string | null;
}
function resolveStatus(check: RecentActivityCheck | undefined) {
@@ -161,7 +162,8 @@ export default function MonitorDetailsPage() {
url: string;
name?: string | null;
granularity: string;
- isPublic?: boolean;
+ timeout?: number | null;
+ cacheBust?: boolean;
jsonParsingConfig?: { enabled: boolean } | null;
} | null>(null);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
@@ -209,6 +211,9 @@ export default function MonitorDetailsPage() {
const deleteMutation = useMutation({
...orpc.uptime.deleteSchedule.mutationOptions(),
});
+ const manualCheckMutation = useMutation({
+ ...orpc.uptime.manualCheck.mutationOptions(),
+ });
const transferMutation = useMutation({
...orpc.uptime.transfer.mutationOptions(),
@@ -328,12 +333,15 @@ export default function MonitorDetailsPage() {
"uptime_response_time_trends"
);
- // --- Pagination effects ---
+ // --- Pagination: reset when filters change (render-time pattern) ---
- useEffect(() => {
+ const paginationResetKey = `${dateRange.start_date}-${dateRange.end_date}-${scheduleId}`;
+ const [prevResetKey, setPrevResetKey] = useState(paginationResetKey);
+ if (prevResetKey !== paginationResetKey) {
+ setPrevResetKey(paginationResetKey);
setRecentChecksPage(1);
setAllRecentChecks([]);
- }, [dateRange, scheduleId]);
+ }
const recentChecksHasNext =
pageRecentChecks.length === RECENT_CHECKS_PAGE_SIZE;
@@ -407,7 +415,8 @@ export default function MonitorDetailsPage() {
url: schedule.url,
name: schedule.name,
granularity: schedule.granularity,
- isPublic: schedule.isPublic,
+ timeout: schedule.timeout,
+ cacheBust: schedule.cacheBust,
jsonParsingConfig: schedule.jsonParsingConfig as {
enabled: boolean;
} | null,
@@ -475,6 +484,26 @@ export default function MonitorDetailsPage() {
setIsRefreshing(false);
};
+ const handleManualCheck = async () => {
+ if (!schedule) {
+ return;
+ }
+ try {
+ await manualCheckMutation.mutateAsync({ scheduleId: schedule.id });
+ toast.success("Check triggered");
+ setTimeout(() => {
+ refetchSchedule();
+ refetchUptimeData();
+ refetchHeatmapData();
+ refetchLatencyData();
+ }, 3000);
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Failed to trigger check";
+ toast.error(errorMessage);
+ }
+ };
+
const handleTransfer = async (targetOrganizationId: string) => {
if (!schedule) {
return;
@@ -568,6 +597,21 @@ export default function MonitorDetailsPage() {
className={isRefreshing ? "animate-spin" : ""}
/>
+