diff --git a/docs/user_guide.md b/docs/user_guide.md index a344604..b5666cc 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -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

diff --git a/docs/user_guide.pdf b/docs/user_guide.pdf index 8eec3ee..605e9d7 100644 Binary files a/docs/user_guide.pdf and b/docs/user_guide.pdf differ diff --git a/scripts/build_webapp_icons.mjs b/scripts/build_webapp_icons.mjs new file mode 100644 index 0000000..c433a70 --- /dev/null +++ b/scripts/build_webapp_icons.mjs @@ -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(); +} diff --git a/sensorius/__init__.py b/sensorius/__init__.py index fcaee48..2cc197c 100644 --- a/sensorius/__init__.py +++ b/sensorius/__init__.py @@ -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" diff --git a/sensorius/saiHtml.py b/sensorius/saiHtml.py index 3cce294..c151869 100644 --- a/sensorius/saiHtml.py +++ b/sensorius/saiHtml.py @@ -1193,6 +1193,8 @@ def _db_label_map_for_switch(sw_id: str) -> dict[str, str]: yield "" yield f"{APP_NAME_LONG}" yield "" + yield "" + yield "" yield "" yield f"" yield "" diff --git a/tests/webapp-manifest.spec.js b/tests/webapp-manifest.spec.js new file mode 100644 index 0000000..fd8b52e --- /dev/null +++ b/tests/webapp-manifest.spec.js @@ -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); +}); diff --git a/ui_static/icons/sensorius-192.png b/ui_static/icons/sensorius-192.png new file mode 100644 index 0000000..2aef1bf Binary files /dev/null and b/ui_static/icons/sensorius-192.png differ diff --git a/ui_static/icons/sensorius-512.png b/ui_static/icons/sensorius-512.png new file mode 100644 index 0000000..db3eb81 Binary files /dev/null and b/ui_static/icons/sensorius-512.png differ diff --git a/ui_static/icons/sensorius-maskable-512.png b/ui_static/icons/sensorius-maskable-512.png new file mode 100644 index 0000000..782ddcb Binary files /dev/null and b/ui_static/icons/sensorius-maskable-512.png differ diff --git a/ui_static/manifest.webmanifest b/ui_static/manifest.webmanifest new file mode 100644 index 0000000..9cc4586 --- /dev/null +++ b/ui_static/manifest.webmanifest @@ -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" + } + ] +} diff --git a/ui_templates/base.html b/ui_templates/base.html index 0ead52d..87f681d 100644 --- a/ui_templates/base.html +++ b/ui_templates/base.html @@ -5,6 +5,8 @@ {% block title %}Sensorius{% endblock %} + +