Skip to content
Open
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
99 changes: 99 additions & 0 deletions src/app/api/domains/sync-busuanzi/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { NextRequest } from "next/server";
import { getServerSession } from "@/lib/auth";
import logger from "@/lib/logger";
import { successResponse, ApiErrors } from "@/lib/api-response";
import { forceSyncAllFromBusuanzi } from "@/utils/busuanzi";
import { domainService } from "@/lib/domain-service";
import { db } from "@/db";
import { domains } from "@/db/schema";
import { and, eq } from "drizzle-orm";

/**
* POST handler - Force sync data from Busuanzi for a domain
* This allows users to manually trigger a re-sync of their data from Busuanzi,
* which is useful when the initial sync failed due to Busuanzi service issues.
*/
export async function POST(req: NextRequest) {
try {
const session = await getServerSession();

if (!session || !session.user) {
return ApiErrors.unauthorized();
}

const userId = session.user.id;
if (!userId) {
return ApiErrors.badRequest("User ID not found in session");
}

const data = await req.json();

if (!data.domainName) {
return ApiErrors.badRequest("Domain name is required");
}

// Check if the domain belongs to the user
const domain = await db.query.domains.findFirst({
where: and(eq(domains.name, data.domainName), eq(domains.userId, userId)),
});

if (!domain) {
return ApiErrors.notFound("Domain not found or does not belong to you");
}

if (!domain.verified) {
return ApiErrors.badRequest("Domain must be verified before syncing data");
}

logger.info(`Starting Busuanzi sync for domain: ${domain.name}`, { userId });

// Force sync from Busuanzi
const syncResult = await forceSyncAllFromBusuanzi(domain.name, domain.name);

if (!syncResult.success) {
logger.warn(`Busuanzi sync partially failed for domain: ${domain.name}`, {
syncResult,
userId
});

// Return partial success with details about what failed
return successResponse(
{
synced: false,
domainName: domain.name,
details: {
siteUv: syncResult.siteUv,
sitePv: syncResult.sitePv,
},
},
syncResult.error || "Some data failed to sync from Busuanzi. The service may be temporarily unavailable.",
200
);
}

// Get updated counter data
const counters = await domainService.getCountersForDomain(domain.name);

logger.info(`Busuanzi sync completed for domain: ${domain.name}`, {
userId,
siteUv: syncResult.siteUv?.value,
sitePv: syncResult.sitePv?.value,
});

return successResponse(
{
synced: true,
domainName: domain.name,
counters,
syncedValues: {
siteUv: syncResult.siteUv?.value,
sitePv: syncResult.sitePv?.value,
},
},
"Data synced successfully from Busuanzi"
);
} catch (error) {
logger.error("Error in POST /api/domains/sync-busuanzi", { error });
return ApiErrors.internalError();
}
}
62 changes: 59 additions & 3 deletions src/app/dashboard/analytics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { toast } from "sonner";
import { HomeIcon, ArrowLeft, RefreshCw } from "lucide-react";
import { HomeIcon, ArrowLeft, RefreshCw, Download } from "lucide-react";
import DashboardHeader from "@/components/dashboard/dashboard-header";
import { safeDecodeURIComponent } from "@/utils/url";
import { CounterTable } from "@/components/data-table/counter-table";
Expand Down Expand Up @@ -40,7 +40,8 @@ export default function CountersPage() {
domains: true,
counters: false,
saving: false,
syncing: false
syncing: false,
syncingBusuanzi: false
});
const [monitoredPaths, setMonitoredPaths] = useState<string[]>([]);
const [newPagePath, setNewPagePath] = useState<string>("");
Expand Down Expand Up @@ -278,6 +279,45 @@ export default function CountersPage() {
}
};

// Sync data from Busuanzi (force re-sync)
const syncFromBusuanzi = async () => {
if (!selectedDomain) return;

try {
setLoading(prev => ({ ...prev, syncingBusuanzi: true }));

const response = await fetch("/api/domains/sync-busuanzi", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
domainName: selectedDomain.name,
}),
});

const resData = await response.json();

if (resData.status === "success" && resData.data?.synced) {
toast.success("Data synced from Busuanzi successfully");
// Reload domain data to reflect the new values
await loadDomainData(selectedDomain.name);
} else if (resData.status === "success" && !resData.data?.synced) {
// Partial failure
toast.warning(resData.message || "Some data failed to sync from Busuanzi");
// Still reload to show any data that was synced
await loadDomainData(selectedDomain.name);
} else {
toast.error(resData.message || "Failed to sync from Busuanzi");
}
} catch (error) {
console.error("Error syncing from Busuanzi:", error);
toast.error("Failed to sync from Busuanzi. Please try again later.");
} finally {
setLoading(prev => ({ ...prev, syncingBusuanzi: false }));
}
};

