diff --git a/src/app/api/domains/sync-busuanzi/route.ts b/src/app/api/domains/sync-busuanzi/route.ts new file mode 100644 index 0000000..03dbf17 --- /dev/null +++ b/src/app/api/domains/sync-busuanzi/route.ts @@ -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(); + } +} diff --git a/src/app/dashboard/analytics/page.tsx b/src/app/dashboard/analytics/page.tsx index f2df0fc..5cbfc6a 100644 --- a/src/app/dashboard/analytics/page.tsx +++ b/src/app/dashboard/analytics/page.tsx @@ -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"; @@ -40,7 +40,8 @@ export default function CountersPage() { domains: true, counters: false, saving: false, - syncing: false + syncing: false, + syncingBusuanzi: false }); const [monitoredPaths, setMonitoredPaths] = useState([]); const [newPagePath, setNewPagePath] = useState(""); @@ -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 => { // This function is not needed in our simplified approach @@ -372,7 +412,23 @@ export default function CountersPage() { {/* Site-wide analytics */}
-

SITE OVERVIEW

+
+

SITE OVERVIEW

+ +
{loading.counters ? (
diff --git a/src/utils/busuanzi.ts b/src/utils/busuanzi.ts index 6b76951..c7e4de3 100644 --- a/src/utils/busuanzi.ts +++ b/src/utils/busuanzi.ts @@ -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, };