@@ -980,4 +980,210 @@ public function getFileInfoStats(): array
980980 ];
981981 });
982982 }
983+
984+ /**
985+ * Get HTTP status code summary for the widget
986+ */
987+ public function getHttpStatusSummary (): array
988+ {
989+ return $ this ->cached ('http_status_summary ' , function () {
990+ $ now = $ this ->now ();
991+ $ todayStart = $ now ->setTime (0 , 0 , 0 )->setTimezone (new \DateTimeZone ('UTC ' ))->format ('Y-m-d H:i:s ' );
992+ $ todayEnd = $ now ->setTime (23 , 59 , 59 )->setTimezone (new \DateTimeZone ('UTC ' ))->format ('Y-m-d H:i:s ' );
993+
994+ // Today's counts by status category
995+ $ categories = db ()->fetchAll (
996+ "SELECT
997+ CASE
998+ WHEN status_code BETWEEN 200 AND 299 THEN '2xx'
999+ WHEN status_code BETWEEN 300 AND 399 THEN '3xx'
1000+ WHEN status_code BETWEEN 400 AND 499 THEN '4xx'
1001+ WHEN status_code BETWEEN 500 AND 599 THEN '5xx'
1002+ ELSE 'other'
1003+ END AS category,
1004+ COUNT(*) AS total
1005+ FROM activity
1006+ WHERE created_at BETWEEN ? AND ?
1007+ AND status_code IS NOT NULL
1008+ GROUP BY category " ,
1009+ [$ todayStart , $ todayEnd ]
1010+ );
1011+
1012+ $ byCategory = ['2xx ' => 0 , '3xx ' => 0 , '4xx ' => 0 , '5xx ' => 0 ];
1013+ foreach ($ categories as $ row ) {
1014+ if (isset ($ byCategory [$ row ['category ' ]])) {
1015+ $ byCategory [$ row ['category ' ]] = (int )$ row ['total ' ];
1016+ }
1017+ }
1018+
1019+ // Top error paths (4xx and 5xx) in last 7 days
1020+ $ weekStart = $ now ->modify ('-7 days ' )->setTime (0 , 0 , 0 )
1021+ ->setTimezone (new \DateTimeZone ('UTC ' ))->format ('Y-m-d H:i:s ' );
1022+
1023+ $ topErrors = db ()->fetchAll (
1024+ "SELECT uri, status_code, COUNT(*) AS total
1025+ FROM activity
1026+ WHERE created_at BETWEEN ? AND ?
1027+ AND status_code >= 400
1028+ GROUP BY uri, status_code
1029+ ORDER BY total DESC
1030+ LIMIT 5 " ,
1031+ [$ weekStart , $ todayEnd ]
1032+ );
1033+
1034+ // Recent error requests
1035+ $ recentErrors = db ()->fetchAll (
1036+ "SELECT a.uri, a.status_code, a.created_at, u.first_name, u.last_name
1037+ FROM activity a
1038+ LEFT JOIN users u ON u.id = a.user_id
1039+ WHERE a.created_at BETWEEN ? AND ?
1040+ AND a.status_code >= 400
1041+ ORDER BY a.created_at DESC
1042+ LIMIT 5 " ,
1043+ [$ weekStart , $ todayEnd ]
1044+ );
1045+
1046+ $ recent = [];
1047+ foreach ($ recentErrors as $ row ) {
1048+ $ user = trim (($ row ['first_name ' ] ?? '' ) . ' ' . ($ row ['last_name ' ] ?? '' ));
1049+ $ recent [] = [
1050+ 'uri ' => $ row ['uri ' ],
1051+ 'status_code ' => (int )$ row ['status_code ' ],
1052+ 'user ' => $ user ?: 'Guest ' ,
1053+ 'time_ago ' => $ this ->timeAgo ($ row ['created_at ' ]),
1054+ ];
1055+ }
1056+
1057+ $ errorTotal = $ byCategory ['4xx ' ] + $ byCategory ['5xx ' ];
1058+
1059+ return [
1060+ 'by_category ' => $ byCategory ,
1061+ 'error_total ' => $ errorTotal ,
1062+ 'top_errors ' => $ topErrors ,
1063+ 'recent ' => $ recent ,
1064+ ];
1065+ });
1066+ }
1067+
1068+ /**
1069+ * Get status code distribution chart data (last 7 days, stacked bar)
1070+ */
1071+ public function getStatusCodeChart (): array
1072+ {
1073+ $ now = $ this ->now ();
1074+ $ tzOffset = $ now ->format ('P ' );
1075+
1076+ $ dayOfWeek = (int )$ now ->format ('N ' );
1077+ $ weekStart = $ now ->modify ('- ' . ($ dayOfWeek - 1 ) . ' days ' )->setTime (0 , 0 , 0 )
1078+ ->setTimezone (new \DateTimeZone ('UTC ' ))->format ('Y-m-d H:i:s ' );
1079+ $ weekEnd = $ now ->modify ('+ ' . (7 - $ dayOfWeek ) . ' days ' )->setTime (23 , 59 , 59 )
1080+ ->setTimezone (new \DateTimeZone ('UTC ' ))->format ('Y-m-d H:i:s ' );
1081+
1082+ $ data = db ()->fetchAll (
1083+ "SELECT
1084+ DATE(CONVERT_TZ(created_at, '+00:00', ?)) AS day_date,
1085+ MIN(DAYNAME(CONVERT_TZ(created_at, '+00:00', ?))) AS day_name,
1086+ CASE
1087+ WHEN status_code BETWEEN 200 AND 299 THEN '2xx'
1088+ WHEN status_code BETWEEN 300 AND 399 THEN '3xx'
1089+ WHEN status_code BETWEEN 400 AND 499 THEN '4xx'
1090+ WHEN status_code BETWEEN 500 AND 599 THEN '5xx'
1091+ ELSE 'other'
1092+ END AS category,
1093+ COUNT(*) AS total
1094+ FROM activity
1095+ WHERE created_at BETWEEN ? AND ?
1096+ AND status_code IS NOT NULL
1097+ GROUP BY day_date, category
1098+ ORDER BY day_date " ,
1099+ [$ tzOffset , $ tzOffset , $ weekStart , $ weekEnd ]
1100+ );
1101+
1102+ $ labels = ['Monday ' , 'Tuesday ' , 'Wednesday ' , 'Thursday ' , 'Friday ' , 'Saturday ' , 'Sunday ' ];
1103+ $ series = [
1104+ '2xx ' => array_fill (0 , 7 , 0 ),
1105+ '3xx ' => array_fill (0 , 7 , 0 ),
1106+ '4xx ' => array_fill (0 , 7 , 0 ),
1107+ '5xx ' => array_fill (0 , 7 , 0 ),
1108+ ];
1109+
1110+ foreach ($ data as $ row ) {
1111+ $ index = array_search ($ row ['day_name ' ], $ labels );
1112+ if ($ index !== false && isset ($ series [$ row ['category ' ]])) {
1113+ $ series [$ row ['category ' ]][$ index ] = (int )$ row ['total ' ];
1114+ }
1115+ }
1116+
1117+ return [
1118+ 'id ' => 'status-code-chart ' ,
1119+ 'title ' => 'Status Codes This Week ' ,
1120+ 'icon ' => 'shield-check ' ,
1121+ 'refresh_url ' => uri ('dashboard.status.chart ' ),
1122+ 'options ' => json_encode ([
1123+ 'type ' => 'bar ' ,
1124+ 'data ' => (object )[
1125+ 'labels ' => $ labels ,
1126+ 'datasets ' => [
1127+ (object )[
1128+ 'label ' => '2xx Success ' ,
1129+ 'data ' => $ series ['2xx ' ],
1130+ 'backgroundColor ' => 'rgba(34, 197, 94, 0.8) ' ,
1131+ 'borderColor ' => '#22c55e ' ,
1132+ 'borderWidth ' => 0 ,
1133+ 'borderRadius ' => 4 ,
1134+ ],
1135+ (object )[
1136+ 'label ' => '3xx Redirect ' ,
1137+ 'data ' => $ series ['3xx ' ],
1138+ 'backgroundColor ' => 'rgba(59, 130, 246, 0.8) ' ,
1139+ 'borderColor ' => '#3b82f6 ' ,
1140+ 'borderWidth ' => 0 ,
1141+ 'borderRadius ' => 4 ,
1142+ ],
1143+ (object )[
1144+ 'label ' => '4xx Client Error ' ,
1145+ 'data ' => $ series ['4xx ' ],
1146+ 'backgroundColor ' => 'rgba(245, 158, 11, 0.8) ' ,
1147+ 'borderColor ' => '#f59e0b ' ,
1148+ 'borderWidth ' => 0 ,
1149+ 'borderRadius ' => 4 ,
1150+ ],
1151+ (object )[
1152+ 'label ' => '5xx Server Error ' ,
1153+ 'data ' => $ series ['5xx ' ],
1154+ 'backgroundColor ' => 'rgba(239, 68, 68, 0.8) ' ,
1155+ 'borderColor ' => '#ef4444 ' ,
1156+ 'borderWidth ' => 0 ,
1157+ 'borderRadius ' => 4 ,
1158+ ],
1159+ ]
1160+ ],
1161+ 'options ' => (object )[
1162+ 'responsive ' => true ,
1163+ 'maintainAspectRatio ' => false ,
1164+ 'plugins ' => (object )[
1165+ 'legend ' => (object )[
1166+ 'display ' => true ,
1167+ 'position ' => 'top ' ,
1168+ ],
1169+ ],
1170+ 'scales ' => (object )[
1171+ 'y ' => (object )[
1172+ 'beginAtZero ' => true ,
1173+ 'stacked ' => true ,
1174+ 'grid ' => (object )[
1175+ 'color ' => 'rgba(0, 0, 0, 0.05) ' ,
1176+ ],
1177+ ],
1178+ 'x ' => (object )[
1179+ 'stacked ' => true ,
1180+ 'grid ' => (object )[
1181+ 'display ' => false ,
1182+ ],
1183+ ],
1184+ ],
1185+ ],
1186+ ]),
1187+ ];
1188+ }
9831189}
0 commit comments