Skip to content

Commit 9e59815

Browse files
committed
fix(dashboard): rewrite Health page with valid syntax + visible icons
Three rendering bugs in one page (visible in the user's screenshot): **1. Raw `@foreach(metric in healthMetrics)` text leaked into the output.** STX's foreach syntax is `@foreach(items as item)`, not `@foreach(item in items)`. The page was using the latter, so the directive parser left the literal `@foreach(...)` and `@endforeach` strings in the rendered HTML — the user saw them as plain text below the four overview cards. Switched to the Laravel-style syntax everywhere. **2. Stat icons rendered as empty colored squares.** The cards used StatsCard with `icon="i-hugeicons-flash"` etc., which need an iconify/UnoCSS engine to inject the SVG mask — and that engine isn't loaded in the dashboard. Replaced the StatsCard usage with inline-SVG cards (same pattern as Dependencies) so the icons actually render: lightning for Uptime, shield for Health Checks, chain for URLs, network for DNS. **3. Vue-style array `:class` bindings didn't evaluate.** The detail-card icon wrapper used `:class="[getStatusIconBg(metric.status), 'p-2.5 rounded-lg flex-shrink-0']"`, which STX's directive processor doesn't expand. Replaced with `class="… {{ statusIconBg[…] }}"` string interpolation. Same pattern for the metrics-grid columns: the previous version had a mangled `class="..."` string that substituted parts of multiple class names into a single attribute; moved the column-count logic to a `metricsGridCols(n)` JS helper in the script block. Bonus cleanup: added a PageHeader with overall-status Badge in the actions slot, expanded the icon-bg map to include `info` for the Certificate transparency check, replaced the stale "Expires May 4, 2024" date with a relative "274 days" string so the placeholder data doesn't ship looking out-of-date.
1 parent 5bc8bae commit 9e59815

1 file changed

Lines changed: 167 additions & 134 deletions

File tree

  • storage/framework/defaults/views/dashboard/health

storage/framework/defaults/views/dashboard/health/index.stx

Lines changed: 167 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -4,214 +4,247 @@
44
<script server>
55
definePageMeta({
66
middleware: ['auth'],
7-
layout: 'default'
7+
layout: 'default',
88
})
99

10+
import PageHeader from '../../../components/Dashboard/UI/PageHeader.stx'
1011
import Card from '../../../components/Dashboard/UI/Card.stx'
1112
import Badge from '../../../components/Dashboard/UI/Badge.stx'
12-
import StatsCard from '../../../components/Dashboard/UI/StatsCard.stx'
1313

14-
interface HealthMetric {
14+
interface Metric {
15+
label: string
16+
value: string
17+
}
18+
19+
interface HealthCheck {
1520
name: string
16-
icon: string
1721
status: 'success' | 'info' | 'warning' | 'danger'
1822
description: string
19-
metrics: { label: string; value: string }[]
23+
/** Inline SVG path data — `d=""` content for a 24x24 stroke icon. */
24+
iconPath: string
25+
metrics: Metric[]
2026
}
2127

22-
let healthMetrics = [
28+
// Placeholder data. Wire this to a real health endpoint once the
29+
// dashboard exposes one (e.g. `GET /api/dashboard/health`). The
30+
// `iconPath` strings are inline SVG `d` attributes — we render them
31+
// with a single shared <svg> wrapper so the icon system doesn't
32+
// depend on a CSS engine.
33+
const checks: HealthCheck[] = [
2334
{
2435
name: 'Uptime',
25-
icon: 'i-hugeicons-flash',
2636
status: 'success',
27-
description: 'Your site is up. We last checked less than a minute ago.',
37+
description: 'Your site is up. Last checked less than a minute ago.',
38+
iconPath: 'M13 10V3L4 14h7v7l9-11h-7z',
2839
metrics: [
29-
{ label: 'Past 7 Days', value: '100%' },
30-
{ label: 'Past 14 Days', value: '100%' },
31-
{ label: 'Past 30 Days', value: '100%' },
40+
{ label: 'Past 7 days', value: '100%' },
41+
{ label: 'Past 14 days', value: '100%' },
42+
{ label: 'Past 30 days', value: '100%' },
3243
],
3344
},
3445
{
3546
name: 'Performance',
36-
icon: 'i-hugeicons-rocket-01',
3747
status: 'success',
38-
description: "Your site is fast. Last checked 3 minutes ago.",
48+
description: 'Your site is fast. Last checked 3 minutes ago.',
49+
iconPath: 'M13 10V3L4 14h7v7l9-11h-7z',
3950
metrics: [
40-
{ label: 'Response Time', value: '235ms' },
41-
{ label: 'Success Rate', value: '99.95%' },
51+
{ label: 'Response time', value: '235ms' },
52+
{ label: 'Success rate', value: '99.95%' },
4253
],
4354
},
4455
{
45-
name: 'Certificate Health',
46-
icon: 'i-hugeicons-shield-01',
56+
name: 'Certificate health',
4757
status: 'success',
48-
description: 'Your certificate is healthy. We last checked 3 minutes ago.',
58+
description: 'TLS certificate is healthy. Last checked 3 minutes ago.',
59+
iconPath: 'M9 12l2 2 4-4M21 12c0 4.97-4.03 9-9 9s-9-4.03-9-9 4.03-9 9-9 9 4.03 9 9z',
4960
metrics: [
50-
{ label: 'Expires In', value: 'May 4, 2024 (14 days' },
51-
{ label: 'Last Renewal', value: 'Feb 4, 2024' }
61+
{ label: 'Expires in', value: '274 days' },
62+
{ label: 'Last renewal', value: '2 months ago' },
5263
],
5364
},
5465
{
55-
name: 'Broken Links',
56-
icon: 'i-hugeicons-link-01',
66+
name: 'Broken links',
5767
status: 'success',
58-
description: 'Your site has no broken links.',
68+
description: 'No broken links found across crawled pages.',
69+
iconPath: 'M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1',
5970
metrics: [
60-
{ label: 'Broken Links', value: '0' },
61-
{ label: 'URLs Crawled', value: '583' },
71+
{ label: 'Broken links', value: '0' },
72+
{ label: 'URLs crawled', value: '583' },
6273
],
6374
},
6475
{
65-
name: 'Mixed Content',
66-
icon: 'i-hugeicons-lock-01',
76+
name: 'Mixed content',
6777
status: 'success',
68-
description: 'We did not find mixed content. We last checked an hour ago.',
78+
description: 'No mixed content found. Last checked an hour ago.',
79+
iconPath: 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z',
6980
metrics: [
70-
{ label: 'Pieces of Mixed Content', value: '0' },
71-
{ label: 'URLs Crawled', value: '0' },
81+
{ label: 'Mixed content', value: '0' },
82+
{ label: 'URLs crawled', value: '583' },
7283
],
7384
},
7485
{
75-
name: 'Certificate Transparency',
76-
icon: 'i-hugeicons-file-security',
86+
name: 'Certificate transparency',
7787
status: 'info',
78-
description: 'Your site is up. We last checked less than a minute ago.',
88+
description: 'CT log entry verified. Last checked less than a minute ago.',
89+
iconPath: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
7990
metrics: [
80-
{ label: 'Last Check', value: '< 1 min ago' },
81-
{ label: 'Status', value: 'Verified' }
91+
{ label: 'Last check', value: '< 1 min ago' },
92+
{ label: 'Status', value: 'Verified' },
8293
],
8394
},
8495
{
85-
name: 'Application Health',
86-
icon: 'i-hugeicons-checkmark-circle-02',
96+
name: 'Application health',
8797
status: 'success',
88-
description: 'Your application seems to be healthy. We last checked 10 minutes ago.',
98+
description: 'All application checks passing. Last checked 10 minutes ago.',
99+
iconPath: 'M5 13l4 4L19 7',
89100
metrics: [
90-
{ label: 'Total Health Checks', value: '2' },
91-
{ label: 'Failed Health Checks', value: '0' },
92-
{ label: 'Last Check', value: '10 min ago' },
101+
{ label: 'Total checks', value: '2' },
102+
{ label: 'Failed', value: '0' },
103+
{ label: 'Last check', value: '10 min ago' },
93104
],
94105
},
95106
{
96107
name: 'DNS',
97-
icon: 'i-hugeicons-globe-02',
98108
status: 'success',
99-
description: 'DNS records found. We last checked an hour ago.',
109+
description: 'DNS records resolved cleanly. Last checked an hour ago.',
110+
iconPath: 'M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
100111
metrics: [
101-
{ label: 'DNS Records', value: '18' },
102-
{ label: 'URLs Crawled', value: '20' },
112+
{ label: 'Records', value: '18' },
113+
{ label: 'URLs crawled', value: '20' },
103114
],
104115
},
105116
]
106117

107-
const getStatusVariant = (status) => {
108-
const map = {
109-
success: 'success',
110-
info: 'primary',
111-
warning: 'warning',
112-
danger: 'danger',
113-
}
114-
return map[status] || 'default'
118+
const passingCount = checks.filter(c => c.status === 'success').length
119+
const totalCount = checks.length
120+
const overallStatus: 'success' | 'warning' | 'danger' =
121+
checks.some(c => c.status === 'danger')
122+
? 'danger'
123+
: checks.some(c => c.status === 'warning')
124+
? 'warning'
125+
: 'success'
126+
127+
const statusIconBg = {
128+
success: 'bg-green-500/10 text-green-600 dark:bg-green-400/15 dark:text-green-400',
129+
info: 'bg-blue-500/10 text-blue-600 dark:bg-blue-400/15 dark:text-blue-400',
130+
warning: 'bg-orange-500/10 text-orange-600 dark:bg-orange-400/15 dark:text-orange-400',
131+
danger: 'bg-red-500/10 text-red-600 dark:bg-red-400/15 dark:text-red-400',
132+
}
133+
134+
const statusBadgeVariant = {
135+
success: 'success',
136+
info: 'primary',
137+
warning: 'warning',
138+
danger: 'danger',
115139
}
116140

117-
const getStatusIconBg = (status) => {
118-
const map = {
119-
success: 'bg-green-500/15 text-green-600 dark:bg-green-400/20 dark:text-green-400',
120-
info: 'bg-blue-500/15 text-blue-600 dark:bg-blue-400/20 dark:text-blue-400',
121-
warning: 'bg-orange-500/15 text-orange-600 dark:bg-orange-400/20 dark:text-orange-400',
122-
danger: 'bg-red-500/15 text-red-600 dark:bg-red-400/20 dark:text-red-400',
123-
}
124-
return map[status] || 'bg-neutral-500/15 text-neutral-600'
141+
// Pluralize the metrics-grid columns based on count. Done here in JS
142+
// because STX templates don't support the Vue-style array `:class`
143+
// binding (`:class="[a, cond ? b : c]"`); a precomputed string is
144+
// simpler than threading conditional classes through the template.
145+
function metricsGridCols(count: number) {
146+
if (count <= 1) return 'grid-cols-1'
147+
if (count === 2) return 'grid-cols-2'
148+
return 'grid-cols-3'
125149
}
126150
</script>
127151

128-
@title('Dashboard - Health')
129-
<div>
130-
<!-- Overview Stats -->
131-
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 mb-6">
132-
<StatsCard
133-
title="Uptime"
134-
value="100%"
135-
:trend="0"
136-
trend-label="Last 30 days"
137-
icon="i-hugeicons-flash"
138-
icon-bg="primary"
139-
/>
140-
<StatsCard
141-
title="Health Checks"
142-
value="2/2"
143-
subtitle="All checks passing"
144-
icon="i-hugeicons-checkmark-circle-02"
145-
icon-bg="success"
146-
/>
147-
<StatsCard
148-
title="URLs Monitored"
149-
value="583"
150-
subtitle="Across all pages"
151-
icon="i-hugeicons-link-01"
152-
icon-bg="primary"
153-
/>
154-
<StatsCard
155-
title="DNS Records"
156-
value="18"
157-
subtitle="Verified records"
158-
icon="i-hugeicons-globe-02"
159-
icon-bg="primary"
160-
/>
152+
<div class="px-6 pt-6 pb-12 mx-auto max-w-6xl space-y-6">
153+
154+
<PageHeader title="Health" description="System uptime, performance, and certificate checks">
155+
<template #actions>
156+
<Badge :variant="statusBadgeVariant[overallStatus]" size="md">
157+
{{ overallStatus === 'success' ? 'All systems normal' : overallStatus === 'warning' ? 'Degraded' : 'Critical' }}
158+
</Badge>
159+
</template>
160+
</PageHeader>
161+
162+
<!-- Overview stats -->
163+
<div class="grid gap-4 grid-cols-2 lg:grid-cols-4">
164+
<Card variant="default" padding="md">
165+
<div class="flex gap-3 items-center">
166+
<div class="flex-shrink-0 flex items-center justify-center h-10 w-10 bg-green-500/10 text-green-600 dark:bg-green-400/15 dark:text-green-400 rounded-lg">
167+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
168+
<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/>
169+
</svg>
170+
</div>
171+
<div class="min-w-0">
172+
<p class="text-neutral-500 text-[11px] uppercase tracking-wider dark:text-neutral-400">Uptime</p>
173+
<p class="font-semibold text-2xl text-neutral-900 leading-tight tracking-tight dark:text-white">100%</p>
174+
</div>
175+
</div>
176+
</Card>
177+
<Card variant="default" padding="md">
178+
<div class="flex gap-3 items-center">
179+
<div class="flex-shrink-0 flex items-center justify-center h-10 w-10 bg-blue-500/10 text-blue-600 dark:bg-blue-400/15 dark:text-blue-400 rounded-lg">
180+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
181+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
182+
</svg>
183+
</div>
184+
<div class="min-w-0">
185+
<p class="text-neutral-500 text-[11px] uppercase tracking-wider dark:text-neutral-400">Health checks</p>
186+
<p class="font-semibold text-2xl text-neutral-900 leading-tight tracking-tight dark:text-white">{{ passingCount }}/{{ totalCount }}</p>
187+
</div>
188+
</div>
189+
</Card>
190+
<Card variant="default" padding="md">
191+
<div class="flex gap-3 items-center">
192+
<div class="flex-shrink-0 flex items-center justify-center h-10 w-10 bg-purple-500/10 text-purple-600 dark:bg-purple-400/15 dark:text-purple-400 rounded-lg">
193+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
194+
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
195+
</svg>
196+
</div>
197+
<div class="min-w-0">
198+
<p class="text-neutral-500 text-[11px] uppercase tracking-wider dark:text-neutral-400">URLs monitored</p>
199+
<p class="font-semibold text-2xl text-neutral-900 leading-tight tracking-tight dark:text-white">583</p>
200+
</div>
201+
</div>
202+
</Card>
203+
<Card variant="default" padding="md">
204+
<div class="flex gap-3 items-center">
205+
<div class="flex-shrink-0 flex items-center justify-center h-10 w-10 bg-cyan-500/10 text-cyan-600 dark:bg-cyan-400/15 dark:text-cyan-400 rounded-lg">
206+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
207+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
208+
</svg>
209+
</div>
210+
<div class="min-w-0">
211+
<p class="text-neutral-500 text-[11px] uppercase tracking-wider dark:text-neutral-400">DNS records</p>
212+
<p class="font-semibold text-2xl text-neutral-900 leading-tight tracking-tight dark:text-white">18</p>
213+
</div>
214+
</div>
215+
</Card>
161216
</div>
162217

163-
<!-- Detailed Health Cards -->
164-
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
165-
@foreach(metric in healthMetrics)
166-
<Card
167-
variant="default"
168-
padding="md"
169-
hoverable
170-
>
218+
<!-- Detailed checks -->
219+
<div class="grid gap-4 grid-cols-1 lg:grid-cols-2">
220+
@foreach(checks as check)
221+
<Card variant="default" padding="md" hoverable>
171222
<div class="flex gap-4 items-start">
172-
<!-- Icon -->
173-
<div :class="[getStatusIconBg(metric.status), 'p-2.5 rounded-lg flex-shrink-0']">
174-
<div :class="[metric.icon, 'h-5 w-5']" />
223+
<div class="flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-lg {{ statusIconBg[check.status] }}">
224+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
225+
<path stroke-linecap="round" stroke-linejoin="round" d="{{ check.iconPath }}"/>
226+
</svg>
175227
</div>
176228

177-
<!-- Content -->
178229
<div class="flex-1 min-w-0">
179230
<div class="flex gap-2 items-center justify-between">
180-
<h3 class="font-medium text-[14px] text-neutral-900 truncate dark:text-white">
181-
{{ metric.name }}
182-
</h3>
183-
<Badge :variant="getStatusVariant(metric.status)" size="sm">
184-
{{ metric.status }}
185-
</Badge>
231+
<h3 class="font-medium text-[14px] text-neutral-900 truncate dark:text-white">{{ check.name }}</h3>
232+
<Badge :variant="statusBadgeVariant[check.status]" size="sm">{{ check.status }}</Badge>
186233
</div>
187-
<p class="mt-1 line-clamp-1 text-[12px] text-neutral-500 dark:text-neutral-400">
188-
{{ metric.description }}
234+
<p class="mt-1 line-clamp-2 text-[12px] text-neutral-500 dark:text-neutral-400">
235+
{{ check.description }}
189236
</p>
190-
</div>
191-
</div>
192237

193-
<!-- Metrics Grid -->
194-
@if(metric.metrics)
195-
<div class="mt-4">
196-
<dl
197-
class="grid gap-3"
198-
class="'grid-cols-1': 'grid-cols-2': 'grid-cols-3': { {{ } }} === === === 1 2, 3, metric.metrics.length metric.metrics.length metric.metrics.length"
199-
>
200-
@foreach(m in metric.metrics)
201-
<div
202-
class="overflow-hidden px-3 py-2.5 bg-black/[0.03] dark:bg-white/[0.05] rounded-lg"
203-
>
204-
<dt class="font-medium text-[11px] text-neutral-500 truncate dark:text-neutral-400">
205-
{{ m.label }}
206-
</dt>
207-
<dd class="mt-0.5 font-semibold text-[14px] text-neutral-900 tracking-tight dark:text-white">
208-
{{ m.value }}
209-
</dd>
238+
<dl class="grid gap-2 mt-4 {{ metricsGridCols(check.metrics.length) }}">
239+
@foreach(check.metrics as m)
240+
<div class="overflow-hidden px-3 py-2 bg-black/[0.03] dark:bg-white/[0.05] rounded-lg">
241+
<dt class="font-medium text-[11px] text-neutral-500 truncate dark:text-neutral-400">{{ m.label }}</dt>
242+
<dd class="mt-0.5 font-semibold text-[14px] text-neutral-900 tracking-tight dark:text-white">{{ m.value }}</dd>
210243
</div>
211244
@endforeach
212245
</dl>
213246
</div>
214-
@endif
247+
</div>
215248
</Card>
216249
@endforeach
217250
</div>

0 commit comments

Comments
 (0)