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
1 change: 1 addition & 0 deletions services/dashboard-service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vercel
90 changes: 90 additions & 0 deletions services/dashboard-service/src/app/api/applications/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { NextResponse } from 'next/server';

// Mock data for demonstration - simulates job application logs
const mockLogs = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve type safety and maintainability, consider explicitly typing the mockLogs array. You could define an interface for the log entries (like the LogEntry in page.tsx) and apply it to this constant. For better code organization, this type could be defined in a shared file (e.g., src/types.ts) and imported both here and in page.tsx.

{
id: 1,
job_id: 'LI-2024-001',
platform: 'LinkedIn',
status: 'success',
details: 'Applied to Senior Software Engineer at Google',
created_at: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
},
{
id: 2,
job_id: 'GD-2024-042',
platform: 'Glassdoor',
status: 'success',
details: 'Applied to Full Stack Developer at Stripe',
created_at: new Date(Date.now() - 1000 * 60 * 15).toISOString(),
},
{
id: 3,
job_id: 'WF-2024-108',
platform: 'Wellfound',
status: 'success',
details: 'Applied to Founding Engineer at AI Startup',
created_at: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
},
{
id: 4,
job_id: 'LI-2024-002',
platform: 'LinkedIn',
status: 'failure',
details: 'Application blocked - requires Easy Apply',
created_at: new Date(Date.now() - 1000 * 60 * 45).toISOString(),
},
{
id: 5,
job_id: 'LI-2024-003',
platform: 'LinkedIn',
status: 'success',
details: 'Applied to Backend Engineer at Meta',
created_at: new Date(Date.now() - 1000 * 60 * 60).toISOString(),
},
{
id: 6,
job_id: 'GD-2024-043',
platform: 'Glassdoor',
status: 'success',
details: 'Applied to DevOps Engineer at Netflix',
created_at: new Date(Date.now() - 1000 * 60 * 90).toISOString(),
},
{
id: 7,
job_id: 'WF-2024-109',
platform: 'Wellfound',
status: 'failure',
details: 'Position closed before application submitted',
created_at: new Date(Date.now() - 1000 * 60 * 120).toISOString(),
},
{
id: 8,
job_id: 'LI-2024-004',
platform: 'LinkedIn',
status: 'success',
details: 'Applied to ML Engineer at OpenAI',
created_at: new Date(Date.now() - 1000 * 60 * 180).toISOString(),
},
{
id: 9,
job_id: 'GD-2024-044',
platform: 'Glassdoor',
status: 'success',
details: 'Applied to Platform Engineer at Vercel',
created_at: new Date(Date.now() - 1000 * 60 * 240).toISOString(),
},
{
id: 10,
job_id: 'LI-2024-005',
platform: 'LinkedIn',
status: 'success',
details: 'Applied to Senior Frontend Engineer at Airbnb',
created_at: new Date(Date.now() - 1000 * 60 * 300).toISOString(),
},
];
Comment on lines +4 to +85
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mock data array lacks explicit type annotations. Consider defining the data with an explicit type annotation using the LogEntry interface to ensure type safety and catch potential mismatches between the API response and the frontend expectations. This would also make the code more maintainable and self-documenting.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Timestamps are computed at module load, not per request.

The Date.now() calls are evaluated when the module is first loaded, not when each request is made. This means the timestamps will remain static and won't reflect the actual time of subsequent requests. For mock data that's meant to simulate recent activity, this results in increasingly stale timestamps.

Apply this diff to compute timestamps fresh on each request:

