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
9 changes: 9 additions & 0 deletions docs/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,15 @@ device names, available metrics, settings, and time ranges depend on your
installation. The screenshots show selected views, not every available option.
Tap an image to open it at full size.

On Android, open the dashboard in Chrome, open the browser menu, and choose
**Add to Home screen** (or **Install app**, when offered). Confirm the name and
icon before adding it. Sensorius supplies its logo in standard and adaptive
Android icon formats. If an existing shortcut still shows an old or generic
icon, remove that shortcut and add it again after reloading the dashboard.
Installation options depend on the browser and connection: a local HTTP hub
may be offered as a shortcut rather than a standalone installed app. This does
not add offline operation; the phone still needs access to the running hub.

### Mobile Dashboard

<p align="center">
Expand Down
Binary file modified docs/user_guide.pdf
Binary file not shown.
34 changes: 34 additions & 0 deletions scripts/build_webapp_icons.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** Rasterize the existing Sensorius SVG for web-app launchers.
* The maskable variant keeps the logo inside Android's circular safe zone.
*/
import { readFile, writeFile } from 'node:fs/promises';
import { chromium } from 'playwright';

const root = new URL('../', import.meta.url);
const svg = await readFile(new URL('ui_static/favicon.svg', root), 'utf8');
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
for (const [name, size, scale] of [
['sensorius-192', 192, 1],
['sensorius-512', 512, 1],
['sensorius-maskable-512', 512, 0.66],
]) {
const data = await page.evaluate(async ({ svg, size, scale }) => {
const img = new Image();
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
await img.decode();
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, size, size);
const inset = size * (1 - scale) / 2;
ctx.drawImage(img, inset, inset, size * scale, size * scale);
return canvas.toDataURL('image/png').split(',')[1];
}, { svg, size, scale });
await writeFile(new URL(`ui_static/icons/${name}.png`, root), Buffer.from(data, 'base64'));
}
} finally {
await browser.close();
}
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.252.4"
__version__ = "v0.26.252.5"
2 changes: 2 additions & 0 deletions sensorius/saiHtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,8 @@ def _db_label_map_for_switch(sw_id: str) -> dict[str, str]:
yield "<!DOCTYPE html>"
yield f"<html><head><title>{APP_NAME_LONG}</title>"
yield "<meta charset='UTF-8'>"
yield "<link rel='manifest' href='/ui_static/manifest.webmanifest'>"
yield "<link rel='icon' href='/ui_static/icons/sensorius-192.png' type='image/png' sizes='192x192'>"
yield "<link rel='icon' href='/ui_static/favicon.svg' type='image/svg+xml'>"
yield f"<link rel='apple-touch-icon' sizes='180x180' href='/ui_static/apple-touch-icon.png?v={APP_VERSION}'>"
yield "<script src='https://cdn.jsdelivr.net/npm/gaugeJS@1.3.7/dist/gauge.min.js'></script>"
Expand Down
32 changes: 32 additions & 0 deletions tests/webapp-manifest.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// @ts-check
import { test, expect } from '@playwright/test';

test('dashboard exposes a valid launcher manifest and loadable Android icons', async ({ page }) => {
await page.route('https://cdn.jsdelivr.net/**', route => route.fulfill({ body: '' }));
await page.goto('/', { waitUntil: 'domcontentloaded' });
const manifestURL = await page.locator('link[rel="manifest"]').evaluate(link => link.href);
const response = await page.request.get(manifestURL);
expect(response.ok()).toBeTruthy();
expect(response.headers()['content-type']).toContain('application/manifest+json');
const manifest = await response.json();
const origin = new URL(page.url()).origin;
expect(new URL(manifest.start_url, manifestURL).href).toBe(`${origin}/`);
expect(new URL(manifest.scope, manifestURL).href).toBe(`${origin}/`);
expect(manifest.display).toBe('standalone');
expect(manifest.icons.filter(icon => icon.purpose === 'any').map(icon => icon.sizes)).toEqual(['192x192', '512x512']);
expect(manifest.icons.some(icon => icon.purpose === 'maskable')).toBeTruthy();
for (const icon of manifest.icons) {
const info = await page.evaluate(async ({ icon, manifestURL }) => {
const image = new Image();
image.src = new URL(icon.src, manifestURL).href;
await image.decode();
return `${image.naturalWidth}x${image.naturalHeight}`;
}, { icon, manifestURL });
expect(info).toBe(icon.sizes);
}
const session = await page.context().newCDPSession(page);
const parsed = await session.send('Page.getAppManifest');
expect(parsed.errors).toEqual([]);
expect(parsed.url).toBe(manifestURL);
await expect(page.locator('link[rel="apple-touch-icon"]')).toHaveCount(1);
});
Binary file added ui_static/icons/sensorius-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui_static/icons/sensorius-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui_static/icons/sensorius-maskable-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions ui_static/manifest.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"id": "/",
"name": "Sensorius AI",
"short_name": "Sensorius",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"icons": [
{
"src": "/ui_static/icons/sensorius-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/ui_static/icons/sensorius-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/ui_static/icons/sensorius-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
2 changes: 2 additions & 0 deletions ui_templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>{% block title %}Sensorius{% endblock %}</title>
<link rel="manifest" href="/ui_static/manifest.webmanifest"/>
<link rel="icon" href="/ui_static/icons/sensorius-192.png" type="image/png" sizes="192x192"/>
<link rel="icon" href="/ui_static/favicon.svg" type="image/svg+xml"/>
<link rel="apple-touch-icon" sizes="180x180" href="/ui_static/apple-touch-icon.png"/>
<link rel="stylesheet" href="/ui_static/css/app.css"/>
Expand Down