// Dummy function for the CounterTable component
const handleUpdatePageViewDummy = async (path: string): Promise<void> => {
// This function is not needed in our simplified approach
Expand Down Expand Up @@ -372,7 +412,23 @@ export default function CountersPage() {

{/* Site-wide analytics */}
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-4">SITE OVERVIEW</h3>
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-muted-foreground">SITE OVERVIEW</h3>
<Button
variant="outline"
size="sm"
onClick={syncFromBusuanzi}
disabled={loading.syncingBusuanzi}
title="Force re-sync data from Busuanzi service"
>
{loading.syncingBusuanzi ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
{loading.syncingBusuanzi ? "Syncing..." : "Sync from Busuanzi"}
</Button>
</div>

{loading.counters ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
Expand Down
83 changes: 83 additions & 0 deletions src/utils/busuanzi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,92 @@ function notifyBusuanziService(hostOriginal: string, pathOriginal: string) {
});
}

/**
* Force sync site UV from Busuanzi service (overwrites existing data)
* @param hostSanitized The sanitized hostname
* @param hostOriginal The original hostname
* @returns Object with success status and the synced value or error message
*/
async function forceSyncBusuanziSiteUV(hostSanitized: string, hostOriginal: string): Promise<{ success: boolean; value?: number; error?: string }> {
const headers = {
Referer: `https://${hostOriginal}/`,
Cookie: "busuanziId=89D15D1F66D2494F91FB315545BF9C2A",
};

const data = await fetchBusuanziData(BUSUANZI_URL, headers);
if (data) {
const siteUv = data.site_uv || 0;
await kv.set(`uv:baseline:${hostSanitized}`, siteUv, { ex: EXPIRATION_TIME });
logger.info(`Force sync: UV data retrieved and stored for ${hostSanitized}: ${siteUv}`);
return { success: true, value: siteUv };
} else {
logger.error(`Force sync: Failed to retrieve UV data from Busuanzi for ${hostSanitized}`);
return { success: false, error: "Failed to retrieve data from Busuanzi service. The service may be temporarily unavailable." };
}
}

/**
* Force sync site PV from Busuanzi service (overwrites existing data)
* @param hostSanitized The sanitized hostname
* @param hostOriginal The original hostname
* @returns Object with success status and the synced value or error message
*/
async function forceSyncBusuanziSitePV(hostSanitized: string, hostOriginal: string): Promise<{ success: boolean; value?: number; error?: string }> {
const headers = {
Referer: `https://${hostOriginal}/`,
Cookie: "busuanziId=89D15D1F66D2494F91FB315545BF9C2A",
};

const data = await fetchBusuanziData(BUSUANZI_URL, headers);
if (data) {
const sitePv = data.site_pv || 0;
await kv.set(`pv:site:${hostSanitized}`, sitePv, { ex: EXPIRATION_TIME });
logger.info(`Force sync: Site PV data retrieved and stored for ${hostSanitized}: ${sitePv}`);
return { success: true, value: sitePv };
} else {
logger.error(`Force sync: Failed to retrieve Site PV data from Busuanzi for ${hostSanitized}`);
return { success: false, error: "Failed to retrieve data from Busuanzi service. The service may be temporarily unavailable." };
}
}

/**
* Force sync all data (site UV, site PV) from Busuanzi service
* @param hostSanitized The sanitized hostname
* @param hostOriginal The original hostname
* @returns Object with success status and synced values or error messages
*/
async function forceSyncAllFromBusuanzi(hostSanitized: string, hostOriginal: string): Promise<{
success: boolean;
siteUv?: { success: boolean; value?: number; error?: string };
sitePv?: { success: boolean; value?: number; error?: string };
error?: string;
}> {
logger.info(`Force sync: Starting full sync from Busuanzi for ${hostSanitized}`);

// Sync both UV and PV in parallel
const [uvResult, pvResult] = await Promise.all([
forceSyncBusuanziSiteUV(hostSanitized, hostOriginal),
forceSyncBusuanziSitePV(hostSanitized, hostOriginal),
]);

const overallSuccess = uvResult.success && pvResult.success;

logger.info(`Force sync completed for ${hostSanitized}: UV=${uvResult.success}, PV=${pvResult.success}`);

return {
success: overallSuccess,
siteUv: uvResult,
sitePv: pvResult,
error: overallSuccess ? undefined : "Some data failed to sync from Busuanzi",
};
}

export {
fetchBusuanziSiteUV,
fetchBusuanziSitePV,
fetchBusuanziPagePV,
notifyBusuanziService,
forceSyncBusuanziSiteUV,
forceSyncBusuanziSitePV,
forceSyncAllFromBusuanzi,
};