feat navbar page title and mobile timeframe dropdown

Show a centered page title in the main navbar on all screen sizes. Collapse timeframe buttons into a dropdown on mobile; keep full button group on md+. Remove redundant inline h1 elements from page content.

Commit
f9026e935f8fc99cb323972ff0abf1a4bc1e7407
Author
gpt-5.6-sol high <agent@openai.com>
Author date
Committer
gpt-5.6-sol high <agent@openai.com>
Committer date
Changed files
roles/dashboard/lib/FAPG/DAQ/Dashboard/Controller/Pages.pm
index 0cf81846..e67f99b8 100644..100644
@@ -12,17 +12,19 @@
12 12
13 13 sub status ($self) {
14 14 $self->render(
15 Removed: template => 'dashboard/status',
16 Removed: items => $self->status_items,
17 Removed: nav_page => 'probes',
15 Added: template => 'dashboard/status',
16 Added: items => $self->status_items,
17 Added: nav_page => 'probes',
18 Added: page_title => 'Status',
18 19 );
19 20 }
20 21
21 22 sub probe_list ($self) {
22 23 $self->render(
23 Removed: template => 'dashboard/probes',
24 Removed: probes => $self->probes,
25 Removed: nav_page => 'probes',
24 Added: template => 'dashboard/probes',
25 Added: probes => $self->probes,
26 Added: nav_page => 'probes',
27 Added: page_title => 'Probe Readings',
26 28 );
27 29 }
28 30
@@ -44,21 +46,24 @@
44 46 oldest => $oldest,
45 47 probes => $self->probes,
46 48 nav_page => 'downloads',
49 Added: page_title => 'Downloads',
47 50 );
48 51 }
49 52
50 53 sub insights ($self) {
51 54 $self->render(
52 Removed: template => 'dashboard/insights',
53 Removed: probes => $self->probes,
54 Removed: nav_page => 'insights',
55 Added: template => 'dashboard/insights',
56 Added: probes => $self->probes,
57 Added: nav_page => 'insights',
58 Added: page_title => 'Insights',
55 59 );
56 60 }
57 61
58 62 sub weather ($self) {
59 63 $self->render(
60 Removed: template => 'dashboard/weather',
61 Removed: nav_page => 'weather',
64 Added: template => 'dashboard/weather',
65 Added: nav_page => 'weather',
66 Added: page_title => 'Weather',
62 67 );
63 68 }
64 69
@@ -73,6 +78,7 @@
73 78 probe => $probe,
74 79 nav_page => 'probes',
75 80 single_probe => 1,
81 Added: page_title => $probe->{label},
76 82 );
77 83 }
78 84
roles/dashboard/public/js/dashboard/graph-controls.js
index ff36afe3..a209f3a2 100644..100644
@@ -21,6 +21,12 @@
21 21 button.setAttribute("aria-pressed", active ? "true" : "false");
22 22 });
23 23
24 Added: document.querySelectorAll("[data-timeframe-dropdown-label]").forEach(el => {
25 Added: el.textContent = document.querySelector(
26 Added: `.timeframe-button[data-timeframe="${timeframe.selected()}"]`
27 Added: )?.dataset.timeframeLabel || "Daily";
28 Added: });
29 Added:
24 30 document.querySelectorAll("[data-range-label]").forEach(label => {
25 31 label.textContent = timeframeRangeLabel(timeframe.selected(), range);
26 32 });
roles/dashboard/t/01-graph-page.t
index ee0a5b69..c13e38ae 100644..100644
@@ -11,7 +11,8 @@
11 11
12 12 $t->get_ok('/probes/ph')
13 13 ->status_is(200)
14 Removed: ->text_is( 'h1', 'pH' )
14 Added: ->element_exists('.navbar-text')
15 Added: ->text_like('.navbar-text', qr/pH/)
15 16 ->element_exists('.sticky-controls .graph-control')
16 17 ->text_is( '.card-header h1 a[href="/probes/ph"]', 'pH' )
17 18 ->element_exists('.navbar-nav a.nav-link.active.dropdown-toggle')
@@ -40,7 +41,7 @@
40 41
41 42 $t->get_ok('/probes')
42 43 ->status_is(200)
43 Removed: ->text_is( 'h1', 'Probes' )
44 Added: ->text_like('.navbar-text', qr/Probe Readings/)
44 45 ->element_exists('.sticky-controls .graph-control')
45 46 ->element_exists('main.container')
46 47 ->element_exists('div.probes-grid')
roles/dashboard/t/05-status-page.t
index 6744b806..4ffcc162 100644..100644
@@ -11,7 +11,7 @@
11 11
12 12 $t->get_ok('/probes/status')
13 13 ->status_is(200)
14 Removed: ->text_is( 'h1', 'Status' )
14 Added: ->text_like('.navbar-text', qr/Status/)
15 15 ->element_exists('main.container')
16 16 ->element_exists('.list-group');
17 17
roles/dashboard/t/06-downloads.t
index 7c2cdd75..e04c5713 100644..100644
@@ -11,7 +11,7 @@
11 11
12 12 $t->get_ok('/downloads')
13 13 ->status_is(200)
14 Removed: ->text_is( 'h1', 'Downloads' )
14 Added: ->text_like('.navbar-text', qr/Downloads/)
15 15 ->element_exists('.navbar-nav a.nav-link.active[href="/downloads"][aria-current="page"]')
16 16 ->element_exists('form.downloads-form[action="/downloads/readings.csv"]')
17 17 ->element_exists('[data-downloads-error]')
roles/dashboard/t/08-insights.t
index 94ef847c..a026eab5 100644..100644
@@ -13,7 +13,7 @@
13 13 $t->get_ok('/insights')
14 14 ->status_is(200)
15 15 ->text_is('title', 'FAPG DAQ Insights')
16 Removed: ->text_is('h1', 'Insights')
16 Added: ->text_like('.navbar-text', qr/Insights/)
17 17 ->element_exists('.navbar-nav a.nav-link.active[href="/insights"]')
18 18 ->element_exists('[data-default-timeframe="day"]');
19 19 };
roles/dashboard/templates/dashboard/downloads.html.ep
index bf3c4f7f..74b31f53 100644..100644
@@ -2,10 +2,6 @@
2 2 % title 'FAPG DAQ Downloads';
3 3
4 4 <section class="mb-4" aria-label="Data downloads">
5 Removed: <div class="mb-3">
6 Removed: <h1 class="h4 fw-bold mb-1">Downloads</h1>
7 Removed: <p class="text-body-secondary small mb-0">Download recorded DAQ readings as a CSV or Excel file.</p>
8 Removed: </div>
9 5
10 6 <div class="card">
11 7
roles/dashboard/templates/dashboard/graph-control.html.ep
index 732ef757..209d411b 100644..100644
@@ -1,29 +1,49 @@
1 1 % my $dt_attr = stash('default_timeframe') ? 'data-default-timeframe="' . stash('default_timeframe') . '"' : '';
2 Removed: <div class="graph-control-scroll overflow-x-auto" tabindex="0" role="region" aria-label="<%= $aria_label %>" <%== $dt_attr %>> <div class="graph-control d-inline-flex gap-2 flex-nowrap">
3 Removed: <div class="input-group input-group-sm range-control" aria-label="Adjust range">
4 Removed: <button
5 Removed: class="btn btn-outline-secondary"
6 Removed: type="button"
7 Removed: data-range-adjustment="decrease"
8 Removed: aria-label="Decrease range">−</button>
9 Removed: <span
10 Removed: class="input-group-text range-label"
11 Removed: data-range-label
12 Removed: aria-live="polite">1</span>
13 Removed: <button
14 Removed: class="btn btn-outline-secondary"
15 Removed: type="button"
16 Removed: data-range-adjustment="increase"
17 Removed: aria-label="Increase range">+</button>
18 Removed: </div>
19 Removed: <div class="btn-group timeframe-control" role="group" aria-label="Select timeframe">
2 Added: <div class="graph-control d-inline-flex gap-2 flex-nowrap align-items-center" <%== $dt_attr %>>
3 Added: <div class="input-group input-group-sm range-control" aria-label="Adjust range">
4 Added: <button
5 Added: class="btn btn-outline-secondary"
6 Added: type="button"
7 Added: data-range-adjustment="decrease"
8 Added: aria-label="Decrease range">−</button>
9 Added: <span
10 Added: class="input-group-text range-label"
11 Added: data-range-label
12 Added: aria-live="polite">1</span>
13 Added: <button
14 Added: class="btn btn-outline-secondary"
15 Added: type="button"
16 Added: data-range-adjustment="increase"
17 Added: aria-label="Increase range">+</button>
18 Added: </div>
19 Added: <%# Full button group for md+ screens %>
20 Added: <div class="btn-group timeframe-control d-none d-md-inline-flex" role="group" aria-label="Select timeframe">
21 Added: % for my $timeframe ( [ hour => 'Hourly' ], [ day => 'Daily' ], [ week => 'Weekly' ], [ month => 'Monthly' ], [ year => 'Yearly' ] ) {
22 Added: <button
23 Added: class="btn btn-sm btn-outline-secondary timeframe-button"
24 Added: type="button"
25 Added: data-timeframe="<%= $timeframe->[0] %>"
26 Added: data-timeframe-label="<%= $timeframe->[1] %>"><%= $timeframe->[1] %></button>
27 Added: % }
28 Added: </div>
29 Added: <%# Dropdown for small screens %>
30 Added: <div class="dropdown d-md-none timeframe-control">
31 Added: <button
32 Added: class="btn btn-sm btn-outline-secondary dropdown-toggle timeframe-dropdown-toggle"
33 Added: type="button"
34 Added: data-bs-toggle="dropdown"
35 Added: aria-expanded="false"
36 Added: data-timeframe-dropdown-label>Daily</button>
37 Added: <ul class="dropdown-menu dropdown-menu-end">
20 38 % for my $timeframe ( [ hour => 'Hourly' ], [ day => 'Daily' ], [ week => 'Weekly' ], [ month => 'Monthly' ], [ year => 'Yearly' ] ) {
21 Removed: <button
22 Removed: class="btn btn-sm btn-outline-secondary timeframe-button"
23 Removed: type="button"
24 Removed: data-timeframe="<%= $timeframe->[0] %>"
25 Removed: data-timeframe-label="<%= $timeframe->[1] %>"><%= $timeframe->[1] %></button>
39 Added: <li>
40 Added: <button
41 Added: class="dropdown-item timeframe-button"
42 Added: type="button"
43 Added: data-timeframe="<%= $timeframe->[0] %>"
44 Added: data-timeframe-label="<%= $timeframe->[1] %>"><%= $timeframe->[1] %></button>
45 Added: </li>
26 46 % }
27 Removed: </div>
47 Added: </ul>
28 48 </div>
29 49 </div>
roles/dashboard/templates/dashboard/graph.html.ep
index b2f15199..2203d615 100644..100644
@@ -3,8 +3,7 @@
3 3 % stash use_charts => 1;
4 4
5 5 <section class="mb-4" aria-label="<%= $probe->{label} %> probe detail">
6 Removed: <div class="d-flex flex-column flex-sm-row align-items-start align-items-sm-center justify-content-between gap-2 mb-3 sticky-controls">
7 Removed: <h1 class="h4 fw-bold mb-0"><%= $probe->{label} %></h1>
6 Added: <div class="d-flex align-items-center justify-content-end gap-2 mb-3 sticky-controls">
8 7 %= include 'dashboard/graph-control', aria_label => "$probe->{label} chart timeframe", default_timeframe => 'day'
9 8 </div>
10 9
roles/dashboard/templates/dashboard/insights.html.ep
index 9c24068d..0ddaec75 100644..100644
@@ -3,11 +3,7 @@
3 3 % stash use_charts => 1;
4 4
5 5 <section class="mb-4" aria-label="Insights visualisations">
6 Removed: <div class="d-flex flex-column flex-sm-row align-items-start align-items-sm-center justify-content-between gap-2 mb-3 sticky-controls">
7 Removed: <div>
8 Removed: <h1 class="h4 fw-bold mb-1">Insights</h1>
9 Removed: <p class="text-body-secondary small mb-0">Derived visualisations for system monitoring</p>
10 Removed: </div>
6 Added: <div class="d-flex align-items-center justify-content-end gap-2 mb-3 sticky-controls">
11 7 %= include 'dashboard/graph-control', aria_label => 'Insights timeframe', default_timeframe => 'day'
12 8 </div>
13 9
roles/dashboard/templates/dashboard/probes.html.ep
index 95ac1a41..11c0bd63 100644..100644
@@ -3,8 +3,7 @@
3 3 % stash use_charts => 1;
4 4
5 5 <section class="mb-4" aria-label="Probe readings">
6 Removed: <div class="d-flex flex-column flex-sm-row align-items-start align-items-sm-center justify-content-between gap-2 mb-3 sticky-controls">
7 Removed: <h1 class="h4 fw-bold mb-0">Probes</h1>
6 Added: <div class="d-flex align-items-center justify-content-end gap-2 mb-3 sticky-controls">
8 7 %= include 'dashboard/graph-control', aria_label => 'All probe chart timeframes', default_timeframe => 'day'
9 8 </div>
10 9
roles/dashboard/templates/dashboard/status.html.ep
index 749edfd4..92000aee 100644..100644
@@ -2,10 +2,6 @@
2 2 % title 'FAPG DAQ Status';
3 3
4 4 <section class="mb-4" aria-label="DAQ status">
5 Removed: <div class="mb-3">
6 Removed: <h1 class="h4 fw-bold mb-1">Status</h1>
7 Removed: <p class="text-body-secondary small mb-0">Current connection status for the hub and sensor probes.</p>
8 Removed: </div>
9 5
10 6 <div class="card">
11 7
roles/dashboard/templates/dashboard/weather.html.ep
index 7f216515..561b3e45 100644..100644
@@ -3,11 +3,7 @@
3 3 % stash use_charts => 1;
4 4
5 5 <section class="mb-4" aria-label="Weather data">
6 Removed: <div class="d-flex flex-column flex-sm-row align-items-start align-items-sm-center justify-content-between gap-2 mb-3 sticky-controls">
7 Removed: <div>
8 Removed: <h1 class="h4 fw-bold mb-1">Weather</h1>
9 Removed: <p class="text-body-secondary small mb-0">Outdoor conditions at Versonnex — Météo France (AROME)</p>
10 Removed: </div>
6 Added: <div class="d-flex align-items-center justify-content-end gap-2 mb-3 sticky-controls">
11 7 %= include 'dashboard/graph-control', aria_label => 'Weather timeframe', default_timeframe => 'day'
12 8 </div>
13 9
roles/dashboard/templates/layouts/default.html.ep
index 8c2a406a..0431410d 100644..100644
@@ -23,6 +23,12 @@
23 23
24 24 <a class="navbar-brand fw-bold text-success" href="/">FAPG</a>
25 25
26 Added: % my $page_title = stash('page_title') // '';
27 Added: % if ($page_title) {
28 Added: <span class="navbar-text fw-semibold text-body position-absolute start-50 translate-middle-x d-none d-sm-inline" aria-hidden="true"><%= $page_title %></span>
29 Added: <span class="navbar-text fw-semibold text-body mx-auto d-sm-none" aria-hidden="true"><%= $page_title %></span>
30 Added: % }
31 Added:
26 32 <div class="d-flex align-items-center gap-2 ms-auto order-md-last">
27 33 <button
28 34 class="navbar-toggler"