Skip to content

Commit 2406e0d

Browse files
authored
Merge pull request #23 from houdaslassi/fix/dashboard-performance-and-scalability
feat: Add performance optimizations for large datasets
2 parents 7203f08 + 75fd5b9 commit 2406e0d

File tree

3 files changed

+259
-38
lines changed

3 files changed

+259
-38
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* Performance optimization: Add indexes for common queries
13+
*/
14+
public function up(): void
15+
{
16+
Schema::table('vantage_jobs', function (Blueprint $table) {
17+
// Index for filtering by created_at (most common filter)
18+
$table->index('created_at', 'idx_vantage_jobs_created_at');
19+
20+
// Index for filtering by status (processed, failed, processing)
21+
$table->index('status', 'idx_vantage_jobs_status');
22+
23+
// Composite index for created_at + status (common combination)
24+
$table->index(['created_at', 'status'], 'idx_vantage_jobs_created_status');
25+
26+
// Index for job_class (for grouping and filtering)
27+
$table->index('job_class', 'idx_vantage_jobs_job_class');
28+
29+
// Index for exception_class (for top exceptions query)
30+
$table->index('exception_class', 'idx_vantage_jobs_exception_class');
31+
32+
// Index for queue (for queue filtering)
33+
$table->index('queue', 'idx_vantage_jobs_queue');
34+
35+
// Index for retried_from_id (for retry chain queries)
36+
$table->index('retried_from_id', 'idx_vantage_jobs_retried_from');
37+
});
38+
}
39+
40+
/**
41+
* Reverse the migrations.
42+
*/
43+
public function down(): void
44+
{
45+
Schema::table('vantage_jobs', function (Blueprint $table) {
46+
$table->dropIndex('idx_vantage_jobs_created_at');
47+
$table->dropIndex('idx_vantage_jobs_status');
48+
$table->dropIndex('idx_vantage_jobs_created_status');
49+
$table->dropIndex('idx_vantage_jobs_job_class');
50+
$table->dropIndex('idx_vantage_jobs_exception_class');
51+
$table->dropIndex('idx_vantage_jobs_queue');
52+
$table->dropIndex('idx_vantage_jobs_retried_from');
53+
});
54+
}
55+
};
56+

resources/vantage-views/dashboard.blade.php