-// Mock data for demonstration - simulates job application logs
-const mockLogs = [
-  {
-    id: 1,
-    job_id: 'LI-2024-001',
-    platform: 'LinkedIn',
-    status: 'success',
-    details: 'Applied to Senior Software Engineer at Google',
-    created_at: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
-  },
-  {
-    id: 2,
-    job_id: 'GD-2024-042',
-    platform: 'Glassdoor',
-    status: 'success',
-    details: 'Applied to Full Stack Developer at Stripe',
-    created_at: new Date(Date.now() - 1000 * 60 * 15).toISOString(),
-  },
-  {
-    id: 3,
-    job_id: 'WF-2024-108',
-    platform: 'Wellfound',
-    status: 'success',
-    details: 'Applied to Founding Engineer at AI Startup',
-    created_at: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
-  },
-  {
-    id: 4,
-    job_id: 'LI-2024-002',
-    platform: 'LinkedIn',
-    status: 'failure',
-    details: 'Application blocked - requires Easy Apply',
-    created_at: new Date(Date.now() - 1000 * 60 * 45).toISOString(),
-  },
-  {
-    id: 5,
-    job_id: 'LI-2024-003',
-    platform: 'LinkedIn',
-    status: 'success',
-    details: 'Applied to Backend Engineer at Meta',
-    created_at: new Date(Date.now() - 1000 * 60 * 60).toISOString(),
-  },
-  {
-    id: 6,
-    job_id: 'GD-2024-043',
-    platform: 'Glassdoor',
-    status: 'success',
-    details: 'Applied to DevOps Engineer at Netflix',
-    created_at: new Date(Date.now() - 1000 * 60 * 90).toISOString(),
-  },
-  {
-    id: 7,
-    job_id: 'WF-2024-109',
-    platform: 'Wellfound',
-    status: 'failure',
-    details: 'Position closed before application submitted',
-    created_at: new Date(Date.now() - 1000 * 60 * 120).toISOString(),
-  },
-  {
-    id: 8,
-    job_id: 'LI-2024-004',
-    platform: 'LinkedIn',
-    status: 'success',
-    details: 'Applied to ML Engineer at OpenAI',
-    created_at: new Date(Date.now() - 1000 * 60 * 180).toISOString(),
-  },
-  {
-    id: 9,
-    job_id: 'GD-2024-044',
-    platform: 'Glassdoor',
-    status: 'success',
-    details: 'Applied to Platform Engineer at Vercel',
-    created_at: new Date(Date.now() - 1000 * 60 * 240).toISOString(),
-  },
-  {
-    id: 10,
-    job_id: 'LI-2024-005',
-    platform: 'LinkedIn',
-    status: 'success',
-    details: 'Applied to Senior Frontend Engineer at Airbnb',
-    created_at: new Date(Date.now() - 1000 * 60 * 300).toISOString(),
-  },
-];
+// Mock data for demonstration - simulates job application logs
+const getMockLogs = () => {
+  const now = Date.now();
+  return [
+    {
+      id: 1,
+      job_id: 'LI-2024-001',
+      platform: 'LinkedIn',
+      status: 'success',
+      details: 'Applied to Senior Software Engineer at Google',
+      created_at: new Date(now - 1000 * 60 * 5).toISOString(),
+    },
+    {
+      id: 2,
+      job_id: 'GD-2024-042',
+      platform: 'Glassdoor',
+      status: 'success',
+      details: 'Applied to Full Stack Developer at Stripe',
+      created_at: new Date(now - 1000 * 60 * 15).toISOString(),
+    },
+    {
+      id: 3,
+      job_id: 'WF-2024-108',
+      platform: 'Wellfound',
+      status: 'success',
+      details: 'Applied to Founding Engineer at AI Startup',
+      created_at: new Date(now - 1000 * 60 * 30).toISOString(),
+    },
+    {
+      id: 4,
+      job_id: 'LI-2024-002',
+      platform: 'LinkedIn',
+      status: 'failure',
+      details: 'Application blocked - requires Easy Apply',
+      created_at: new Date(now - 1000 * 60 * 45).toISOString(),
+    },
+    {
+      id: 5,
+      job_id: 'LI-2024-003',
+      platform: 'LinkedIn',
+      status: 'success',
+      details: 'Applied to Backend Engineer at Meta',
+      created_at: new Date(now - 1000 * 60 * 60).toISOString(),
+    },
+    {
+      id: 6,
+      job_id: 'GD-2024-043',
+      platform: 'Glassdoor',
+      status: 'success',
+      details: 'Applied to DevOps Engineer at Netflix',
+      created_at: new Date(now - 1000 * 60 * 90).toISOString(),
+    },
+    {
+      id: 7,
+      job_id: 'WF-2024-109',
+      platform: 'Wellfound',
+      status: 'failure',
+      details: 'Position closed before application submitted',
+      created_at: new Date(now - 1000 * 60 * 120).toISOString(),
+    },
+    {
+      id: 8,
+      job_id: 'LI-2024-004',
+      platform: 'LinkedIn',
+      status: 'success',
+      details: 'Applied to ML Engineer at OpenAI',
+      created_at: new Date(now - 1000 * 60 * 180).toISOString(),
+    },
+    {
+      id: 9,
+      job_id: 'GD-2024-044',
+      platform: 'Glassdoor',
+      status: 'success',
+      details: 'Applied to Platform Engineer at Vercel',
+      created_at: new Date(now - 1000 * 60 * 240).toISOString(),
+    },
+    {
+      id: 10,
+      job_id: 'LI-2024-005',
+      platform: 'LinkedIn',
+      status: 'success',
+      details: 'Applied to Senior Frontend Engineer at Airbnb',
+      created_at: new Date(now - 1000 * 60 * 300).toISOString(),
+    },
+  ];
+};

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In services/dashboard-service/src/app/api/applications/route.ts around lines
4-85, the mockLogs array computes timestamps at module load (Date.now() used
inline) so they become stale; change the implementation to compute timestamps
when handling each request by turning mockLogs into a factory function (e.g.,
getMockLogs()) or move the array creation inside the route handler so new
Date(Date.now() - ...) is evaluated per request, then call that
function/construct the array inside the handler to return fresh ISO timestamps.


