|
673 | 673 | document.addEventListener('DOMContentLoaded', function() { |
674 | 674 | const ctx = document.getElementById('successRateChart'); |
675 | 675 | |
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 | + } |
680 | 679 | |
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 |
682 | 704 | 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)); |
685 | 709 | }); |
686 | 710 | |
687 | | - // Format labels |
| 711 | + // Format labels - handle date parsing |
688 | 712 | 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 | + } |
691 | 730 | }); |
692 | 731 | |
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, { |
694 | 770 | type: 'line', |
695 | 771 | data: { |
696 | | - labels: labels, |
| 772 | + labels: chartLabels, |
697 | 773 | datasets: [ |
698 | 774 | { |
699 | 775 | label: 'Success Rate (%)', |
700 | | - data: successRates, |
| 776 | + data: chartSuccessRates, |
701 | 777 | borderColor: 'rgb(34, 197, 94)', |
702 | 778 | backgroundColor: 'rgba(34, 197, 94, 0.1)', |
| 779 | + borderWidth: 2, |
703 | 780 | tension: 0.4, |
704 | 781 | 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, |
706 | 791 | }, |
707 | 792 | { |
708 | 793 | label: 'Total Jobs', |
709 | | - data: totals, |
| 794 | + data: chartTotals, |
710 | 795 | borderColor: 'rgb(99, 102, 241)', |
711 | 796 | backgroundColor: 'rgba(99, 102, 241, 0.1)', |
| 797 | + borderWidth: 2, |
712 | 798 | tension: 0.4, |
713 | 799 | 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, |
715 | 808 | }, |
716 | 809 | { |
717 | 810 | label: 'Failed Jobs', |
718 | | - data: failures, |
| 811 | + data: chartFailures, |
719 | 812 | borderColor: 'rgb(239, 68, 68)', |
720 | 813 | backgroundColor: 'rgba(239, 68, 68, 0.1)', |
| 814 | + borderWidth: 2, |
721 | 815 | tension: 0.4, |
722 | 816 | 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, |
724 | 825 | } |
725 | 826 | ] |
726 | 827 | }, |
727 | 828 | options: { |
728 | 829 | responsive: true, |
729 | 830 | maintainAspectRatio: false, |
| 831 | + animation: { |
| 832 | + duration: 750, |
| 833 | + }, |
730 | 834 | interaction: { |
731 | 835 | mode: 'index', |
732 | 836 | intersect: false, |
733 | 837 | }, |
| 838 | + elements: { |
| 839 | + line: { |
| 840 | + borderJoinStyle: 'round', |
| 841 | + borderCapStyle: 'round', |
| 842 | + }, |
| 843 | + point: { |
| 844 | + hoverRadius: 8, |
| 845 | + } |
| 846 | + }, |
734 | 847 | plugins: { |
735 | 848 | legend: { |
736 | 849 | display: true, |
|
756 | 869 | } |
757 | 870 | }, |
758 | 871 | 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 | + }, |
759 | 884 | y: { |
760 | 885 | type: 'linear', |
761 | 886 | display: true, |
|
765 | 890 | text: 'Success Rate (%)' |
766 | 891 | }, |
767 | 892 | min: 0, |
768 | | - max: 100 |
| 893 | + max: 100, |
| 894 | + beginAtZero: true, |
769 | 895 | }, |
770 | 896 | y1: { |
771 | 897 | type: 'linear', |
|
775 | 901 | display: true, |
776 | 902 | text: 'Job Count' |
777 | 903 | }, |
| 904 | + beginAtZero: true, |
778 | 905 | grid: { |
779 | 906 | drawOnChartArea: false, |
780 | 907 | }, |
|
783 | 910 | } |
784 | 911 | }); |
785 | 912 | |
| 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 | + |
786 | 920 | // Auto-refresh every 30 seconds |
787 | 921 | setTimeout(function() { |
788 | 922 | location.reload(); |
|
0 commit comments