Skip to content

Commit 86e0658

Browse files
Surface acceleration-vs-correctness health in Waterline worker panel
The Health checks card on the workers surface now groups checks by the category field Workflow\V2\Support\HealthCheck returns on every entry, so operators can read correctness-substrate checks (is work being discovered from the durable dispatch tables?) separately from acceleration-layer checks (is the wake backend propagating?). Each category carries its own status rollup derived from the snapshot's categories map when present and from the grouped check list otherwise, so older engine releases that omit the category map still render with a safe default. The long_poll_wake_acceleration check now shows in its own panel with copy reminding operators that degraded acceleration does not mask a correctness failure. Extend V2HealthControllerTest with a scenario that pins the category contract end-to-end: every check exposed through the Waterline v2 health endpoint carries a category of correctness or acceleration, and the long_poll_wake_acceleration check is surfaced and marked acceleration so the separation operators need is observable in the response body Waterline consumes.
1 parent 861b2f5 commit 86e0658

2 files changed

Lines changed: 221 additions & 25 deletions

File tree

resources/js/components/WorkerHealth.vue

Lines changed: 174 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -185,35 +185,57 @@
185185
</article>
186186

187187
<article class="card worker-health__panel">
188-
<div class="card-header d-flex align-items-center justify-content-between">
189-
<div>
190-
<h5 class="mb-0">Health checks</h5>
191-
<small class="text-muted">Operator readiness and compatibility signals.</small>
192-
</div>
193-
194-
<span class="worker-health__pill worker-health__pill--muted">
195-
{{ healthChecks.length.toLocaleString() }} checks
196-
</span>
188+
<div class="card-header">
189+
<h5 class="mb-0">Health checks</h5>
190+
<small class="text-muted">
191+
Correctness answers <em>is work being discovered?</em>; acceleration answers <em>is the acceleration layer propagating?</em>.
192+
</small>
197193
</div>
198194

199-
<div class="card-body card-bg-secondary">
200-
<div v-if="healthChecks.length > 0" class="worker-health__checks">
201-
<article v-for="check in healthChecks" :key="check.name" class="worker-health__check">
202-
<div class="worker-health__check-head">
203-
<span class="worker-health__pill" :class="statusToneClass(check.status)">
204-
{{ check.status }}
205-
</span>
206-
<strong>{{ check.name }}</strong>
195+
<div class="card-body card-bg-secondary worker-health__categories-body">
196+
<section
197+
v-for="category in categorizedChecks"
198+
:key="category.key"
199+
class="worker-health__category"
200+
>
201+
<header class="worker-health__category-header">
202+
<div>
203+
<span class="worker-health__category-eyebrow">{{ category.eyebrow }}</span>
204+
<h6 class="worker-health__category-title">{{ category.title }}</h6>
205+
<p class="worker-health__category-subtitle">{{ category.subtitle }}</p>
207206
</div>
208207

