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
@@ -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')
@@ -1719,6 +1720,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).
@@ -1790,6 +1817,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).</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', {