Lines changed: 153 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -673,64 +673,177 @@
673673
document.addEventListener('DOMContentLoaded', function() {
674674
const ctx = document.getElementById('successRateChart');
675675
676-
// Prepare data
677-
const hours = @json($jobsByHour->pluck('hour'));
678-
const totals = @json($jobsByHour->pluck('count'));
679-
const failures = @json($jobsByHour->pluck('failed_count'));
676+
if (!ctx) {
677+
return;
678+
}
680679
681-
// Calculate success rates
680+
// Prepare data - ensure we get proper arrays
681+
const jobsData = @json($jobsByHour->toArray());
682+
683+
if (!jobsData || jobsData.length === 0) {
684+
if (ctx && ctx.parentElement) {
685+
ctx.parentElement.innerHTML = '<div class="p-4 text-gray-500 text-center">No data available for the selected time period.</div>';
686+
}
687+
return;
688+
}
689+
690+
// Extract arrays from data
691+
const hours = jobsData.map(item => item.hour);
692+
const totals = jobsData.map(item => parseInt(item.count) || 0);
693+
const failures = jobsData.map(item => parseInt(item.failed_count) || 0);
694+
695+
// Ensure arrays are valid and same length
696+
if (hours.length === 0 || totals.length === 0 || hours.length !== totals.length) {
697+
if (ctx && ctx.parentElement) {
698+
ctx.parentElement.innerHTML = '<div class="p-4 text-gray-500 text-center">No data available for the selected time period.</div>';
699+
}
700+
return;
701+
}
702+
703+
// Calculate success rates - ensure failures is a number
682704
const successRates = totals.map((total, index) => {
683-
if (total === 0) return 0;
684-
return ((total - failures[index]) / total * 100).toFixed(1);
705+
const failed = parseInt(failures[index]) || 0;
706+
const totalNum = parseInt(total) || 0;
707+
if (totalNum === 0) return 0;
708+
return parseFloat(((totalNum - failed) / totalNum * 100).toFixed(1));
685709
});
686710
687-
// Format labels
711+
// Format labels - handle date parsing
688712
const labels = hours.map(h => {
689-
const date = new Date(h);
690-
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit' });
713+
try {
714+
// Handle MySQL/SQLite datetime format: "2025-11-21 19:00:00"
715+
// Convert to ISO format: "2025-11-21T19:00:00"
716+
const isoDate = h.replace(' ', 'T');
717+
const date = new Date(isoDate);
718+
if (isNaN(date.getTime())) {
719+
// Fallback: try parsing as-is
720+
const fallbackDate = new Date(h);
721+
if (!isNaN(fallbackDate.getTime())) {
722+
return fallbackDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit' });
723+
}
724+
return h;
725+
}
726+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit' });
727+
} catch (e) {
728+
return h;
729+
}
691730
});
692731
693-
new Chart(ctx, {
732+
// Ensure we have valid data
733+
if (labels.length === 0 || successRates.length === 0 || totals.length === 0) {
734+
return;
735+
}
736+
737+
// For single data point, create a small time range to show as a line
738+
// This ensures the chart always shows as a line chart with filled area
739+
let chartLabels = labels;
740+
let chartSuccessRates = successRates;
741+
let chartTotals = totals;
742+
let chartFailures = failures;
743+
744+
if (labels.length === 1) {
745+
// Create a second point 1 hour later to form a line segment
746+
try {
747+
const singleDate = new Date(hours[0].replace(' ', 'T'));
748+
const nextHour = new Date(singleDate.getTime() + 60 * 60 * 1000);
749+
const nextLabel = nextHour.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit' });
750+
751+
chartLabels = [labels[0], nextLabel];
752+
chartSuccessRates = [successRates[0], successRates[0]];
753+
chartTotals = [totals[0], totals[0]];
754+
chartFailures = [failures[0], failures[0]];
755+
} catch (e) {
756+
// Fallback: just duplicate if date parsing fails
757+
chartLabels = [labels[0], labels[0]];
758+
chartSuccessRates = [successRates[0], successRates[0]];
759+
chartTotals = [totals[0], totals[0]];
760+
chartFailures = [failures[0], failures[0]];
761+
}
762+
}
763+
764+
try {
765+
// Point radius - smaller for single point (since we duplicate it)
766+
const pointRadius = 4;
767+
const pointHoverRadius = 6;
768+
769+
const chart = new Chart(ctx, {
694770
type: 'line',
695771
data: {
696-
labels: labels,
772+
labels: chartLabels,
697773
datasets: [
698774
{
699775
label: 'Success Rate (%)',
700-
data: successRates,
776+
data: chartSuccessRates,
701777
borderColor: 'rgb(34, 197, 94)',
702778
backgroundColor: 'rgba(34, 197, 94, 0.1)',
779+
borderWidth: 2,
703780
tension: 0.4,
704781
fill: true,
705-
yAxisID: 'y'
782+
yAxisID: 'y',
783+
pointRadius: pointRadius,
784+
pointHoverRadius: pointHoverRadius,
785+
pointBackgroundColor: 'rgb(34, 197, 94)',
786+
pointBorderColor: '#fff',
787+
pointBorderWidth: 2,
788+
// Ensure line is visible even with single point
789+
spanGaps: false,
790+
showLine: true,
706791
},
707792
{
708793
label: 'Total Jobs',
709-
data: totals,
794+
data: chartTotals,
710795
borderColor: 'rgb(99, 102, 241)',
711796
backgroundColor: 'rgba(99, 102, 241, 0.1)',
797+
borderWidth: 2,
712798
tension: 0.4,
713799
fill: true,
714-
yAxisID: 'y1'
800+
yAxisID: 'y1',
801+
pointRadius: pointRadius,
802+
pointHoverRadius: pointHoverRadius,
803+
pointBackgroundColor: 'rgb(99, 102, 241)',
804+
pointBorderColor: '#fff',
805+
pointBorderWidth: 2,
806+
spanGaps: false,
807+
showLine: true,
715808
},
716809
{
717810
label: 'Failed Jobs',
718-
data: failures,
811+
data: chartFailures,
719812
borderColor: 'rgb(239, 68, 68)',
720813
backgroundColor: 'rgba(239, 68, 68, 0.1)',
814+
borderWidth: 2,
721815
tension: 0.4,
722816
fill: true,
723-
yAxisID: 'y1'
817+
yAxisID: 'y1',
818+
pointRadius: pointRadius,
819+
pointHoverRadius: pointHoverRadius,
820+
pointBackgroundColor: 'rgb(239, 68, 68)',
821+
pointBorderColor: '#fff',
822+
pointBorderWidth: 2,
823+
spanGaps: false,
824+
showLine: true,
724825
}
725826
]
726827
},
727828
options: {
728829
responsive: true,
729830
maintainAspectRatio: false,
831+
animation: {
832+
duration: 750,
833+
},
730834
interaction: {
731835
mode: 'index',
732836
intersect: false,
733837
},
838+
elements: {
839+
line: {
840+
borderJoinStyle: 'round',
841+
borderCapStyle: 'round',
842+
},
843+
point: {
844+
hoverRadius: 8,
845+
}
846+
},
734847
plugins: {
735848
legend: {
736849
display: true,
@@ -756,6 +869,18 @@
756869
}
757870
},
758871
scales: {
872+
x: {
873+
display: true,
874+
// Ensure x-axis displays even with single data point
875+
ticks: {
876+
autoSkip: false,
877+
maxRotation: 45,
878+
minRotation: 0,
879+
},
880+
// For single point, ensure it's visible
881+
min: undefined,
882+
max: undefined,
883+
},
759884
y: {
760885
type: 'linear',
761886
display: true,
@@ -765,7 +890,8 @@
765890
text: 'Success Rate (%)'
766891
},
767892
min: 0,
768-
max: 100
893+
max: 100,
894+
beginAtZero: true,
769895
},
770896
y1: {
771897
type: 'linear',
@@ -775,6 +901,7 @@
775901
display: true,
776902
text: 'Job Count'
777903
},
904+
beginAtZero: true,
778905
grid: {
779906
drawOnChartArea: false,
780907
},
@@ -783,6 +910,13 @@
783910
}
784911
});
785912
913+
} catch (error) {
914+
// Show error message to user
915+
if (ctx && ctx.parentElement) {
916+
ctx.parentElement.innerHTML = '<div class="p-4 text-red-600">Error loading chart: ' + error.message + '</div>';
917+
}
918+
}
919+
786920
// Auto-refresh every 30 seconds
787921
setTimeout(function() {
788922
location.reload();

0 commit comments

Comments
 (0)