Skip to content

Commit 4e64d16

Browse files
committed
feat: add http status to activity table
1 parent d28e3ec commit 4e64d16

5 files changed

Lines changed: 242 additions & 0 deletions

File tree

app/Http/Controllers/Admin/Modules/ActivityController.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ protected function defineTable(TableSchemaBuilder $builder): void
2424
->searchable()
2525
->formatUsing(fn($col, $val) => $this->countryFlag($val));
2626
$builder->column('uri', 'URI', 'activity.uri')->searchable();
27+
$builder->column('status_code', 'Status', 'activity.status_code')
28+
->formatUsing(fn($col, $val) => $this->statusBadge($val));
2729
$builder->column('created_at', 'Created', 'activity.created_at');
2830

2931
$builder->filter('email', 'users.email')
@@ -41,10 +43,32 @@ protected function defineTable(TableSchemaBuilder $builder): void
4143
$builder->filterLink('Unauthenticated', "user_id IS NULL");
4244
$builder->filterLink('Authenticated', "user_id IS NOT NULL");
4345
$builder->filterLink('Me', sprintf("user_id = %s", user()->id));
46+
$builder->filterLink('Errors', "status_code >= 400");
4447

4548
$builder->toolbarAction('export');
4649
}
4750

51+
/**
52+
* Format an HTTP status code as a colored badge
53+
*/
54+
private function statusBadge(?string $code): string
55+
{
56+
if (!$code) {
57+
return '<span class="badge bg-secondary">-</span>';
58+
}
59+
60+
$status = (int)$code;
61+
$bg = match (true) {
62+
$status >= 500 => 'bg-danger',
63+
$status >= 400 => 'bg-warning text-dark',
64+
$status >= 300 => 'bg-info',
65+
$status >= 200 => 'bg-success',
66+
default => 'bg-secondary',
67+
};
68+
69+
return sprintf('<span class="badge %s">%d</span>', $bg, $status);
70+
}
71+
4872
/**
4973
* Convert a 2-letter country code to a flag icon, or a fallback icon
5074
*/

app/Http/Controllers/Admin/Modules/DashboardController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ public function requestsYtdChart()
4444
return $this->render('admin/dashboard-chart.html.twig', $this->service->getYTDRequestsChart());
4545
}
4646

47+
#[Get("/status/chart", "status.chart")]
48+
public function statusCodeChart()
49+
{
50+
return $this->render('admin/dashboard-chart.html.twig', $this->service->getStatusCodeChart());
51+
}
52+
4753
#[Get("/widgets/{id}", "widgets.render")]
4854
public function renderWidget(string $id): string
4955
{

app/Providers/WidgetServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Echo\Framework\Admin\Widgets\DatabaseWidget;
1010
use Echo\Framework\Admin\Widgets\EmailQueueWidget;
1111
use Echo\Framework\Admin\Widgets\FileInfoWidget;
12+
use Echo\Framework\Admin\Widgets\HttpStatusWidget;
1213
use Echo\Framework\Admin\Widgets\RedisWidget;
1314
use Echo\Framework\Admin\Widgets\StatsWidget;
1415
use Echo\Framework\Admin\Widgets\SystemHealthWidget;
@@ -37,6 +38,7 @@ public function register(): void
3738
WidgetRegistry::register('database', DatabaseWidget::class);
3839
WidgetRegistry::register('email-queue', EmailQueueWidget::class);
3940
WidgetRegistry::register('audit-summary', AuditSummaryWidget::class);
41+
WidgetRegistry::register('http-status', HttpStatusWidget::class);
4042
WidgetRegistry::register('users', UsersWidget::class);
4143
}
4244

app/Services/Admin/DashboardService.php

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

templates/admin/dashboard.html.twig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
</div>
66

77
<div id="charts">
8+
<div class="row">
9+
<div class="col-12 mb-3" hx-get="{{ uri('dashboard.status.chart') }}" hx-trigger="load">
10+
</div>
11+
</div>
812
<div class="row">
913
<div class="col-12 mb-3" hx-get="{{ uri('dashboard.requests.today.chart') }}" hx-trigger="load">
1014
</div>

0 commit comments

Comments
 (0)