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
5 changes: 1 addition & 4 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,6 @@ const config = {
},
],

// Examples:
// ['@docusaurus/plugin-google-analytics', { trackingID: 'UA-XXXXXX-X' }],
// ['docusaurus-plugin-sass', {}],
// Add any other plugins you need
],

markdown: {
Expand Down Expand Up @@ -197,6 +193,7 @@ const config = {
},
{ to: "/blog", label: "Blog", position: "left" },
{ to: "/videos", label: "Videos", position: "left" },
{ to: "/nightly-e2e", label: "Nightly E2E", position: "left" },
{
type: 'html',
position: 'right',
Expand Down
229 changes: 229 additions & 0 deletions src/components/NightlyE2EStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import React, { useState, useEffect } from 'react';

const API_URL = 'https://console.kubestellar.io/api/public/nightly-e2e/runs';
const POLL_INTERVAL = 300000; // 5 minutes

const PLATFORMS = ['OCP', 'GKE', 'CKS'];
const PLATFORM_COLORS = { OCP: '#f97316', GKE: '#3b82f6', CKS: '#a855f7' };
const CONCLUSION_COLORS = { success: '#22c55e', failure: '#ef4444', cancelled: '#6b7280', skipped: '#6b7280' };

function timeAgo(ts) {
if (!ts) return '';
const ms = Date.now() - new Date(ts).getTime();
const h = Math.floor(ms / 3600000);
if (h < 1) return Math.floor(ms / 60000) + 'm ago';
if (h < 24) return h + 'h ago';
return Math.floor(h / 24) + 'd ago';
}

export default function NightlyE2EStatus({ styles }) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
let cancelled = false;

const fetchData = async () => {
try {
const res = await fetch(API_URL);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
if (!cancelled) {
setData(json);
setError(null);
}
} catch (e) {
if (!cancelled) {
setError(e.message);
setData(null);
}
} finally {
if (!cancelled) setLoading(false);
}
};

fetchData();
const interval = setInterval(fetchData, POLL_INTERVAL);
return () => {
cancelled = true;
clearInterval(interval);
};
}, []);

if (loading) {
return (
<div className={styles.statusCard}>
<div className={styles.statusHeader}>
<span className={`${styles.statusDot} ${styles.statusInfo}`} />
<span className={styles.statusTitle}>Nightly E2E Status</span>
</div>
<p className={styles.loadingText}>Loading status data...</p>
</div>
);
}

if (error) {
return (
<div className={styles.statusCard}>
<div className={styles.statusHeader}>
<span className={`${styles.statusDot} ${styles.statusError}`} />
<span className={styles.statusTitle}>Nightly E2E Status</span>
</div>
<div className={styles.offlineMessage}>
<p className={styles.offlineTitle}>Unable to load E2E status</p>
<p className={styles.offlineDetail}>
Could not connect to the status API. Please try again later.
</p>
</div>
</div>
);
}

const guides = data?.guides || [];
const totalGuides = guides.length;
const failing = guides.filter(g => g.latestConclusion === 'failure').length;
const allRuns = guides.flatMap(g => g.runs || []);
const completedRuns = allRuns.filter(r => r.status === 'completed');
const passedRuns = completedRuns.filter(r => r.conclusion === 'success');
const passRate = completedRuns.length > 0
? Math.round((passedRuns.length / completedRuns.length) * 100)
: 0;

