fix(starlark): blank app locations use the device location, not San Francisco

A Starlark (Tidbyt) app whose Location field is blank rendered at its
author's hard-coded DEFAULT_LOCATION -- usually San Francisco -- even with
the device city set under General settings. A user in Charlotte, NC got San
Francisco weather and radar with nothing in config.json to explain it.

src/device_location.py fills unset location fields at render time (display
plugin and the web standalone render): the device city is geocoded once via
Open-Meteo, preferring a match in the configured state/country, and cached
permanently. A saved location always wins; if the lookup fails the field is
dropped so the app uses its own default, and the failure is not retried for
30 minutes.

Also fixes the config form: clearing a location omitted the key, and the
save merges, so the old value could never be removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-23 11:32:41 -04:00
co-authored by Claude Opus 5.5
parent e1ce7189f1
commit b076313392
9 changed files with 758 additions and 6 deletions
@@ -969,3 +969,59 @@ class TestPixletEditorHostDefaultsButDoesNotOverride:
def test_keeps_an_operator_configured_loopback_host(self, client, app_dir, tmp_path):
env = self._start(client, app_dir, tmp_path, operator_host='127.0.0.1')
assert env['PIXLET_EDITOR_HOST'] == '127.0.0.1'
class TestStandaloneRenderUsesTheDeviceLocation:
"""The web-service render (plugin not loaded) fills a blank Location field
the same way the display plugin does -- see test/test_device_location.py.
"""
SCHEMA = {"schema": [{"typeOf": "location", "id": "location"}]}
@pytest.fixture
def app_dir(self, tmp_path, monkeypatch):
from web_interface.blueprints import api_v3 as module
apps_dir = tmp_path / "starlark-apps"
app_dir = apps_dir / "weather"
app_dir.mkdir(parents=True)
(app_dir / "weather.star").write_text("# app")
(app_dir / "schema.json").write_text(json.dumps(self.SCHEMA))
monkeypatch.setattr(module, '_STARLARK_APPS_DIR', apps_dir)
monkeypatch.setattr(module, '_STARLARK_MANIFEST_FILE', apps_dir / 'manifest.json')
(apps_dir / 'manifest.json').write_text(json.dumps(
{'apps': {'weather': {'star_file': 'weather.star'}}}))
config_manager = MagicMock()
config_manager.load_config.return_value = {
'timezone': 'America/New_York',
'location': {'city': 'Charlotte', 'state': 'North Carolina', 'country': 'US'},
}
monkeypatch.setattr(module.api_v3, 'config_manager', config_manager, raising=False)
monkeypatch.setattr(module, '_find_pixlet_binary', lambda _p=None: '/usr/bin/pixlet')
from src.device_location import DeviceLocationResolver
geocoder = MagicMock(return_value={'lat': 35.22709, 'lng': -80.84313,
'timezone': 'America/New_York'})
monkeypatch.setattr(module, '_starlark_device_location',
DeviceLocationResolver(None, MagicMock(), geocoder))
return app_dir
def _render_args(self, app_dir):
from web_interface.blueprints import api_v3 as module
def fake_run(cmd, **kwargs):
(app_dir / 'cached_render.webp').write_bytes(b'webp')
return MagicMock(returncode=0, stderr='')
with patch.object(module.subprocess, 'run', side_effect=fake_run) as run:
ok, status, err = module._standalone_render_starlark_app('weather')
assert ok, err
return [a for a in run.call_args.args[0] if a.startswith('location=')]
def test_a_blank_location_renders_at_the_device_city(self, app_dir):
(app_dir / 'config.json').write_text(json.dumps({'location': ''}))
[arg] = self._render_args(app_dir)
assert json.loads(arg[len('location='):])['lat'] == '35.2271'
def test_a_saved_location_wins(self, app_dir):
saved = json.dumps({'lat': '40.6782', 'lng': '-73.9442'})
(app_dir / 'config.json').write_text(json.dumps({'location': saved}))
assert self._render_args(app_dir) == [f'location={saved}']