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
19 changes: 8 additions & 11 deletions app/api/acquia/views/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ export async function GET(request: NextRequest) {
const subscriptionUuid = searchParams.get('subscriptionUuid');
const from = searchParams.get('from');
const to = searchParams.get('to');
const resolution = searchParams.get('resolution'); // Get granularity for daily data
/**
console.log('🚀 Views by Application API Route called with params:', {
subscriptionUuid,
from,
to,
resolution
});
*/

if (!subscriptionUuid) {
console.error('❌ Missing required parameter: subscriptionUuid');
return NextResponse.json(
Expand All @@ -22,12 +23,11 @@ export async function GET(request: NextRequest) {
);
}

// Update the API service initialization with better error handling
if (!process.env.ACQUIA_API_KEY || !process.env.ACQUIA_API_SECRET) {
console.error('❌ Missing required environment variables!');
console.error('Available env vars:', Object.keys(process.env).filter(k => k.startsWith('ACQUIA')));
return NextResponse.json(
{
{
error: 'Server configuration error: missing API credentials',
envCheck: {
ACQUIA_API_KEY: process.env.ACQUIA_API_KEY ? `${process.env.ACQUIA_API_KEY.substring(0, 8)}...` : 'missing',
Expand All @@ -41,7 +41,6 @@ export async function GET(request: NextRequest) {
}

try {
// Update the API service initialization
const apiService = new AcquiaApiServiceFixed({
baseUrl: process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api',
authUrl: process.env.ACQUIA_AUTH_BASE_URL || 'https://accounts.acquia.com/api',
Expand All @@ -53,32 +52,30 @@ export async function GET(request: NextRequest) {
// console.log('📈 Views progress:', progress);
});

// console.log('🔧 Using FIXED API Service for views by application (with pagination)');

const data = await apiService.getViewsDataByApplication(
subscriptionUuid,
from || undefined,
to || undefined
to || undefined,
resolution || undefined
);

// console.log('✅ Successfully fetched ALL views by application data, total count:', data.length);

return NextResponse.json({
data,
totalItems: data.length,
message: `Successfully fetched ${data.length} view records across all pages`,
});
} catch (error) {
console.error('❌ API Route Error:', error);

if (error instanceof Error) {
console.error('🔍 Error name:', error.name);
console.error('🔍 Error message:', error.message);
console.error('🔍 Error stack:', error.stack);
}

return NextResponse.json(
{
{
error: 'Failed to fetch views by application data',
details: error instanceof Error ? error.message : 'Unknown error',
},
Expand Down
20 changes: 12 additions & 8 deletions app/api/acquia/visits/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export async function GET(request: NextRequest) {
const subscriptionUuid = searchParams.get('subscriptionUuid');
const from = searchParams.get('from');
const to = searchParams.get('to');
const resolution = searchParams.get('resolution'); // Get granularity for daily data

/**
console.log('🚀 Visits by Application API Route called with params:', {
Expand All @@ -28,7 +29,7 @@ export async function GET(request: NextRequest) {
console.error('❌ Missing required environment variables!');
console.error('Available env vars:', Object.keys(process.env).filter(k => k.startsWith('ACQUIA')));
return NextResponse.json(
{
{
error: 'Server configuration error: missing API credentials',
envCheck: {
ACQUIA_API_KEY: process.env.ACQUIA_API_KEY ? `${process.env.ACQUIA_API_KEY.substring(0, 8)}...` : 'missing',
Expand All @@ -52,29 +53,32 @@ export async function GET(request: NextRequest) {
apiService.setProgressCallback((progress) => {
// console.log('📊 Visits progress:', progress);
});

// console.log('🔧 Using FIXED API Service for visits by application (with pagination)');

const data = await apiService.getVisitsDataByApplication(subscriptionUuid, from || undefined, to || undefined);

const data = await apiService.getVisitsDataByApplication(
subscriptionUuid,
from || undefined,
to || undefined,
resolution || undefined
);

// console.log('✅ Successfully fetched ALL visits by application data, total count:', data.length);

return NextResponse.json({
data,
totalItems: data.length,
message: `Successfully fetched ${data.length} visit records across all pages`
});
} catch (error) {
console.error('❌ API Route Error:', error);

if (error instanceof Error) {
console.error('🔍 Error name:', error.name);
console.error('🔍 Error message:', error.message);
console.error('🔍 Error stack:', error.stack);
}

return NextResponse.json(
{
{
error: 'Failed to fetch visits by application data',
details: error instanceof Error ? error.message : 'Unknown error'
},
Expand Down
159 changes: 119 additions & 40 deletions app/applications/[uuid]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,31 @@

import React, { useState, useEffect } from 'react';
import CountUpTimer from '@/components/CountUpTimer';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';

const DEFAULT_SUBSCRIPTION_UUID = process.env.NEXT_PUBLIC_ACQUIA_SUBSCRIPTION_UUID || '';
interface PageProps {
params: { uuid: string };

// Define a type for our chart data points
interface DailyDataPoint {
date: string;
value: number;
}

// Define a type for the expected API response structure
interface AcquiaApiResponse {
data?: Array<{
applicationUuid: string;
date: string;
views?: number;
visits?: number;
}>;
}

// Use 'any' in the signature to satisfy the Next.js build process for client components.
export default function ApplicationDetailPage({ params }: any) {
// Re-introduce the type inside the component for type safety.
const typedParams: { uuid: string } = params;

const [subscriptionUuid, setSubscriptionUuid] = useState(DEFAULT_SUBSCRIPTION_UUID);
const [from, setFrom] = useState('');
const [to, setTo] = useState('');
Expand All @@ -21,6 +39,8 @@ export default function ApplicationDetailPage({ params }: any) {
const [viewsPct, setViewsPct] = useState(0);
const [visitsPct, setVisitsPct] = useState(0);
const [error, setError] = useState<string | null>(null);
const [dailyViews, setDailyViews] = useState<DailyDataPoint[]>([]);
const [dailyVisits, setDailyVisits] = useState<DailyDataPoint[]>([]);

// Fetch application name on mount or when subscriptionUuid changes
useEffect(() => {
Expand All @@ -29,7 +49,7 @@ export default function ApplicationDetailPage({ params }: any) {
setLoadingStep('Fetching application info...');
const res = await fetch(`/api/acquia/applications?subscriptionUuid=${subscriptionUuid}`);
const apps = await res.json();
const app = Array.isArray(apps) ? apps.find((a: any) => a.uuid === params.uuid) : null;
const app = Array.isArray(apps.data) ? apps.data.find((a: any) => a.uuid === typedParams.uuid) : null;
setAppName(app ? app.name : '');
} catch {
setAppName('');
Expand All @@ -38,7 +58,7 @@ export default function ApplicationDetailPage({ params }: any) {
}
};
if (subscriptionUuid) fetchAppName();
}, [subscriptionUuid, params.uuid]);
}, [subscriptionUuid, typedParams.uuid]);

const fetchAppDetail = async () => {
setLoading(true);
Expand All @@ -51,45 +71,62 @@ export default function ApplicationDetailPage({ params }: any) {
if (subscriptionUuid) paramsObj.subscriptionUuid = subscriptionUuid;
if (from) paramsObj.from = from;
if (to) paramsObj.to = to;
const query = new URLSearchParams(paramsObj).toString();

const dailyQuery = new URLSearchParams({ ...paramsObj, resolution: 'day' }).toString();

setLoadingStep('Fetching views and visits...');
const [viewsRes, visitsRes] = await Promise.all([
fetch(`/api/acquia/views?${query}`),
fetch(`/api/acquia/visits?${query}`),
const [dailyViewsRes, dailyVisitsRes] = await Promise.all([
fetch(`/api/acquia/views?${dailyQuery}`),
fetch(`/api/acquia/visits?${dailyQuery}`),
]);
const [viewsRaw, visitsRaw] = await Promise.all([
viewsRes.ok ? viewsRes.json() : [],
visitsRes.ok ? visitsRes.json() : [],

const [dailyViewsRaw, dailyVisitsRaw]: [AcquiaApiResponse, AcquiaApiResponse] = await Promise.all([
dailyViewsRes.ok ? dailyViewsRes.json() : {},
dailyVisitsRes.ok ? dailyVisitsRes.json() : {},
]);
const viewsArr = Array.isArray(viewsRaw)
? viewsRaw
: viewsRaw && Array.isArray(viewsRaw.data)
? viewsRaw.data
: [];
const visitsArr = Array.isArray(visitsRaw)
? visitsRaw
: visitsRaw && Array.isArray(visitsRaw.data)
? visitsRaw.data
: [];
const appViewsTotal = viewsArr
.filter((v: any) => v.uuid === params.uuid || v.applicationUuid === params.uuid)
.reduce((sum: number, v: any) => sum + (v.views || 0), 0);

const appVisitsTotal = visitsArr
.filter((v: any) => v.uuid === params.uuid || v.applicationUuid === params.uuid)
.reduce((sum: number, v: any) => sum + (v.visits || 0), 0);

const totalViews = viewsArr.reduce((sum: number, v: any) => sum + (v.views || 0), 0);
const totalVisits = visitsArr.reduce((sum: number, v: any) => sum + (v.visits || 0), 0);

setViews(appViewsTotal);
setVisits(appVisitsTotal);
setViewsPct(totalViews ? (appViewsTotal / totalViews) * 100 : 0);
setVisitsPct(totalVisits ? (appVisitsTotal / totalVisits) * 100 : 0);

// Helper to process and aggregate daily data with proper types
const processDailyData = (rawData: AcquiaApiResponse, metric: 'views' | 'visits'): DailyDataPoint[] => {
const dailyMap = new Map<string, number>();
const dataArray = rawData.data || [];

const appData = dataArray.filter((d) => d.applicationUuid === typedParams.uuid);

for (const record of appData) {
const date = record.date.split('T')[0];
const value = record[metric] || 0;
dailyMap.set(date, (dailyMap.get(date) || 0) + value);
}

return Array.from(dailyMap.entries())
.map(([date, value]) => ({ date, value }))
.sort((a, b) => a.date.localeCompare(b.date));
};

const processedDailyViews = processDailyData(dailyViewsRaw, 'views');
const processedDailyVisits = processDailyData(dailyVisitsRaw, 'visits');

setDailyViews(processedDailyViews);
setDailyVisits(processedDailyVisits);

const appTotalViews = processedDailyViews.reduce((sum, day) => sum + day.value, 0);
const appTotalVisits = processedDailyVisits.reduce((sum, day) => sum + day.value, 0);

const overallViewsData = dailyViewsRaw.data || [];
const overallVisitsData = dailyVisitsRaw.data || [];

const overallTotalViews = overallViewsData.reduce((sum, v) => sum + (v.views || 0), 0);
const overallTotalVisits = overallVisitsData.reduce((sum, v) => sum + (v.visits || 0), 0);

setViews(appTotalViews);
setVisits(appTotalVisits);
setViewsPct(overallTotalViews > 0 ? (appTotalViews / overallTotalViews) * 100 : 0);
setVisitsPct(overallTotalVisits > 0 ? (appTotalVisits / overallTotalVisits) * 100 : 0);

setLoadingStep('Complete!');
} catch (err) {
setError('Failed to fetch application details.');
console.error(err);
} finally {
const endTime = Date.now();
setElapsedTime((endTime - startTime) / 1000);
Expand All @@ -105,7 +142,7 @@ export default function ApplicationDetailPage({ params }: any) {
color: 'var(--stanford-black)',
}}>
<h1 className="text-2xl font-bold mb-6">
Views and Visits Data for {appName ? appName : <span className="font-mono">{params.uuid}</span>}
Views and Visits Data for {appName ? appName : <span className="font-mono">{typedParams.uuid}</span>}
</h1>
<section className="mb-8 max-w-xl mx-auto bg-white rounded-lg shadow-md p-6 border-2" style={{ borderColor: 'var(--stanford-cardinal)' }}>
<form>
Expand Down Expand Up @@ -208,15 +245,15 @@ export default function ApplicationDetailPage({ params }: any) {
{error && (
<div className="mb-4 text-red-600">{error}</div>
)}
{!appName ? (
<div>No application found with UUID: <span className="font-mono">{params.uuid}</span></div>
{!appName && !loading ? (
<div>No application found with UUID: <span className="font-mono">{typedParams.uuid}</span></div>
) : (
<div>
<div className="mb-4">
<strong>Name:</strong> {appName}
</div>
<div className="mb-4">
<strong>UUID:</strong> <span className="font-mono">{params.uuid}</span>
<strong>UUID:</strong> <span className="font-mono">{typedParams.uuid}</span>
</div>
<div className="mb-4">
<strong>Views{from && to ? ` (${from} to ${to})` : ''}:</strong> {views.toLocaleString()} ({viewsPct.toFixed(1)}%)
Expand All @@ -226,6 +263,48 @@ export default function ApplicationDetailPage({ params }: any) {
</div>
</div>
)}

{/* Data Display Section */}
{!loading && (views > 0 || visits > 0) && (
<section className="mt-8">
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mt-8">
<div className="p-4 rounded-lg shadow-md" style={{ backgroundColor: '#F9F6F2' }}>
<h4 className="text-lg font-semibold mb-4 text-center" style={{ color: 'var(--stanford-cardinal)' }}>Daily Views</h4>
<ResponsiveContainer width="100%" height={300}>
<LineChart
data={dailyViews}
margin={{ top: 5, right: 20, left: 30, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" fontSize={12} />
<YAxis tickFormatter={(value) => new Intl.NumberFormat('en-US').format(value as number)} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="value" name="Views" stroke="#8C1515" strokeWidth={2} dot={true} />
</LineChart>
</ResponsiveContainer>
</div>

<div className="p-4 rounded-lg shadow-md" style={{ backgroundColor: '#F9F6F2' }}>
<h4 className="text-lg font-semibold mb-4 text-center" style={{ color: 'var(--stanford-cardinal)' }}>Daily Visits</h4>
<ResponsiveContainer width="100%" height={300}>
<LineChart
data={dailyVisits}
margin={{ top: 5, right: 20, left: 30, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" fontSize={12} />
<YAxis tickFormatter={(value) => new Intl.NumberFormat('en-US').format(value as number)} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="value" name="Visits" stroke="#B83A4B" strokeWidth={2} dot={true} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
</section>
)}
</div>
);
}
Loading