From e767ca210577b73453341f0c20e0a4de9280c45f Mon Sep 17 00:00:00 2001 From: Kemal Yaylali Date: Sat, 15 Aug 2026 22:50:20 +0100 Subject: [PATCH] Move tabs into the header, fix Conditions ahead overflow The tab bar was its own grid row costing about 70 px of vertical space on every tab to hold five buttons, which is a poor trade on a layout that refuses to scroll. The tablist now sits in the header between the title and the status block, so the shell drops from three rows to two. That space goes to the pressure tendency chart, which was 56 px and had no room for Chart.js to lay out its tick row: measured, the caption sat 2 px below its own card. The chart is now 80 px with explicit layout padding, and the column has 11 px of slack instead of overflowing. Also made the tendency x-axis adaptive. Hour-only labels collapsed to three identical ticks on a short window, which is what a young station always has. Below a six hour span the label now carries minutes. Verified at 1600x900: Live, History, Models and Nerd all zero scrollbars, zero clipping, zero console errors. Tabs stay reachable and the header stays one row at 1280 and 1024 wide. Methods improved incidentally, from 68 px of overflow to 4. --- ashvale/dashboard.py | 55 +++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/ashvale/dashboard.py b/ashvale/dashboard.py index 5c90eb7..36dafd0 100644 --- a/ashvale/dashboard.py +++ b/ashvale/dashboard.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""The dashboard: five tabs, one viewport, no scrolling. +"""The dashboard: five tabs in the header, one viewport, no scrolling. Layout contract. The page is a fixed three-row grid pinned to the viewport height: header, tab bar, then a content region that takes the @@ -94,10 +94,13 @@ DASHBOARD_HTML = r"""
-
+
-
-
+ +
+
-
+ + +
health @@ -120,13 +131,6 @@ DASHBOARD_HTML = r"""
-
@@ -256,8 +260,8 @@ DASHBOARD_HTML = r""" pressure, last 24 h --
-
-

The only signal that sees past your walls. Its slope drives the forecast.

+
+

Slope, not level, drives the forecast.

@@ -606,7 +610,7 @@ function applyPrecip(p) { // decide which glyph honestly represents it. const wx = wxPick(p.condition, lastDerived.cloud_index, lastDerived.solar_elevation, lastDerived.temperature, p.rain_probability); - el('c-icon').innerHTML = wxSvg(wx[0], 56); + el('c-icon').innerHTML = wxSvg(wx[0], 44); el('c-sky').innerText = wx[1]; el('c-z').innerText = p.zambretti_z!==undefined ? p.zambretti_z : '-'; el('c-trend').innerText = p.pressure_characteristic||'-'; @@ -663,9 +667,24 @@ charts.tend = new Chart(el('tendChart').getContext('2d'), { data:{ datasets:[{ data:[], borderColor:'#a78bfa', backgroundColor:'rgba(167,139,250,.12)', fill:true, borderWidth:1.6, pointRadius:0, tension:.3, parsing:false }] }, options:{ responsive:true, maintainAspectRatio:false, animation:false, - scales:{ x:{ type:'linear', grid:{display:false}, ticks:{ color:'#475569', font:{family:'JetBrains Mono',size:8}, maxTicksLimit:4, - callback:v=>new Date(v*1000).toLocaleTimeString([], {hour:'2-digit'}) } }, - y:{ grid:{color:GRID}, ticks:{ color:'#a78bfa', font:{family:'JetBrains Mono',size:8}, maxTicksLimit:4, callback:v=>v.toFixed(0) } } }, + // Chart.js lays the tick row out inside the canvas. At the old 56 px there + // was not enough of it left, so the labels crowded the edge and the caption + // spilled past the card. The layout padding reserves that strip explicitly. + layout:{ padding:{ bottom:2, left:2, right:4 } }, + scales:{ x:{ type:'linear', grid:{display:false}, + ticks:{ color:'#64748b', font:{family:'JetBrains Mono',size:9}, maxTicksLimit:4, + maxRotation:0, minRotation:0, padding:2, + // Hour-only labels collapse to three identical ticks when the window is + // short, which happens on a young station. Add minutes below six hours. + callback:function (v) { + const sc = this.chart.scales.x; + const span = (sc.max - sc.min) || 0; + const opts = span > 6 * 3600 + ? {hour: '2-digit'} + : {hour: '2-digit', minute: '2-digit'}; + return new Date(v * 1000).toLocaleTimeString([], opts); + } } }, + y:{ grid:{color:GRID}, ticks:{ color:'#a78bfa', font:{family:'JetBrains Mono',size:9}, maxTicksLimit:4, padding:2, callback:v=>v.toFixed(0) } } }, plugins:{ legend:{display:false}, tooltip:{ backgroundColor:'rgba(15,23,42,.95)', titleFont:MONO, callbacks:{ title:i=>tsFmt(i[0].parsed.x) } } } } });