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

* 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>

* docs(starlark): say what happens when the device location can't be used

A blank app Location only renders at the device's city when one is set and
the Open-Meteo lookup finds it. With no city, no match, or the geocoder
unreachable (retried after 30 minutes), the app gets no location and keeps
its author's default. The guide, the config page hint, CONFIG_REFERENCE and
the CHANGELOG entry now say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-23 16:48:02 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 61e462c635
commit 9a1f94f793
9 changed files with 761 additions and 6 deletions
@@ -49,6 +49,7 @@ from src.web_interface.validators import (
from src.error_aggregator import get_error_aggregator
from src.common.permission_utils import install_requirements_file
from src.common.path_safety import resolve_under
from src.device_location import DeviceLocationResolver, apply_device_location
_SUDO = shutil.which('sudo')
_JOURNALCTL = shutil.which('journalctl')
_GIT = shutil.which('git')
@@ -1703,6 +1704,32 @@ def _validate_starlark_app_path(app_id: str) -> Tuple[Optional[Path], Optional[s
except OSError as e:
logger.warning("Path validation error for app_id %r: %s", app_id, e)
return None, "Invalid app_id"
_starlark_device_location: Optional[DeviceLocationResolver] = None
def _get_starlark_device_location() -> DeviceLocationResolver:
"""One resolver per process, so a geocode failure's backoff is shared."""
global _starlark_device_location
if _starlark_device_location is None:
_starlark_device_location = DeviceLocationResolver(
getattr(api_v3, 'cache_manager', None) or _ensure_cache_manager(), logger)
return _starlark_device_location
def _read_starlark_schema(app_dir: Path) -> Optional[Dict[str, Any]]:
"""An installed app's schema.json, or None if it has none or it's unreadable."""
schema_file = app_dir / 'schema.json'
if not schema_file.exists():
return None
try:
with open(schema_file) as f:
schema = json.load(f)
except (OSError, json.JSONDecodeError) as e:
logger.warning("Could not read schema.json at %s: %s", schema_file, e)
return None
return schema if isinstance(schema, dict) else None
def _standalone_render_starlark_app(app_id: str) -> Tuple[bool, int, Optional[str]]:
"""Render a Starlark app via pixlet directly (no plugin required).
@@ -1774,6 +1801,9 @@ def _standalone_render_starlark_app(app_id: str) -> Tuple[bool, int, Optional[st
INTERNAL_KEYS = {'render_interval', 'display_duration'}
pixlet_config = {k: v for k, v in app_config.items() if k not in INTERNAL_KEYS}
pixlet_config = apply_device_location(
pixlet_config, _read_starlark_schema(app_dir),
_get_starlark_device_location(), full_config)
output_path = str(app_dir / 'cached_render.webp')
cmd = [pixlet_path, 'render', str(star_file)]
@@ -221,6 +221,7 @@
data-starlark-location-key="timezone">
</div>
</div>
<p class="text-xs text-gray-500 mt-1">Leave latitude and longitude blank to use this device's location (City / State / Country in General settings). If no city is set there, or it can't be looked up, the app uses its own default.</p>
{% if field_desc %}
<p class="text-xs text-gray-400 mt-1">{{ field_desc }}</p>
{% endif %}
@@ -421,9 +422,10 @@ function saveStarlarkConfig(appId) {
var locKey = sub.getAttribute('data-starlark-location-key');
if (sub.value) loc[locKey] = sub.value;
});
if (Object.keys(loc).length > 0) {
config[fieldId] = JSON.stringify(loc);
}
// Blank still has to be sent: the save merges into the stored config,
// so leaving the key out kept the old location with no way to clear
// it back to the device's.
config[fieldId] = Object.keys(loc).length > 0 ? JSON.stringify(loc) : '';
});
fetch('/api/v3/starlark/apps/' + encodeURIComponent(appId) + '/config', {