|
4 | 4 | <script server> |
5 | 5 | definePageMeta({ |
6 | 6 | middleware: ['auth'], |
7 | | - layout: 'default' |
| 7 | + layout: 'default', |
8 | 8 | }) |
9 | 9 |
|
| 10 | +import PageHeader from '../../../components/Dashboard/UI/PageHeader.stx' |
10 | 11 | import Card from '../../../components/Dashboard/UI/Card.stx' |
11 | 12 | import Badge from '../../../components/Dashboard/UI/Badge.stx' |
12 | | -import StatsCard from '../../../components/Dashboard/UI/StatsCard.stx' |
13 | 13 |
|
14 | | -interface HealthMetric { |
| 14 | +interface Metric { |
| 15 | + label: string |
| 16 | + value: string |
| 17 | +} |
| 18 | + |
| 19 | +interface HealthCheck { |
15 | 20 | name: string |
16 | | - icon: string |
17 | 21 | status: 'success' | 'info' | 'warning' | 'danger' |
18 | 22 | 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[] |
20 | 26 | } |
21 | 27 |
|
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[] = [ |
23 | 34 | { |
24 | 35 | name: 'Uptime', |
25 | | - icon: 'i-hugeicons-flash', |
26 | 36 | 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', |
28 | 39 | 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%' }, |
32 | 43 | ], |
33 | 44 | }, |
34 | 45 | { |
35 | 46 | name: 'Performance', |
36 | | - icon: 'i-hugeicons-rocket-01', |
37 | 47 | 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', |
39 | 50 | 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%' }, |
42 | 53 | ], |
43 | 54 | }, |
44 | 55 | { |
45 | | - name: 'Certificate Health', |
46 | | - icon: 'i-hugeicons-shield-01', |
| 56 | + name: 'Certificate health', |
47 | 57 | 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', |
49 | 60 | 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' }, |
52 | 63 | ], |
53 | 64 | }, |
54 | 65 | { |
55 | | - name: 'Broken Links', |
56 | | - icon: 'i-hugeicons-link-01', |
| 66 | + name: 'Broken links', |
57 | 67 | 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', |
59 | 70 | 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' }, |
62 | 73 | ], |
63 | 74 | }, |
64 | 75 | { |
65 | | - name: 'Mixed Content', |
66 | | - icon: 'i-hugeicons-lock-01', |
| 76 | + name: 'Mixed content', |
67 | 77 | 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', |
69 | 80 | 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' }, |
72 | 83 | ], |
73 | 84 | }, |
74 | 85 | { |
75 | | - name: 'Certificate Transparency', |
76 | | - icon: 'i-hugeicons-file-security', |
| 86 | + name: 'Certificate transparency', |
77 | 87 | 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', |
79 | 90 | 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' }, |
82 | 93 | ], |
83 | 94 | }, |
84 | 95 | { |
85 | | - name: 'Application Health', |
86 | | - icon: 'i-hugeicons-checkmark-circle-02', |
| 96 | + name: 'Application health', |
87 | 97 | 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', |
89 | 100 | 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' }, |
93 | 104 | ], |
94 | 105 | }, |
95 | 106 | { |
96 | 107 | name: 'DNS', |
97 | | - icon: 'i-hugeicons-globe-02', |
98 | 108 | 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', |
100 | 111 | metrics: [ |
101 | | - { label: 'DNS Records', value: '18' }, |
102 | | - { label: 'URLs Crawled', value: '20' }, |
| 112 | + { label: 'Records', value: '18' }, |
| 113 | + { label: 'URLs crawled', value: '20' }, |
103 | 114 | ], |
104 | 115 | }, |
105 | 116 | ] |
106 | 117 |
|
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', |
115 | 139 | } |
116 | 140 |
|
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' |
125 | 149 | } |
126 | 150 | </script> |
127 | 151 |
|
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> |
161 | 216 | </div> |
162 | 217 |
|
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> |
171 | 222 | <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> |
175 | 227 | </div> |
176 | 228 |
|
177 | | - <!-- Content --> |
178 | 229 | <div class="flex-1 min-w-0"> |
179 | 230 | <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> |
186 | 233 | </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 }} |
189 | 236 | </p> |
190 | | - </div> |
191 | | - </div> |
192 | 237 |
|
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> |
210 | 243 | </div> |
211 | 244 | @endforeach |
212 | 245 | </dl> |
213 | 246 | </div> |
214 | | - @endif |
| 247 | + </div> |
215 | 248 | </Card> |
216 | 249 | @endforeach |
217 | 250 | </div> |
|
0 commit comments