return (
<div className={styles.statusCard}>
<div className={styles.statusHeader}>
<span
className={styles.statusDot}
style={{ backgroundColor: failing > 0 ? '#ef4444' : '#22c55e' }}
/>
<span className={styles.statusTitle}>Nightly E2E Status</span>
{data?.cachedAt && (
<span className={styles.lastUpdated}>
Updated {timeAgo(data.cachedAt)}
</span>
)}
</div>

<div className={styles.statsRow}>
<div className={styles.stat}>
<div className={styles.statValue} style={{ color: '#a855f7' }}>{passRate}%</div>
<div className={styles.statLabel}>Pass Rate</div>
</div>
<div className={styles.stat}>
<div className={styles.statValue}>{totalGuides}</div>
<div className={styles.statLabel}>Guides</div>
</div>
<div className={styles.stat}>
<div
className={styles.statValue}
style={{ color: failing > 0 ? '#ef4444' : '#22c55e' }}
>
{failing}
</div>
<div className={styles.statLabel}>Failing</div>
</div>
</div>

{PLATFORMS.map(platform => {
const platGuides = guides.filter(g => g.platform === platform);
if (platGuides.length === 0) return null;
return (
<div key={platform} className={styles.platformSection}>
<div
className={styles.platformName}
style={{ color: PLATFORM_COLORS[platform] }}
>
{platform}
</div>
{platGuides.map(g => {
const runs = (g.runs || []).slice(0, 7);
const failedAll = runs.filter(r => r.status === 'completed' && r.conclusion === 'failure');
const gpuFails = failedAll.filter(r => r.failureReason === 'gpu_unavailable').length;
const workflowUrl = `https://github.com/${g.repo}/actions/workflows/${g.workflowFile}`;

return (
<div key={g.guide + g.platform} className={styles.guideRow}>
<a
href={workflowUrl}
target="_blank"
rel="noopener noreferrer"
className={styles.guideAcronym}
title={`${g.guide} — View workflow`}
>
{g.acronym}
</a>
<div className={styles.guideNameWrap}>
<span className={styles.guideName}>{g.guide}</span>
<span className={styles.guideDetail}>
{g.model}{g.gpuType && g.gpuType !== 'CPU' ? ` · ${g.gpuCount}× ${g.gpuType}` : ''}
</span>
</div>
<div className={styles.runDots}>
{runs.map((run, i) => {
const isGpu = run.conclusion === 'failure' && run.failureReason === 'gpu_unavailable';
const dotColor = run.status !== 'completed'
? '#60a5fa'
: isGpu ? '#f59e0b'
: (CONCLUSION_COLORS[run.conclusion] || '#6b7280');
const label = (run.conclusion || run.status) + (isGpu ? ' (GPU unavailable)' : '');

return (
<a
key={i}
href={run.htmlUrl}
target="_blank"
rel="noopener noreferrer"
className={styles.runDotLink}
title={`${label} — ${timeAgo(run.updatedAt || run.createdAt)}`}
>
<span
className={`${styles.runDot} ${run.status !== 'completed' ? styles.runDotPulse : ''}`}
style={{ backgroundColor: dotColor }}
/>
</a>
);
})}
{runs.length === 0 && (
<span className={styles.noRuns}>no runs</span>
)}
</div>
<div className={styles.guideStats}>
{gpuFails > 0 && (
<span className={styles.gpuBadge} title="GPU unavailable failures">
GPU: {gpuFails}
</span>
)}
<span className={styles.guidePassRate} style={{
color: g.passRate >= 80 ? '#22c55e' : g.passRate >= 50 ? '#eab308' : '#ef4444',
}}>
{g.passRate}%
</span>
</div>
</div>
);
})}
</div>
);
})}

<div className={styles.legend}>
<span className={styles.legendItem}>
<span className={styles.legendDot} style={{ backgroundColor: '#22c55e' }} /> Pass
</span>
<span className={styles.legendItem}>
<span className={styles.legendDot} style={{ backgroundColor: '#ef4444' }} /> Fail
</span>
<span className={styles.legendItem}>
<span className={styles.legendDot} style={{ backgroundColor: '#f59e0b' }} /> GPU unavailable
</span>
<span className={styles.legendItem}>
<span className={styles.legendDot} style={{ backgroundColor: '#60a5fa' }} /> In progress
</span>
<span className={styles.legendItem}>
<span className={styles.legendDot} style={{ backgroundColor: '#6b7280' }} /> Cancelled
</span>
</div>
</div>
);
}
54 changes: 54 additions & 0 deletions src/pages/nightly-e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import Layout from '@theme/Layout';
import Head from '@docusaurus/Head';
import NightlyE2EStatus from '@site/src/components/NightlyE2EStatus';
import styles from './nightly-e2e.module.css';

export default function NightlyE2E() {
return (
<>
<Head>
<meta name="keywords" content="llm-d, nightly, e2e, end-to-end, testing, status, OCP, GKE, CKS, kubernetes" />
<meta property="og:title" content="llm-d Nightly E2E Status" />
<meta property="og:description" content="Live pass/fail status of llm-d nightly E2E workflows across OCP, GKE, and CKS platforms." />
</Head>
<Layout
title="Nightly E2E"
description="Live nightly E2E test status for llm-d across OCP, GKE, and CKS platforms">
<main className={styles.statusPage}>
<div className={styles.heroSection}>
<div className={styles.heroContent}>
<h1 className={styles.heroTitle}>
<span className={styles.heroIcon}>&#x2713;</span>
Nightly E2E Status
</h1>
<p className={styles.heroSubtitle}>
Live pass/fail status of llm-d nightly end-to-end workflows
across OCP, GKE, and CKS platforms.
</p>
</div>
</div>

<div className={styles.container}>
<NightlyE2EStatus styles={styles} />
</div>

<div className={styles.ctaSection}>
<h2 className={styles.ctaTitle}>Ready to get started?</h2>
<p className={styles.ctaText}>
Dive into our documentation or join our community to learn more.
</p>
<div className={styles.ctaButtons}>
<a href="/docs/guide" className={styles.ctaButtonPrimary}>
Read the Docs
</a>
<a href="/slack" className={styles.ctaButtonSecondary}>
Join Slack
</a>
</div>
</div>
</main>
</Layout>
</>
);
}
Loading