---
import Layout from '../layouts/Layout.astro';
// Server-side data fetching
let dashboardData = null;
let error = null;
let accountError = null;
let userEmail = null;
// Handle OAuth token from URL parameter
const urlToken = Astro.url.searchParams.get('token');
if (urlToken) {
// Set the token as a cookie for server-side auth
Astro.cookies.set('auth_token', urlToken, {
httpOnly: true,
secure: import.meta.env.PROD,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 7 days
});
// Redirect to clean URL without token parameter
const cleanUrl = new URL(Astro.url);
cleanUrl.searchParams.delete('token');
return Astro.redirect(cleanUrl.pathname + cleanUrl.search);
}
// Get auth context from middleware
const authContext = Astro.locals.auth;
// Only fetch data if user is authenticated
if (authContext.isAuthenticated) {
try {
// Always use container service name for internal communication
const baseUrl = 'http://vultr-backend:8000';
// First, get user information to check account type
const userResponse = await fetch(`${baseUrl}/api/auth/me`, {
headers: {
'Authorization': `Bearer ${Astro.cookies.get('auth_token')?.value}`,
'Content-Type': 'application/json'
}
});
if (userResponse.ok) {
const userData = await userResponse.json();
userEmail = userData.email;
// Check if this is a GitHub account (simple heuristic)
const isGitHubAccount = userData.provider === 'github' ||
userData.github_id ||
userData.login_type === 'github';
if (!isGitHubAccount) {
accountError = 'github_required';
}
}
// Try to fetch dashboard data
const response = await fetch(`${baseUrl}/api/collections`, {
headers: {
'Authorization': `Bearer ${Astro.cookies.get('auth_token')?.value}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
// If collections endpoint works, try dashboard overview
const dashboardResponse = await fetch(`${baseUrl}/api/dashboard/overview`, {
headers: {
'Authorization': `Bearer ${Astro.cookies.get('auth_token')?.value}`,
'Content-Type': 'application/json'
}
});
if (dashboardResponse.ok) {
dashboardData = await dashboardResponse.json();
} else {
// Dashboard overview might not exist, that's okay
dashboardData = { summary: { total_collections: 0, active_operations: 0, pending_approvals: 0, recent_activity_count: 0 } };
}
} else if (response.status === 500) {
// Database error - check if it's the enum issue
const errorText = await response.text().catch(() => '');
if (errorText.includes('projectrole') || errorText.includes('does not exist')) {
accountError = 'setup_required';
} else {
accountError = 'github_required';
}
} else if (response.status === 403) {
accountError = 'github_required';
} else {
console.error('Failed to fetch dashboard data:', response.status, response.statusText);
error = 'Unable to load your dashboard. Please try signing in again.';
}
} catch (err) {
console.error('Error fetching dashboard data:', err);
// Network errors might indicate the account isn't properly set up
accountError = 'setup_required';
}
}
// Provide fallback data structure if no data received
const overview = dashboardData?.summary || {
total_collections: 0,
active_operations: 0,
pending_approvals: 0,
recent_activity_count: 0
};
const recentActivity = dashboardData?.recent_activity || [];
const environmentStats = dashboardData?.collections_by_environment || {};
const statusStats = dashboardData?.collections_by_status || {};
---
<Layout title="Dashboard - Service Collections">
<div class="py-10">
<header>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold leading-tight text-gray-900">Dashboard</h1>
<p class="mt-2 text-sm text-gray-600">
Overview of your Service Collections and recent activity
</p>
</div>
</header>
<main>
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
{accountError === 'github_required' && (
<div class="mt-8 rounded-md bg-yellow-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-6-6.75h12a2.25 2.25 0 110 4.5h-12a2.25 2.25 0 110-4.5z" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">GitHub Account Required</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>The email address <strong>{userEmail}</strong> is not associated with a GitHub account.</p>
<p class="mt-1">This application requires a GitHub account for authentication. Please:</p>
<ul class="mt-2 list-disc list-inside space-y-1">
<li>Sign out and use a different GitHub account, or</li>
<li>Create a GitHub account using this email address</li>
</ul>
</div>
<div class="mt-4">
<a href="/login"
class="text-sm bg-yellow-100 hover:bg-yellow-200 text-yellow-800 py-2 px-3 rounded-md border border-yellow-200">
Sign in with different account
</a>
</div>
</div>
</div>
</div>
)}
{accountError === 'setup_required' && (
<div class="mt-8 rounded-md bg-blue-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-blue-800">Account Setup in Progress</h3>
<div class="mt-2 text-sm text-blue-700">
<p>Your account is being configured for first-time access.</p>
<p class="mt-1">This usually takes a few moments. Please:</p>
<ul class="mt-2 list-disc list-inside space-y-1">
<li>Wait a moment and refresh the page, or</li>
<li>Contact your administrator if this persists</li>
</ul>
</div>
<div class="mt-4 space-x-3">
<button onclick="window.location.reload()"
class="text-sm bg-blue-100 hover:bg-blue-200 text-blue-800 py-2 px-3 rounded-md border border-blue-200">
Refresh page
</button>
<a href="/login"
class="text-sm bg-white hover:bg-gray-50 text-blue-800 py-2 px-3 rounded-md border border-blue-200">
Sign in again
</a>
</div>
</div>
</div>
</div>
)}
{error && !accountError && (
<div class="mt-8 rounded-md bg-red-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Unable to Load Dashboard</h3>
<div class="mt-2 text-sm text-red-700">{error}</div>
<div class="mt-4">
<button onclick="window.location.reload()"
class="text-sm bg-red-100 hover:bg-red-200 text-red-800 py-2 px-3 rounded-md border border-red-200">
Try again
</button>
</div>
</div>
</div>
</div>
)}
<!-- Dashboard Content - Only show if no account errors -->
{!accountError && (
<>
<!-- Overview Cards -->
<div class="mt-8">
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
<!-- Total Collections -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Collections</dt>
<dd class="text-lg font-medium text-gray-900">{overview.total_collections}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<a href="/collections"
onclick="navigateWithTransition('/collections', event)"
class="font-medium text-primary-700 hover:text-primary-900">View all</a>
</div>
</div>
</div>
<!-- Active Operations -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Active Operations</dt>
<dd class="text-lg font-medium text-gray-900">{overview.active_operations}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<a href="/operations"
onclick="navigateWithTransition('/operations', event)"
class="font-medium text-primary-700 hover:text-primary-900">View all</a>
</div>
</div>
</div>
<!-- Pending Approvals -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 01-1.043 3.296 3.745 3.745 0 01-3.296 1.043A3.745 3.745 0 0112 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 01-3.296-1.043 3.745 3.745 0 01-1.043-3.296A3.745 3.745 0 013 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 011.043-3.296 3.746 3.746 0 013.296-1.043A3.746 3.746 0 0112 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 013.296 1.043 3.746 3.746 0 011.043 3.296A3.745 3.745 0 0121 12z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Pending Approvals</dt>
<dd class="text-lg font-medium text-gray-900">{overview.pending_approvals}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<a href="/approvals"
onclick="navigateWithTransition('/approvals', event)"
class="font-medium text-primary-700 hover:text-primary-900">View all</a>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Recent Activity</dt>
<dd class="text-lg font-medium text-gray-900">{overview.recent_activity_count}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<a href="/activity"
onclick="navigateWithTransition('/activity', event)"
class="font-medium text-primary-700 hover:text-primary-900">View all</a>
</div>
</div>
</div>
</div>
</div>
<!-- Environment Distribution -->
{Object.keys(environmentStats).length > 0 && (
<div class="mt-8">
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Collections by Environment</h3>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{Object.entries(environmentStats).map(([environment, count]) => (
<div class="text-center">
<div class="text-2xl font-bold text-gray-900">{count}</div>
<div class="text-sm text-gray-500 capitalize">{environment}</div>
</div>
))}
</div>
</div>
</div>
</div>
)}
<!-- Status Distribution -->
{Object.keys(statusStats).length > 0 && (
<div class="mt-8">
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Collections by Status</h3>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{Object.entries(statusStats).map(([status, count]) => (
<div class="text-center">
<div class="text-2xl font-bold text-gray-900">{count}</div>
<div class={`text-sm capitalize ${
status === 'active' ? 'text-green-600' :
status === 'inactive' ? 'text-yellow-600' :
status === 'suspended' ? 'text-red-600' :
'text-gray-500'
}`}>{status}</div>
</div>
))}
</div>
</div>
</div>
</div>
)}
<!-- Recent Activity List -->
{recentActivity.length > 0 && (
<div class="mt-8">
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium text-gray-900">Recent Activity</h3>
<p class="mt-1 text-sm text-gray-500">Latest actions across your service collections</p>
</div>
<ul class="divide-y divide-gray-200">
{recentActivity.slice(0, 10).map((activity) => (
<li class="px-4 py-4 sm:px-6 hover:bg-gray-50">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class={`h-2 w-2 rounded-full ${
activity.severity === 'high' ? 'bg-red-400' :
activity.severity === 'medium' ? 'bg-yellow-400' :
'bg-green-400'
}`}></div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">{activity.action}</p>
<p class="text-sm text-gray-500">{activity.resource_type}: {activity.resource_name || 'N/A'}</p>
</div>
</div>
<div class="text-sm text-gray-500">
{new Date(activity.timestamp).toLocaleString()}
</div>
</div>
</li>
))}
</ul>
{recentActivity.length > 10 && (
<div class="bg-gray-50 px-4 py-3">
<div class="text-sm">
<a href="/activity"
onclick="navigateWithTransition('/activity', event)"
class="font-medium text-primary-700 hover:text-primary-900">
View all {recentActivity.length} activities
</a>
</div>
</div>
)}
</div>
</div>
)}
{/* Empty state when no collections exist */}
{overview.total_collections === 0 && !error && (
<div class="mt-8 text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
</svg>
<h3 class="mt-2 text-sm font-semibold text-gray-900">No service collections</h3>
<p class="mt-1 text-sm text-gray-500">Get started by creating your first service collection.</p>
<div class="mt-6">
<a href="/projects"
onclick="navigateWithTransition('/projects', event)"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
<svg class="-ml-1 mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Create Project
</a>
</div>
</div>
)}
</>
)}
</div>
</main>
</div>
<!-- Navigation with View Transitions -->
<script>
// Clear redirect counter on successful dashboard load
sessionStorage.removeItem('redirect_count');
console.log('✅ Dashboard loaded successfully, redirect counter cleared');
// Attach to window object for global access from inline onclick handlers
window.navigateWithTransition = function(url, event) {
if (event) {
event.preventDefault();
}
if (document.startViewTransition) {
document.startViewTransition(() => {
window.location.href = url;
});
} else {
window.location.href = url;
}
};
// Auto-refresh dashboard data every 30 seconds (optional progressive enhancement)
if ('fetch' in window) {
setInterval(async () => {
try {
const response = await fetch('/api/dashboard/overview');
if (response.ok) {
// Optionally reload page with new data
// For now, we'll just keep it static until user navigates
}
} catch (error) {
// Silent fail for auto-refresh
}
}, 30000);
}
</script>
</Layout>