feat rework status page nodes

Replace the status table with probe-grouped node sequences and live state markers. Add narrow and wide page containers, placing the toolbar within each page container. Update dashboard page coverage for the new status and layout structure.

Commit
ca17e3b00282fad4ba43595e01c224814363e940
Author
GPT-5 default <codex@openai.com>
Author date
Committer
GPT-5 default <codex@openai.com>
Committer date
Changed files
roles/dashboard/lib/FAPG/DAQ/Dashboard/Controller/Dashboard.pm
index 6b775daa..4c88e057 100644..100644
@@ -25,9 +25,10 @@
25 25
26 26 sub probe_list ($self) {
27 27 $self->render(
28 Removed: template => 'dashboard/probes',
29 Removed: probes => $self->probes,
30 Removed: breadcrumbs =>
28 Added: template => 'dashboard/probes',
29 Added: probes => $self->probes,
30 Added: container_class => 'container-wide',
31 Added: breadcrumbs =>
31 32 [ { label => 'Home', href => '/' }, { label => 'Probes' }, ],
32 33 graph_control_aria_label => 'All probe chart timeframes',
33 34 );
roles/dashboard/public/css/app.css
index 4f3b0664..221ec3cd 100644..100644
@@ -27,11 +27,20 @@
27 27 color: var(--text);
28 28 }
29 29
30 Removed: .site-main {
30 Added: .container,
31 Added: .container-wide {
31 32 width: calc(100% - 2rem);
32 33 margin: 0 auto 2rem;
33 34 }
34 35
36 Added: .container {
37 Added: max-width: 56rem;
38 Added: }
39 Added:
40 Added: .container-wide {
41 Added: max-width: 100rem;
42 Added: }
43 Added:
35 44 .site-footer {
36 45 width: min(1100px, calc(100% - 2rem));
37 46 margin: 0 auto 2rem;
@@ -72,8 +81,8 @@
72 81 align-items: center;
73 82 justify-content: space-between;
74 83 gap: 1rem;
75 Removed: width: calc(100% - 2rem);
76 Removed: margin: 0 auto 1rem;
84 Added: width: 100%;
85 Added: margin: 0 0 1rem;
77 86 }
78 87
79 88 .breadcrumbs {
@@ -1011,15 +1020,122 @@
1011 1020 border-bottom: 0;
1012 1021 }
1013 1022
1014 Removed: .status-table {
1015 Removed: min-width: 760px;
1023 Added: .status-probe-groups {
1024 Added: display: grid;
1025 Added: gap: 1.5rem;
1016 1026 }
1017 1027
1018 Removed: .status-table .status-pill {
1028 Added: .status-probe-group h2 {
1029 Added: margin: 0 0 0.75rem;
1030 Added: font-size: 1rem;
1031 Added: }
1032 Added:
1033 Added: .status-node-sequence {
1034 Added: margin: 0;
1035 Added: padding: 0;
1036 Added: list-style: none;
1037 Added: }
1038 Added:
1039 Added: .status-node {
1040 Added: position: relative;
1041 Added: display: grid;
1042 Added: grid-template-columns: 1.25rem minmax(0, 1fr);
1043 Added: gap: 0.75rem;
1044 Added: }
1045 Added:
1046 Added: .status-node + .status-node {
1047 Added: margin-top: 0.75rem;
1048 Added: }
1049 Added:
1050 Added: .status-node + .status-node::before {
1051 Added: position: absolute;
1052 Added: top: -0.75rem;
1053 Added: bottom: calc(100% - 0.75rem);
1054 Added: left: 0.55rem;
1055 Added: width: 0.15rem;
1056 Added: border-radius: 999px;
1057 Added: background: var(--border);
1058 Added: content: "";
1059 Added: }
1060 Added:
1061 Added: .status-node-marker {
1062 Added: z-index: 1;
1063 Added: width: 1.25rem;
1064 Added: height: 1.25rem;
1065 Added: margin-top: 1rem;
1066 Added: border: 0.3rem solid var(--unknown-soft);
1067 Added: border-radius: 50%;
1068 Added: background: var(--unknown);
1069 Added: }
1070 Added:
1071 Added: .status-node-marker.is-online,
1072 Added: .status-node-marker.is-healthy {
1073 Added: border-color: var(--accent-soft);
1074 Added: background: var(--accent);
1075 Added: }
1076 Added:
1077 Added: .status-node-marker.is-stale,
1078 Added: .status-node-marker.is-probe-error {
1079 Added: border-color: var(--warn-soft);
1080 Added: background: var(--warn);
1081 Added: }
1082 Added:
1083 Added: .status-node-marker.is-offline,
1084 Added: .status-node-marker.is-unreachable {
1085 Added: border-color: var(--danger-soft);
1086 Added: background: var(--danger);
1087 Added: }
1088 Added:
1089 Added: .status-node-content {
1090 Added: min-width: 0;
1091 Added: border: 1px solid var(--border);
1092 Added: border-radius: 0.5rem;
1093 Added: padding: 1rem;
1094 Added: background: #fafcfb;
1095 Added: }
1096 Added:
1097 Added: .status-node-summary {
1098 Added: display: flex;
1099 Added: flex-wrap: wrap;
1100 Added: align-items: center;
1101 Added: justify-content: space-between;
1102 Added: gap: 0.75rem;
1103 Added: }
1104 Added:
1105 Added: .status-node-name {
1106 Added: min-width: 0;
1107 Added: margin: 0;
1108 Added: overflow-wrap: anywhere;
1109 Added: font-weight: 700;
1110 Added: }
1111 Added:
1112 Added: .status-node-summary .status-pill {
1019 1113 min-width: 7rem;
1020 1114 padding-block: 0.3rem;
1021 1115 }
1022 1116
1117 Added: .status-node-details {
1118 Added: display: grid;
1119 Added: grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
1120 Added: gap: 0.75rem 1rem;
1121 Added: margin: 1rem 0 0;
1122 Added: }
1123 Added:
1124 Added: .status-node-details div {
1125 Added: min-width: 0;
1126 Added: }
1127 Added:
1128 Added: .status-node-details dt {
1129 Added: color: var(--muted);
1130 Added: font-size: 0.78rem;
1131 Added: font-weight: 700;
1132 Added: }
1133 Added:
1134 Added: .status-node-details dd {
1135 Added: margin: 0.25rem 0 0;
1136 Added: overflow-wrap: anywhere;
1137 Added: }
1138 Added:
1023 1139 @media (max-width: 900px) {
1024 1140 .reading-quick-grid {
1025 1141 grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1027,7 +1143,8 @@
1027 1143 }
1028 1144
1029 1145 @media (max-width: 700px) {
1030 Removed: .site-main {
1146 Added: .container,
1147 Added: .container-wide {
1031 1148 width: min(100% - 1rem, 1100px);
1032 1149 margin: 0 auto 1rem;
1033 1150 }
@@ -1040,7 +1157,7 @@
1040 1157 .toolbar {
1041 1158 flex-direction: column;
1042 1159 align-items: stretch;
1043 Removed: width: min(100% - 1rem, 1100px);
1160 Added: width: 100%;
1044 1161 margin-bottom: 1rem;
1045 1162 }
1046 1163
@@ -1208,6 +1325,16 @@
1208 1325 padding-inline: 0.65rem;
1209 1326 text-align: center;
1210 1327 white-space: normal;
1328 Added: }
1329 Added:
1330 Added: .status-node-summary .status-pill {
1331 Added: width: auto;
1332 Added: min-height: 0;
1333 Added: }
1334 Added:
1335 Added: .status-node-details {
1336 Added: grid-template-columns: 1fr;
1337 Added: gap: 0.75rem;
1211 1338 }
1212 1339
1213 1340 .download-range-control {
roles/dashboard/public/js/dashboard/status.js
index 0ae19c97..c3212b2a 100644..100644
@@ -118,9 +118,11 @@
118 118 const state = stateForNodeStatus(status);
119 119 const timestamp = status?.received_at || status?.timestamp;
120 120 const stateElement = row.querySelector("[data-status-page-state]");
121 Added: const marker = row.querySelector(".status-node-marker");
121 122
122 123 stateElement.className = `status-pill is-${state}`;
123 124 stateElement.textContent = statusLabel(state);
125 Added: marker.className = `status-node-marker is-${state}`;
124 126 row.querySelector("[data-status-page-node]").textContent = status?.node || "Unknown";
125 127 row.querySelector("[data-status-page-last-seen]").textContent = timestamp
126 128 ? `${formatDateTime(timestamp)} (${formatAge(ageMs(timestamp))})`
roles/dashboard/t/00-homepage.t
index 744264cf..7bc7e048 100644..100644
@@ -14,7 +14,7 @@
14 14 $t->get_ok('/')
15 15 ->status_is(200)
16 16 ->element_exists(
17 Removed: 'body > header.page-header + div.toolbar + main.site-main')
17 Added: 'body > header.page-header + main.container > div.toolbar')
18 18 ->text_is( 'a.page-brand[href="/"]', 'FAPG' )
19 19 ->text_is( 'div.nav-links a[href="/probes"]', 'Probes' )
20 20 ->text_is( 'div.nav-links a[href="/downloads"]', 'Downloads' )
roles/dashboard/t/01-graph-page.t
index d5a56f18..6f9d1865 100644..100644
@@ -43,6 +43,7 @@
43 43 ->status_is(200)
44 44 ->text_is( 'nav.breadcrumbs a[href="/"]', 'Home' )
45 45 ->text_is( 'nav.breadcrumbs [aria-current="page"]', 'Probes' )
46 Added: ->element_exists('main.container-wide')
46 47 ->element_exists('div.probes-grid')
47 48 ->element_exists('div.toolbar-items div.graph-control');
48 49
roles/dashboard/t/05-status-page.t
index 74501d50..512f1d2a 100644..100644
@@ -16,12 +16,12 @@
16 16 ->text_is( 'nav.breadcrumbs a[href="/"]', 'Home' )
17 17 ->text_is( 'nav.breadcrumbs [aria-current="page"]', 'Status' )
18 18 ->text_is( 'h1', 'DAQ Status' )
19 Removed: ->element_exists('table.status-table')
20 Removed: ->element_exists('table.status-table thead th[scope="col"]');
19 Added: ->element_exists('main.container')
20 Added: ->element_exists('[data-status-page-groups]');
21 21
22 22 for my $item (qw(hub ph do orp ec)) {
23 23 $t->element_exists(
24 Removed: qq{tr[data-status-page-item="$item"] [data-status-page-state]});
24 Added: qq{[data-status-page-probe="$item"] [data-status-page-item="$item"] [data-status-page-state]});
25 25 }
26 26
27 27 done_testing;
roles/dashboard/templates/dashboard/status.html.ep
index 1a9de88a..497df237 100644..100644
@@ -12,29 +12,36 @@
12 12 </div>
13 13
14 14 <div class="card-body">
15 Removed: <div class="message-table-wrap">
16 Removed: <table class="message-table status-table">
17 Removed: <thead>
18 Removed: <tr>
19 Removed: <th scope="col">Probe</th>
20 Removed: <th scope="col">Node</th>
21 Removed: <th scope="col">Status</th>
22 Removed: <th scope="col">Last seen online</th>
23 Removed: <th scope="col">Message</th>
24 Removed: </tr>
25 Removed: </thead>
26 Removed: <tbody data-status-page-rows>
27 Removed: % for my $item ($items->@*) {
28 Removed: <tr data-status-page-item="<%= $item->{key} %>">
29 Removed: <td data-status-page-label><%= $item->{label} %></td>
30 Removed: <td data-status-page-node>Checking…</td>
31 Removed: <td><span class="status-pill is-unknown" data-status-page-state>Checking</span></td>
32 Removed: <td data-status-page-last-seen>Checking…</td>
33 Removed: <td data-status-page-message>Checking status…</td>
34 Removed: </tr>
35 Removed: % }
36 Removed: </tbody>
37 Removed: </table>
15 Added: <div class="status-probe-groups" data-status-page-groups>
16 Added: % for my $item ($items->@*) {
17 Added: <section
18 Added: class="status-probe-group"
19 Added: data-status-page-probe="<%= $item->{key} %>"
20 Added: aria-labelledby="status-probe-<%= $item->{key} %>">
21 Added: <h2 id="status-probe-<%= $item->{key} %>" data-status-page-label><%= $item->{label} %></h2>
22 Added: <ol class="status-node-sequence" aria-label="<%= $item->{label} %> nodes">
23 Added: <li class="status-node" data-status-page-item="<%= $item->{key} %>">
24 Added: <span class="status-node-marker is-unknown" aria-hidden="true"></span>
25 Added: <div class="status-node-content">
26 Added: <div class="status-node-summary">
27 Added: <p class="status-node-name" data-status-page-node>Checking…</p>
28 Added: <span class="status-pill is-unknown" data-status-page-state>Checking</span>
29 Added: </div>
30 Added: <dl class="status-node-details">
31 Added: <div>
32 Added: <dt>Last status</dt>
33 Added: <dd data-status-page-last-seen>Checking…</dd>
34 Added: </div>
35 Added: <div>
36 Added: <dt>Message</dt>
37 Added: <dd data-status-page-message>Checking status…</dd>
38 Added: </div>
39 Added: </dl>
40 Added: </div>
41 Added: </li>
42 Added: </ol>
43 Added: </section>
44 Added: % }
38 45 </div>
39 46 </div>
40 47 </div>
roles/dashboard/templates/layouts/default.html.ep
index e1dbf916..0fe68b4a 100644..100644
@@ -39,8 +39,8 @@
39 39 data-header-status-label="<%= $item->{label} %>"></span>
40 40 % }
41 41 </header>
42 Removed: %= include 'dashboard/toolbar', breadcrumbs => (stash('breadcrumbs') // []), graph_control_aria_label => stash('graph_control_aria_label'), show_overview_status => stash('show_overview_status')
43 Removed: <main class="site-main">
42 Added: <main class="<%= stash('container_class') // 'container' %>">
43 Added: %= include 'dashboard/toolbar', breadcrumbs => (stash('breadcrumbs') // []), graph_control_aria_label => stash('graph_control_aria_label'), show_overview_status => stash('show_overview_status')
44 44 <%= content %>
45 45 </main>
46 46 <footer class="site-footer">