Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ peach-and-brown palette therefore continue behind and through the dialog views.
names, locations, metrics, switch channels, forecast source, selected theme,
and live values will vary by installation.

The dashboard header centers the Sensorius image between individual pill buttons for
the full-screen graph (left) and General Settings (right).

The dashboard presents:
### Interface Icons

- <img src="../assets/icons/dashboard-graph.svg" alt="Graph icon" width="16"> The
**Graph icon**—a blue line graph on horizontal and vertical axes—opens
the full-screen **Sensorius Graphum** workspace.
- <img src="../assets/icons/settings-gear.svg" alt="Settings icon" width="16"> The
**Settings icon (gear)** beside **Sensorius AI** opens General Settings.
**Settings icon (gear)** to the right of the **Sensorius image** opens General Settings.
Other gear icons open settings for the associated sensor or switch, or open
the timer control for a Switch Tile.

Expand Down
2 changes: 1 addition & 1 deletion sensorius/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
release notes, and supporting tooling can report a consistent build identity.
"""

__version__ = "v0.26.251.8"
__version__ = "v0.26.251.13"
36 changes: 8 additions & 28 deletions sensorius/saiHtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -1686,7 +1686,7 @@ def _status_color_hex(status: str) -> str:
yield "<div class='dashboard-content' style='text-align:center; width:100%;'>"

yield "<h2 id='sensor_header'>"
yield "<a href='javascript:void(0)' onclick='openGraphModal()' title='View Graph' style='margin-right:8px; vertical-align:middle;'>"
yield "<a href='javascript:void(0)' onclick='openGraphModal()' title='View Graph' aria-label='Full Screen Graphs' class='dashboard-header-action'>"
yield " <svg xmlns='http://www.w3.org/2000/svg' class='dashboard-graph-icon' width='22' height='22' viewBox='0 0 24 24' role='img'>"
yield " <title>Full Screen Graphs</title>"
yield " <!-- Y-axis -->"
Expand All @@ -1698,8 +1698,8 @@ def _status_color_hex(status: str) -> str:
yield " fill='none' stroke='blue' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/>"
yield " </svg>"
yield "</a>"
yield f" {APP_NAME_SHORT} "
yield "<a href='#' onclick='window.editSystemSettings && window.editSystemSettings(); return false;' title='Open General Settings' style='margin-left:8px; text-decoration:none; font-size:0.8em; vertical-align:middle;'>"
yield f"<img class='dashboard-brand-image' src='/ui_static/01-sensorius-overview-v5.png?v={APP_VERSION}' alt='Sensorius AI'>"
yield "<a href='#' onclick='window.editSystemSettings && window.editSystemSettings(); return false;' title='Open General Settings' aria-label='Open General Settings' class='dashboard-header-action'>"
yield from _settings_gear_svg_lines(indent=" ")
yield "</a>"
yield "</h2>"
Expand Down Expand Up @@ -2371,7 +2371,7 @@ def _format_stat_value(stat_value):
yield " const ts = document.getElementById('update_time');"
yield " if (ts) {"
yield " const formatted = now.toLocaleString('en-CA', { year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit' }).replace(',', '');"
yield " const weekday = now.toLocaleDateString('en-CA', { weekday:'long' });"
yield " const weekday = now.toLocaleDateString('en-CA', { weekday:'short' });"
yield " ts.textContent = `${weekday} ${formatted}`;"
yield " }"
yield " const biodynamicMinuteKey = `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}-${now.getHours()}-${now.getMinutes()}`;"
Expand Down Expand Up @@ -4360,9 +4360,8 @@ def _format_stat_value(stat_value):
yield ""
yield " const existingRow = document.querySelector('.sensor-row');"
yield " const byGraphModal = document.getElementById('graphModal')?.parentElement || null;"
yield " const overviewFooter = document.getElementById('dashboard-overview-footer');"
yield " const dashboard = document.querySelector('.dashboard');"
yield " const parent = (overviewFooter && overviewFooter.parentElement) || (existingRow && existingRow.parentElement) || dashboard || byGraphModal || document.body;"
yield " const parent = document.querySelector('.dashboard-content') || (existingRow && existingRow.parentElement) || dashboard || byGraphModal || document.body;"
yield " const switchGroup = document.getElementById('group_switches');"
yield ""

Expand All @@ -4378,7 +4377,6 @@ def _format_stat_value(stat_value):
yield " group.style.flex = '0 0 100%';"
yield " group.style.display = 'block';"
yield " if (switchGroup && switchGroup.parentElement === parent) parent.insertBefore(group, switchGroup);"
yield " else if (overviewFooter && overviewFooter.parentElement === parent) parent.insertBefore(group, overviewFooter);"
yield " else parent.appendChild(group);"
yield " }"
yield ""
Expand Down Expand Up @@ -6689,36 +6687,21 @@ def _format_stat_value(stat_value):
yield " if (btn) btn.setAttribute('aria-expanded', 'false');"
yield " });"
yield "};"
yield "window.pinDashboardOverviewFooter = function(){"
yield " const overviewFooter = document.getElementById('dashboard-overview-footer');"
yield " if (!overviewFooter) return;"
yield " const sensorGroup = document.querySelector('.sensor-group[data-sensor-id]');"
yield " const dashboardContent = (sensorGroup && sensorGroup.parentElement) || overviewFooter.parentElement || document.querySelector('.dashboard-content');"
yield " if (!dashboardContent) return;"
yield " if (overviewFooter.parentElement !== dashboardContent || dashboardContent.lastElementChild !== overviewFooter) dashboardContent.appendChild(overviewFooter);"
yield "};"
yield "window.applySensorGroupOrder = function(order){"
yield " if (!Array.isArray(order) || !order.length) return;"
yield " const overviewFooter = document.getElementById('dashboard-overview-footer');"
yield " const firstSensorGroup = document.querySelector('.sensor-group[data-sensor-id]');"
yield " const dashboardContent = (firstSensorGroup && firstSensorGroup.parentElement) || (overviewFooter && overviewFooter.parentElement) || document.querySelector('.dashboard-content');"
yield " const dashboardContent = (firstSensorGroup && firstSensorGroup.parentElement) || document.querySelector('.dashboard-content');"
yield " if (!dashboardContent) return;"
yield " const insertBeforeFooter = (el) => {"
yield " if (!el) return;"
yield " if (overviewFooter && overviewFooter.parentElement === dashboardContent) dashboardContent.insertBefore(el, overviewFooter);"
yield " else dashboardContent.appendChild(el);"
yield " };"
yield " const groups = new Map();"
yield " dashboardContent.querySelectorAll('.sensor-group[data-sensor-id]').forEach((el) => {"
yield " groups.set(el.getAttribute('data-sensor-id') || '', el);"
yield " });"
yield " order.forEach((sid) => {"
yield " const el = groups.get(String(sid || ''));"
yield " if (el) insertBeforeFooter(el);"
yield " if (el) dashboardContent.appendChild(el);"
yield " });"
yield " const switchGroup = document.getElementById('group_switches');"
yield " if (switchGroup) insertBeforeFooter(switchGroup);"
yield " window.pinDashboardOverviewFooter();"
yield " if (switchGroup) dashboardContent.appendChild(switchGroup);"
yield "};"
yield "window.reorderSensorGroup = async function(sensorId, direction){"
yield " const sid = String(sensorId || '').trim();"
Expand Down Expand Up @@ -8172,9 +8155,6 @@ def _format_stat_value(stat_value):

yield "</script>"

yield "<div class='dashboard-overview-graphic' id='dashboard-overview-footer'>"
yield f" <img src='/ui_static/01-sensorius-overview-v5.png?v={APP_VERSION}' alt='Sensorius system overview' loading='lazy'>"
yield "</div>"
yield "<div id='modal-host'></div>"
yield "</div>"
yield "<section class='dashboard-theme-view' id='dashboardThemeView' aria-label='Sensorius dashboard theme preview' hidden>"
Expand Down
32 changes: 11 additions & 21 deletions testApparatus/test_sai_html_gauge_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ def test_dashboard_live_refresh_has_recovery_hooks():
assert "document.addEventListener('visibilitychange'" in html


def test_dashboard_renders_centered_overview_graphic_at_bottom():
def test_dashboard_renders_overview_graphic_in_header():
html = "".join(
render_dashboard(
"All",
Expand All @@ -486,18 +486,14 @@ def test_dashboard_renders_centered_overview_graphic_at_bottom():
)
css = (Path(__file__).resolve().parents[1] / "ui_static" / "css" / "app.css").read_text(encoding="utf-8")

graphic = "<div class='dashboard-overview-graphic' id='dashboard-overview-footer'>"
assert graphic in html
assert f"src='/ui_static/01-sensorius-overview-v5.png?v={APP_VERSION}'" in html
assert html.index(graphic) < html.index("<div id='modal-host'></div>")
assert ".dashboard-overview-graphic{" in css
assert "order:2147483647;" in css
assert "justify-content:center;" in css
assert "width:min(100%, 522px);" in css
assert "border-radius:12px;" in css
header = html[html.index("<h2 id='sensor_header'>"):html.index("<p id='update_time'>")]
assert f"src='/ui_static/01-sensorius-overview-v5.png?v={APP_VERSION}'" in header
assert header.count("class='dashboard-header-action'") == 2
assert 'dashboard-overview-footer' not in html
assert 'width:261px;' in css


def test_sensor_reordering_keeps_overview_graphic_as_footer():
def test_sensor_reordering_preserves_dashboard_content_parent():
html = "".join(
render_dashboard(
"All",
Expand All @@ -514,20 +510,14 @@ def test_sensor_reordering_keeps_overview_graphic_as_footer():
start = html.index("window.applySensorGroupOrder = function(order)")
end = html.index("window.reorderSensorGroup = async function", start)
reorder_block = html[start:end]
assert "window.pinDashboardOverviewFooter = function()" in html
assert "dashboardContent.lastElementChild !== overviewFooter" in html
assert "document.getElementById('dashboard-overview-footer')" in reorder_block
assert "dashboardContent.insertBefore(el, overviewFooter)" in reorder_block
assert "(firstSensorGroup && firstSensorGroup.parentElement)" in reorder_block
assert "if (switchGroup) insertBeforeFooter(switchGroup);" in reorder_block
assert "window.pinDashboardOverviewFooter();" in reorder_block
assert "dashboard.appendChild(el)" not in reorder_block

assert "if (switchGroup) dashboardContent.appendChild(switchGroup);" in reorder_block
assert 'overviewFooter' not in html
ensure_start = html.index("function ensureSensorUI(sid, metricList, locationText)")
ensure_end = html.index("window.DISPLAY_STYLES", ensure_start)
ensure_block = html[ensure_start:ensure_end]
assert "(overviewFooter && overviewFooter.parentElement)" in ensure_block
assert "parent.insertBefore(group, overviewFooter)" in ensure_block
assert "document.querySelector('.dashboard-content')" in ensure_block
assert "parent.insertBefore(group, switchGroup)" in ensure_block


def test_dashboard_metric_cards_render_and_refresh_trend_arrows():
Expand Down
35 changes: 24 additions & 11 deletions tests/dashboard.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,19 @@ test('renders the Sensorius dashboard and expands additional metrics', async ({
await expect(sensorGroup.locator('.metric-container').last()).toBeVisible();
});

test('keeps the overview graphic below sensors after moving the bottom sensor up', async ({ page }) => {
test('keeps the header image above sensors after moving the bottom sensor up', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.evaluate(() => {
const footer = document.getElementById('dashboard-overview-footer');
const dashboardContent = footer?.parentElement;
const dashboardContent = document.querySelector('.dashboard-content');
const source = dashboardContent?.querySelector('.sensor-group[data-sensor-id]');
if (!dashboardContent || !source || !footer) throw new Error('dashboard fixture is incomplete');
if (!dashboardContent || !source) throw new Error('dashboard fixture is incomplete');
const ecowitt = source.cloneNode(true);
ecowitt.id = 'group_ecowitt-bottom-test';
ecowitt.dataset.sensorId = 'ecowitt-bottom-test';
ecowitt.querySelectorAll('[data-sensor-id]').forEach((element) => {
element.setAttribute('data-sensor-id', 'ecowitt-bottom-test');
});
dashboardContent.insertBefore(ecowitt, footer);
dashboardContent.appendChild(ecowitt);
});

let reordered = false;
Expand All @@ -108,13 +107,27 @@ test('keeps the overview graphic below sensors after moving the bottom sensor up
await ecowitt.locator('.sensor-order-btn').click();
await ecowitt.locator(".sensor-order-item[data-move='up']").click();
await expect.poll(() => reordered).toBe(true);
await expect.poll(() => page.evaluate(() => document.getElementById('dashboard-overview-footer')?.parentElement?.lastElementChild?.id)).toBe('dashboard-overview-footer');

const footerTop = await page.locator('#dashboard-overview-footer').evaluate((element) => element.getBoundingClientRect().top + window.scrollY);
const sensorBottom = await page.locator('.sensor-group[data-sensor-id]').evaluateAll((elements) =>
Math.max(...elements.map((element) => element.getBoundingClientRect().bottom + window.scrollY)),
const image = page.locator('#sensor_header .dashboard-brand-image');
await expect(image).toBeVisible();
await expect(page.locator('#dashboard-overview-footer')).toHaveCount(0);
const imageBox = await image.boundingBox();
expect(imageBox.width).toBeCloseTo(261, 0);
const sensorTop = await page.locator('.sensor-group[data-sensor-id]').evaluateAll((elements) =>
Math.min(...elements.map((element) => element.getBoundingClientRect().top)),
);
expect(footerTop).toBeGreaterThanOrEqual(sensorBottom);
expect(imageBox.y + imageBox.height).toBeLessThanOrEqual(sensorTop);
for (const width of [1280, 375]) {
await page.setViewportSize({ width, height: 900 });
await expect.poll(() => page.locator('#sensor_header').evaluate((header) => {
const brand = header.querySelector('.dashboard-brand-image').getBoundingClientRect();
return Array.from(header.querySelectorAll('.dashboard-header-action')).map((button) => {
const box = button.getBoundingClientRect();
return Math.round(Math.abs((box.y + box.height / 2) - (brand.y + brand.height / 2)));
});
})).toEqual([0, 0]);
await page.locator('#sensor_header').screenshot({ path: test.info().outputPath(`header-${width}.png`) });
}

});

test('shares and persists the Lunar Calendar view mode', async ({ page }) => {
Expand Down
35 changes: 27 additions & 8 deletions ui_static/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ body.dashboard-page.dashboard-theme-fruit{
background-size:cover;
}

body.dashboard-page .dashboard-content > #sensor_header,
body.dashboard-page .dashboard-content > #update_time,
body.dashboard-page .sensor-group-header{
box-sizing:border-box;
Expand All @@ -84,7 +83,6 @@ body.dashboard-page .sensor-group-header{
color:var(--dashboard-card-text);
}

body.dashboard-page .dashboard-content > #sensor_header,
body.dashboard-page .dashboard-content > #update_time{
width:fit-content;
max-width:calc(100% - 10px);
Expand Down Expand Up @@ -349,21 +347,42 @@ body.dashboard-theme-preview-mode .dashboard-content{opacity:0;pointer-events:no
padding:1rem;
}

.dashboard-overview-graphic{
order:2147483647;
flex:0 0 100%;
body.dashboard-page #sensor_header{
display:flex;
align-items:center;
justify-content:center;
padding:1rem 0;
gap:12px;
margin:5px auto;
padding:0;
background:none;
border:0;
}

.dashboard-overview-graphic img{
.dashboard-brand-image{
display:block;
width:min(100%, 522px);
width:261px;
max-width:calc(100vw - 168px);
height:auto;
border-radius:12px;
}

.dashboard-header-action{
display:inline-flex;
align-items:center;
justify-content:center;
flex:0 0 56px;
height:44px;
box-sizing:border-box;
border:1px solid var(--dashboard-card-border);
border-radius:999px;
background:var(--dashboard-card-bg);
color:var(--dashboard-card-text);
text-decoration:none;
font-size:1.2rem;
}
.dashboard-header-action:hover{filter:brightness(.95)}
.dashboard-header-action:focus-visible{outline:2px solid currentColor;outline-offset:3px}

/* Each sensor’s row of metrics */
.sensor-row{
display:flex;
Expand Down