209-
<p class="worker-health__check-copy">{{ check.message }}</p>
210-
</article>
211-
</div>
212-
213-
<div v-else class="worker-health__empty-state worker-health__empty-state--compact">
214-
<strong>No health checks reported</strong>
215-
<p class="mb-0 text-muted">The health endpoint returned no explicit checks.</p>
216-
</div>
208+
<span
209+
class="worker-health__pill"
210+
:class="statusToneClass(category.rollupStatus)"
211+
:title="category.rollupTitle"
212+
>
213+
{{ category.rollupStatus.toUpperCase() }}
214+
</span>
215+
</header>
216+
217+
<div v-if="category.checks.length > 0" class="worker-health__checks">
218+
<article
219+
v-for="check in category.checks"
220+
:key="check.name"
221+
class="worker-health__check"
222+
>
223+
<div class="worker-health__check-head">
224+
<span class="worker-health__pill" :class="statusToneClass(check.status)">
225+
{{ check.status }}
226+
</span>
227+
<strong>{{ check.name }}</strong>
228+
</div>
229+
230+
<p class="worker-health__check-copy">{{ check.message }}</p>
231+
</article>
232+
</div>
233+
234+
<div v-else class="worker-health__empty-state worker-health__empty-state--compact">
235+
<strong>No {{ category.title.toLowerCase() }} checks reported</strong>
236+
<p class="mb-0 text-muted">The health endpoint did not return any checks for this category.</p>
237+
</div>
238+
</section>
217239
</div>
218240
</article>
219241
</section>
@@ -277,6 +299,46 @@ export default {
277299
return this.healthData?.checks || [];
278300
},
279301
302+
categorizedChecks() {
303+
const definitions = [
304+
{
305+
key: 'correctness',
306+
eyebrow: 'Durable substrate',
307+
title: 'Correctness',
308+
subtitle: 'Answers "is work being discovered?" from durable dispatch state.',
309+
rollupTitle: 'Rollup of correctness-category checks.',
310+
},
311+
{
312+
key: 'acceleration',
313+
eyebrow: 'Optional layer',
314+
title: 'Acceleration',
315+
subtitle: 'Answers "is the acceleration layer propagating?". Degraded acceleration never masks correctness.',
316+
rollupTitle: 'Rollup of acceleration-category checks.',
317+
},
318+
];
319+
320+
const grouped = {correctness: [], acceleration: []};
321+
322+
this.healthChecks.forEach((check) => {
323+
if (!check || typeof check !== 'object') return;
324+
325+
if (check.name === 'engine_source') {
326+
return;
327+
}
328+
329+
const category = this.categoryForCheck(check);
330+
grouped[category].push(check);
331+
});
332+
333+
const rollups = this.healthData?.categories || {};
334+
335+
return definitions.map((definition) => ({
336+
...definition,
337+
checks: grouped[definition.key],
338+
rollupStatus: this.rollupForCategory(definition.key, grouped[definition.key], rollups),
339+
}));
340+
},
341+
280342
workersTableClass() {
281343
const classes = ['table', 'table-hover', 'mb-0'];
282344
@@ -510,6 +572,44 @@ export default {
510572
}
511573
},
512574
575+
categoryForCheck(check) {
576+
const declared = typeof check.category === 'string' ? check.category.toLowerCase() : null;
577+
578+
if (declared === 'correctness' || declared === 'acceleration') {
579+
return declared;
580+
}
581+
582+
// Backwards-compatibility: if an older workflow package version
583+
// omits the category field, infer a safe default so the UI still
584+
// renders. Only the wake-acceleration check is known to belong to
585+
// the acceleration category; every other check is durable
586+
// correctness-substrate.
587+
if (check.name === 'long_poll_wake_acceleration') {
588+
return 'acceleration';
589+
}
590+
591+
return 'correctness';
592+
},
593+
594+
rollupForCategory(key, checks, rollups) {
595+
const declared = rollups && rollups[key] && rollups[key].status;
596+
597+
if (declared === 'ok' || declared === 'warning' || declared === 'error') {
598+
return declared;
599+
}
600+
601+
if (!Array.isArray(checks) || checks.length === 0) {
602+
return 'ok';
603+
}
604+
605+
const statuses = checks.map((check) => check.status);
606+
607+
if (statuses.includes('error')) return 'error';
608+
if (statuses.includes('warning')) return 'warning';
609+
610+
return 'ok';
611+
},
612+
513613
statusColor(status) {
514614
return {
515615
ok: 'text-success',
@@ -752,6 +852,55 @@ export default {
752852
min-height: 22rem;
753853
}
754854
855+
.worker-health__categories-body {
856+
display: flex;
857+
flex-direction: column;
858+
gap: 1.4rem;
859+
}
860+
861+
.worker-health__category {
862+
display: flex;
863+
flex-direction: column;
864+
gap: 0.75rem;
865+
padding-bottom: 1.1rem;
866+
border-bottom: 1px solid color-mix(in srgb, var(--wl-text) 8%, transparent);
867+
}
868+
869+
.worker-health__category:last-child {
870+
border-bottom: 0;
871+
padding-bottom: 0;
872+
}
873+
874+
.worker-health__category-header {
875+
display: flex;
876+
align-items: flex-start;
877+
justify-content: space-between;
878+
gap: 0.75rem;
879+
}
880+
881+
.worker-health__category-eyebrow {
882+
display: block;
883+
color: var(--wl-text-soft);
884+
font-family: 'IBM Plex Mono', monospace;
885+
font-size: 0.7rem;
886+
letter-spacing: 0.08em;
887+
text-transform: uppercase;
888+
}
889+
890+
.worker-health__category-title {
891+
margin: 0.25rem 0 0;
892+
color: var(--wl-text);
893+
font-size: 1rem;
894+
font-weight: 600;
895+
}
896+
897+
.worker-health__category-subtitle {
898+
margin: 0.3rem 0 0;
899+
color: var(--wl-text-muted);
900+
font-size: 0.88rem;
901+
line-height: 1.4;
902+
}
903+
755904
.worker-health__checks {
756905
display: grid;
757906
gap: 0.75rem;

tests/Feature/V2HealthControllerTest.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,53 @@ public function testHealthEndpointReturnsV2HealthSnapshot(): void
3939
->assertJsonPath('operator_metrics.backend.supported', true);
4040
}
4141

42+
public function testHealthEndpointCategorizesEveryCheckAndExposesWakeAcceleration(): void
43+
{
44+
config()->set('queue.default', 'redis');
45+
config()->set('queue.connections.redis.driver', 'redis');
46+
config()->set('cache.default', 'file');
47+
48+
$response = $this->get('/waterline/api/v2/health')
49+
->assertStatus(200);
50+
51+
$payload = $response->json();
52+
53+
$this->assertIsArray($payload);
54+
$this->assertArrayHasKey('checks', $payload);
55+
$this->assertArrayHasKey('categories', $payload);
56+
$this->assertArrayHasKey('correctness', $payload['categories']);
57+
$this->assertArrayHasKey('acceleration', $payload['categories']);
58+
59+
$workflowChecks = array_values(array_filter(
60+
$payload['checks'],
61+
static fn (array $check): bool => ($check['name'] ?? null) !== 'engine_source',
62+
));
63+
64+
$wakeChecks = array_values(array_filter(
65+
$workflowChecks,
66+
static fn (array $check): bool => ($check['name'] ?? null) === 'long_poll_wake_acceleration',
67+
));
68+
69+
$this->assertCount(1, $wakeChecks, 'Waterline health must surface the long_poll_wake_acceleration check so operators can read acceleration-layer health.');
70+
$this->assertSame('acceleration', $wakeChecks[0]['category']);
71+
72+
foreach ($workflowChecks as $check) {
73+
$this->assertArrayHasKey('category', $check, sprintf(
74+
'Waterline health check %s must carry a category field.',
75+
$check['name'] ?? 'unknown',
76+
));
77+
$this->assertContains(
78+
$check['category'],
79+
['correctness', 'acceleration'],
80+
sprintf(
81+
'Waterline health check %s has invalid category %s.',
82+
$check['name'] ?? 'unknown',
83+
(string) ($check['category'] ?? ''),
84+
),
85+
);
86+
}
87+
}
88+
4289
public function testHealthEndpointReturnsUnavailableForBlockingBackendIssues(): void
4390
{
4491
config()->set('queue.default', 'sync');

0 commit comments

Comments
 (0)