Rebuild the editor as a Vue app: sidebar, pages side by side, tile settings in a drawer (0.2.74)

The page is now web/ (Vue 3 + Vite + TypeScript), built into screen_manager/app/static
with hashed file names; server.py serves index.html as built and /assets/. The Python
API, the data and the firmware are unchanged. Your screens sit in a sidebar, the pages
of a screen stand next to each other at the screen's own shape, the library sits on the
right, and a tile's settings slide in as a drawer while the mockup stays in view. New
screen, Firmware & USB, Alerts, Override YAML and Settings are pages of their own.
Fonts moved to web/src/assets; the tests read the source through tests/editor_sources.py.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-18 07:48:45 +02:00
parent 9f5694b9eb
commit 510ebdf885
72 changed files with 5486 additions and 5409 deletions
+23 -11
View File
@@ -1,11 +1,11 @@
"""The page loads its own script and styles by content stamp (app 0.2.58).
"""The page loads its own script and styles by content hash (app 0.2.58, Vite build since 0.2.74).
After updating to 0.2.57, Safari showed an empty Screen settings panel: it kept a cached script from before
the update. The page now asks for app.js and style.css with a stamp of their content, so a new version is
always fetched and an old one is never combined with a new page.
the update. Vite names every built file after a hash of its content and index.html asks for those names, so a
new version is always fetched and an old one is never combined with a new page.
"""
import hashlib
import importlib.util
import re
import tempfile
import unittest
from pathlib import Path
@@ -15,28 +15,40 @@ STATIC = ROOT / 'screen_manager/app/static'
HAS_AIOHTTP = importlib.util.find_spec('aiohttp') is not None
class Build(unittest.TestCase):
def test_the_build_is_committed_and_names_its_files_by_hash(self):
page = (STATIC / 'index.html').read_text()
script = re.search(r'<script type="module" crossorigin src="\./(assets/index-[\w-]+\.js)"></script>', page)
styles = re.search(r'<link rel="stylesheet" crossorigin href="\./(assets/index-[\w-]+\.css)">', page)
self.assertTrue(script and styles, 'index.html is the Vite build of web/ (cd web && npm run build)')
for name in (script[1], styles[1]):
self.assertTrue((STATIC / name).exists(), name)
self.assertFalse((STATIC / 'app.js').exists(), 'the old script is gone')
self.assertEqual(len(list((STATIC / 'assets').glob('index-*.js'))), 1, 'one build at a time: npm run build clears assets/')
@unittest.skipUnless(HAS_AIOHTTP, 'Run using .venv-portal/bin/python for server tests')
class Assets(unittest.IsolatedAsyncioTestCase):
async def test_script_and_styles_carry_a_stamp_of_their_content(self):
async def test_the_server_serves_the_page_and_its_hashed_assets(self):
import sys
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
sys.path.insert(0, str(ROOT / 'tests'))
import test_scaling
from aiohttp.test_utils import TestClient, TestServer
from server import Manager, create_app
stamp = hashlib.sha1((STATIC / 'app.js').read_bytes() + (STATIC / 'style.css').read_bytes()).hexdigest()[:12]
page = (STATIC / 'index.html').read_text()
script = re.search(r'src="\./(assets/index-[\w-]+\.js)"', page)[1]
with tempfile.TemporaryDirectory() as tmp:
manager = Manager(test_scaling.fake_ha(), Path(tmp) / 'screens.json')
async with TestClient(TestServer(create_app(manager, True))) as client:
response = await client.get('/')
page = await response.text()
self.assertEqual(response.status, 200)
self.assertIn(f'<script src="static/app.js?v={stamp}" defer></script>', page)
self.assertIn(f'<link rel="stylesheet" href="static/style.css?v={stamp}" />', page)
self.assertEqual(await response.text(), page)
self.assertEqual(response.headers['Cache-Control'], 'no-store')
self.assertIn('text/html', response.headers['Content-Type'])
script = await client.get(f'/static/app.js?v={stamp}')
self.assertEqual((script.status, await script.read()), (200, (STATIC / 'app.js').read_bytes()))
asset = await client.get(f'/{script}')
self.assertEqual((asset.status, await asset.read()), (200, (STATIC / script).read_bytes()))
self.assertEqual((await client.get('/static/app.js')).status, 404)
if __name__ == '__main__':