export async function GET() {
await new Promise(resolve => setTimeout(resolve, 100));
return NextResponse.json(mockLogs);
}
183 changes: 130 additions & 53 deletions services/dashboard-service/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { useState, useEffect } from 'react';

// Define the type for a single log entry
interface LogEntry {
id: number;
job_id: string;
Expand All @@ -12,25 +11,26 @@ interface LogEntry {
created_at: string;
}

// The API URL for the monitoring service
const API_URL = process.env.NEXT_PUBLIC_API_URL
? `${process.env.NEXT_PUBLIC_API_URL}/api/logs`
: '/api/logs';

export default function Dashboard() {
const [logs, setLogs] = useState<LogEntry[]>([]);
const [error, setError] = useState<string | null>(null);
const [isInitialLoad, setIsInitialLoad] = useState<boolean>(true);
const [stats, setStats] = useState({ total: 0, success: 0, failure: 0 });

useEffect(() => {
const fetchLogs = async () => {
try {
const response = await fetch(API_URL);
const response = await fetch('/api/applications');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The fetch to /api/applications will be intercepted by the Next.js rewrite rule, preventing it from reaching the internal Route Handler and instead forwarding it to an external backend.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

The hardcoded fetch call to /api/applications in page.tsx is intended to hit the new internal Next.js Route Handler. However, the next.config.js file contains a rewrite rule that intercepts all requests matching /api/:path* and proxies them to an external backend service. Because Next.js processes rewrites before resolving Route Handlers, the request will never reach the internal mock API. This will cause the data fetch to fail in all environments, resulting in the dashboard displaying a "Could not connect to monitoring service" error to users.

💡 Suggested Fix

To ensure the fetch request reaches the internal Route Handler, either modify the source path in the next.config.js rewrite rule to be more specific and avoid capturing this internal API call, or change the path of the internal API route to something not captured by /api/:path*, such as /internal-api/applications.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: services/dashboard-service/src/app/page.tsx#L23

Potential issue: The hardcoded `fetch` call to `/api/applications` in `page.tsx` is
intended to hit the new internal Next.js Route Handler. However, the `next.config.js`
file contains a rewrite rule that intercepts all requests matching `/api/:path*` and
proxies them to an external backend service. Because Next.js processes rewrites before
resolving Route Handlers, the request will never reach the internal mock API. This will
cause the data fetch to fail in all environments, resulting in the dashboard displaying
a "Could not connect to monitoring service" error to users.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 7553421

if (!response.ok) {
throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`);
throw new Error(`Failed to fetch data: ${response.status}`);
}
const data: LogEntry[] = await response.json();
setLogs(data);
setStats({
total: data.length,
success: data.filter(l => l.status === 'success').length,
failure: data.filter(l => l.status === 'failure').length,
});
Comment on lines +29 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation iterates over the data array twice to calculate the number of successful and failed applications. This can be optimized by using a single reduce operation to compute all stats in one pass. This is more efficient and becomes more important as the dataset grows.

Suggested change
setStats({
total: data.length,
success: data.filter(l => l.status === 'success').length,
failure: data.filter(l => l.status === 'failure').length,
});
const { success, failure } = data.reduce(
(acc, log) => {
log.status === 'success' ? acc.success++ : acc.failure++;
return acc;
},
{ success: 0, failure: 0 }
);
setStats({ total: data.length, success, failure });

Comment on lines +29 to +33
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stats object is recalculated on every render when the data changes. Consider using useMemo to memoize the stats calculation since it only depends on the logs data, which would improve performance especially as the number of logs grows.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +33
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new stats calculation logic (calculating total, success, and failure counts) lacks test coverage. The existing test suite doesn't verify that these statistics are correctly computed and displayed in the stats cards.

Copilot uses AI. Check for mistakes.
setError(null);
} catch (err) {
console.error('Error fetching logs:', err);
Expand All @@ -41,67 +41,144 @@ export default function Dashboard() {
};

fetchLogs();
// Optional: Set up polling to refresh data periodically
const interval = setInterval(fetchLogs, 5000); // Refresh every 5 seconds
const interval = setInterval(fetchLogs, 10000);
return () => clearInterval(interval);
}, []);

const getPlatformColor = (platform: string) => {
switch (platform.toLowerCase()) {
case 'linkedin': return '#0077B5';
case 'glassdoor': return '#0CAA41';
case 'wellfound': return '#CC0000';
default: return '#666';
}
};
Comment on lines +48 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getPlatformColor function is a pure function that doesn't depend on any component state or props. To prevent it from being redeclared on every render of the Dashboard component, you can define it outside the component scope (e.g., right before the Dashboard component definition). This is a small but good performance optimization.

Comment on lines +48 to +55
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new getPlatformColor function lacks test coverage. Tests should verify that the correct colors are returned for LinkedIn, Glassdoor, Wellfound, and the default case to ensure consistent platform branding.

Copilot uses AI. Check for mistakes.

return (
<div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
<h1>Agent Application Dashboard</h1>
<p style={{ color: '#666', marginBottom: '20px' }}>
Monitor your automated job application activities in real-time
</p>

{isInitialLoad && <p>Loading application logs...</p>}

<div style={{ fontFamily: 'system-ui, -apple-system, sans-serif', padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This component uses extensive inline styling. While this works, it has several disadvantages:

  • Readability: It clutters the JSX, making it harder to read and understand the component's structure.
  • Maintainability: Changing styles requires hunting through the JSX. It's harder to maintain consistency.
  • Performance: Inline styles can't be cached by the browser as effectively as external stylesheets and can lead to larger component payloads.

Since tailwindcss is already a dependency in your project, I strongly recommend using it to style your components. It will make your code cleaner, more maintainable, and more consistent with modern React development practices.

For example, this line:

<div style={{ fontFamily: 'system-ui, -apple-system, sans-serif', padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>

Could be rewritten with Tailwind classes as:

<div className="font-sans p-6 max-w-7xl mx-auto">

<div style={{ marginBottom: '32px' }}>
<h1 style={{ fontSize: '28px', fontWeight: '700', marginBottom: '8px', color: '#111' }}>
Agent Application Dashboard
</h1>
<p style={{ color: '#666', fontSize: '16px' }}>
Monitor your automated job application activities in real-time
</p>
</div>

{/* Stats Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px', marginBottom: '24px' }}>
<div style={{ background: '#f8f9fa', borderRadius: '12px', padding: '20px', border: '1px solid #e9ecef' }}>
<p style={{ color: '#666', fontSize: '14px', marginBottom: '4px' }}>Total Applications</p>
<p style={{ fontSize: '32px', fontWeight: '700', color: '#111', margin: 0 }}>{stats.total}</p>
</div>
<div style={{ background: '#f0fdf4', borderRadius: '12px', padding: '20px', border: '1px solid #bbf7d0' }}>
<p style={{ color: '#166534', fontSize: '14px', marginBottom: '4px' }}>Successful</p>
<p style={{ fontSize: '32px', fontWeight: '700', color: '#16a34a', margin: 0 }}>{stats.success}</p>
</div>
<div style={{ background: '#fef2f2', borderRadius: '12px', padding: '20px', border: '1px solid #fecaca' }}>
<p style={{ color: '#991b1b', fontSize: '14px', marginBottom: '4px' }}>Failed</p>
<p style={{ fontSize: '32px', fontWeight: '700', color: '#dc2626', margin: 0 }}>{stats.failure}</p>
</div>
<div style={{ background: '#eff6ff', borderRadius: '12px', padding: '20px', border: '1px solid #bfdbfe' }}>
<p style={{ color: '#1e40af', fontSize: '14px', marginBottom: '4px' }}>Success Rate</p>
<p style={{ fontSize: '32px', fontWeight: '700', color: '#2563eb', margin: 0 }}>
{stats.total > 0 ? Math.round((stats.success / stats.total) * 100) : 0}%
</p>
</div>
</div>

{isInitialLoad && (
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>
<p>Loading application logs...</p>
</div>
)}

{error && (
<div style={{
backgroundColor: '#fff3cd',
border: '1px solid #ffc107',
padding: '12px',
borderRadius: '4px',
padding: '16px',
borderRadius: '8px',
marginBottom: '20px'
}}>
<p style={{ color: '#856404', margin: 0 }}>
<strong>Notice:</strong> The monitoring service is not currently available.
This is expected if the backend services are not running. {error}
<strong>Notice:</strong> Could not connect to monitoring service. {error}
</p>
</div>
)}

<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ backgroundColor: '#f2f2f2' }}>
<th style={{ padding: '8px', border: '1px solid #ddd', textAlign: 'left' }}>Platform</th>
<th style={{ padding: '8px', border: '1px solid #ddd', textAlign: 'left' }}>Job ID</th>
<th style={{ padding: '8px', border: '1px solid #ddd', textAlign: 'left' }}>Status</th>
<th style={{ padding: '8px', border: '1px solid #ddd', textAlign: 'left' }}>Timestamp</th>
<th style={{ padding: '8px', border: '1px solid #ddd', textAlign: 'left' }}>Details</th>
</tr>
</thead>
<tbody>
{!isInitialLoad && logs.length === 0 ? (
<tr>
<td colSpan={5} style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
{error ? 'Unable to load application logs.' : 'No application logs available yet.'}
</td>

{/* Application Logs Table */}
<div style={{ background: 'white', borderRadius: '12px', border: '1px solid #e5e7eb', overflow: 'hidden' }}>
<div style={{ padding: '16px 20px', borderBottom: '1px solid #e5e7eb' }}>
<h2 style={{ fontSize: '18px', fontWeight: '600', margin: 0 }}>Recent Applications</h2>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The table is missing a caption element for accessibility. Screen reader users would benefit from a descriptive table caption that explains the purpose of the table, such as "Job application history and status".

Suggested change
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<caption style={{
position: 'absolute',
width: '1px',
height: '1px',
padding: 0,
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0,0,0,0)',
border: 0
}}>
Job application history and status
</caption>

Copilot uses AI. Check for mistakes.
<thead>
<tr style={{ backgroundColor: '#f9fafb' }}>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Platform</th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Job ID</th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Status</th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Details</th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Time</th>
</tr>
) : (
logs.map((log) => (
<tr key={log.id}>
<td style={{ padding: '8px', border: '1px solid #ddd' }}>{log.platform}</td>
<td style={{ padding: '8px', border: '1px solid #ddd' }}>{log.job_id}</td>
<td style={{ padding: '8px', border: '1px solid #ddd', color: log.status === 'success' ? 'green' : 'red' }}>
{log.status}
</thead>
<tbody>
{!isInitialLoad && logs.length === 0 ? (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{!isInitialLoad && logs.length === 0 ? (
{!isInitialLoad && logs.length === 0 && !error ? (

Error state is conflated with empty data state. When the API fetch fails, both the error message and "No application logs available yet" message appear together, which is confusing to users.

View Details

Analysis

Error state conflated with empty data state in Dashboard

What fails: Dashboard component renders both error notification banner and "No application logs available yet" message simultaneously when API fetch fails, creating a confusing UX that doesn't clearly indicate whether the empty state is due to an error or absence of data.

How to reproduce:

  1. Start the dashboard with a working API endpoint
  2. Trigger an API failure (simulate network error or server error by mocking fetch to reject)
  3. Observe the rendered output

Result: Both appear together:

  • Error message: "Could not connect to monitoring service. [error details]"
  • Empty state message: "No application logs available yet."

Expected: When an error occurs, only the error message should display. The empty state message should only appear when there's genuinely no data and no error.

Root cause: The table tbody condition !isInitialLoad && logs.length === 0 doesn't check for the presence of an error state. When an API call fails, logs remains empty but error is set, causing both messages to render.

Fix: Added && !error to the condition so the empty state message only displays when there's no error: !isInitialLoad && logs.length === 0 && !error

This ensures:

  • Error scenario: Only error notification is shown
  • No-data scenario: Only empty state message is shown
  • Clear user communication in all cases

<tr>
<td colSpan={5} style={{ padding: '40px', textAlign: 'center', color: '#666' }}>
No application logs available yet.
</td>
<td style={{ padding: '8px', border: '1px solid #ddd' }}>{new Date(log.created_at).toLocaleString()}</td>
<td style={{ padding: '8px', border: '1px solid #ddd' }}>{log.details}</td>
</tr>
))
)}
</tbody>
</table>
) : (
logs.map((log, index) => (
<tr key={log.id} style={{ borderBottom: index < logs.length - 1 ? '1px solid #e5e7eb' : 'none' }}>
<td style={{ padding: '14px 16px' }}>
<span style={{
display: 'inline-block',
padding: '4px 10px',
borderRadius: '9999px',
fontSize: '12px',
fontWeight: '500',
color: 'white',
backgroundColor: getPlatformColor(log.platform)
}}>
{log.platform}
</span>
Comment on lines +136 to +146
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The platform badges use white text on colored backgrounds. For the default gray color (#666), white text may not meet WCAG AA contrast ratio requirements. Ensure adequate contrast ratios (at least 4.5:1 for normal text) for all platform badges to improve accessibility.

Copilot uses AI. Check for mistakes.
</td>
<td style={{ padding: '14px 16px', fontFamily: 'monospace', fontSize: '13px', color: '#374151' }}>
{log.job_id}
</td>
<td style={{ padding: '14px 16px' }}>
<span style={{
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
padding: '4px 10px',
borderRadius: '9999px',
fontSize: '12px',
fontWeight: '500',
color: log.status === 'success' ? '#166534' : '#991b1b',
backgroundColor: log.status === 'success' ? '#dcfce7' : '#fee2e2'
}}>
{log.status === 'success' ? '✓' : '✗'} {log.status}
</span>
</td>
<td style={{ padding: '14px 16px', color: '#374151', fontSize: '14px' }}>
{log.details}
</td>
<td style={{ padding: '14px 16px', color: '#6b7280', fontSize: '13px', whiteSpace: 'nowrap' }}>
{new Date(log.created_at).toLocaleString()}
</td>
</tr>
))
)}
</tbody>
</table>
</div>

<p style={{ textAlign: 'center', color: '#9ca3af', fontSize: '12px', marginTop: '24px' }}>
Auto-refreshing every 10 seconds
</p>
</div>
);
}
}
Loading