sky form TZ: render-readonly + drop #id_nf_tz_hint; placeholder absorbs the auto-detected hint copy — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

A user-typed TZ override fed through schedulePreview's `if (tz) params.set('tz', tz)` path made PySwiss compute the chart against a TZ that didn't match the lat/lon, so a partial edit (e.g. "America/New_Yo|") returned HTTP 400. Mirror the lat/lon convention: tz field gets readonly + tabindex:-1 across all three sky contexts (Dashsky sky.html, in-room PICK SKY _sky_overlay.html, My Sky applet _applet-my-sky.html). Auto-population still works because the JS writes via .value rather than via user input. The <small id="id_nf_tz_hint"> "Auto-detected from coordinates." line is removed; that copy now lives on the <input>'s placeholder so an empty TZ field self-explains. JS purges every tzHint reference (const declaration + 4 .textContent writes per file × 3 files).

SkyViewTest.test_tz_input_is_readonly_and_carries_auto_detect_placeholder pins the rendered Dashsky markup: id_nf_tz carries `readonly`, the placeholder is "auto-detected from coordinates", and `id="id_nf_tz_hint"` no longer appears anywhere. Existing MySkyTimezoneRefreshTest still passes — it asserts the field auto-fills via JS, which still works on a readonly input.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-08 15:27:09 -04:00
parent 8a8d1536b1
commit 1111df8465
4 changed files with 24 additions and 21 deletions

View File

@@ -37,6 +37,22 @@ class SkyViewTest(TestCase):
self.assertContains(response, reverse("sky_preview")) self.assertContains(response, reverse("sky_preview"))
self.assertContains(response, reverse("sky_save")) self.assertContains(response, reverse("sky_save"))
def test_tz_input_is_readonly_and_carries_auto_detect_placeholder(self):
"""Manual TZ edits throw the schedulePreview / PySwiss fetch off (the
backend gets a stale TZ for the new lat/lon), so the field is render-
readonly like lat/lon — auto-fills from preview, never from a typed
override. The old <small id="id_nf_tz_hint"> is gone; its copy lives
in the placeholder so an empty field is self-explanatory."""
response = self.client.get(reverse("sky"))
self.assertContains(response, 'id="id_nf_tz"')
# readonly + tabindex:-1 mirrors the lat/lon pattern.
self.assertRegex(
response.content.decode(),
r'id="id_nf_tz"[^>]*\breadonly\b',
)
self.assertContains(response, 'placeholder="auto-detected from coordinates"')
self.assertNotContains(response, 'id="id_nf_tz_hint"')
class SkyPreviewTest(TestCase): class SkyPreviewTest(TestCase):
def setUp(self): def setUp(self):

View File

@@ -54,8 +54,8 @@
<div class="sky-field"> <div class="sky-field">
<label for="id_nf_tz">Timezone</label> <label for="id_nf_tz">Timezone</label>
<input id="id_nf_tz" name="tz" type="text" <input id="id_nf_tz" name="tz" type="text"
placeholder="auto-detected from location"> placeholder="auto-detected from coordinates"
<small id="id_nf_tz_hint"></small> readonly tabindex="-1">
</div> </div>
</form> </form>
@@ -145,7 +145,6 @@
const latInput = document.getElementById('id_nf_lat'); const latInput = document.getElementById('id_nf_lat');
const lonInput = document.getElementById('id_nf_lon'); const lonInput = document.getElementById('id_nf_lon');
const tzInput = document.getElementById('id_nf_tz'); const tzInput = document.getElementById('id_nf_tz');
const tzHint = document.getElementById('id_nf_tz_hint');
const suggestions = document.getElementById('id_nf_suggestions'); const suggestions = document.getElementById('id_nf_suggestions');
const PREVIEW_URL = section.dataset.previewUrl; const PREVIEW_URL = section.dataset.previewUrl;
@@ -185,7 +184,7 @@
if (d.place) placeInput.value = d.place; if (d.place) placeInput.value = d.place;
if (d.lat) latInput.value = d.lat; if (d.lat) latInput.value = d.lat;
if (d.lon) lonInput.value = d.lon; if (d.lon) lonInput.value = d.lon;
if (d.tz) { tzInput.value = d.tz; tzHint.textContent = 'Auto-detected from coordinates.'; } if (d.tz) { tzInput.value = d.tz; }
if (_formReady()) schedulePreview(); if (_formReady()) schedulePreview();
} }
@@ -250,7 +249,6 @@
latInput.value = parseFloat(place.lat).toFixed(4); latInput.value = parseFloat(place.lat).toFixed(4);
lonInput.value = parseFloat(place.lon).toFixed(4); lonInput.value = parseFloat(place.lon).toFixed(4);
tzInput.value = ''; tzInput.value = '';
tzHint.textContent = '';
hideSuggestions(); hideSuggestions();
_saveForm(); _saveForm();
schedulePreview(); schedulePreview();
@@ -269,7 +267,6 @@
latInput.value = pos.coords.latitude.toFixed(4); latInput.value = pos.coords.latitude.toFixed(4);
lonInput.value = pos.coords.longitude.toFixed(4); lonInput.value = pos.coords.longitude.toFixed(4);
tzInput.value = ''; tzInput.value = '';
tzHint.textContent = '';
fetch( fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latInput.value}&lon=${lonInput.value}`, `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latInput.value}&lon=${lonInput.value}`,
{ headers: { 'User-Agent': USER_AGENT } } { headers: { 'User-Agent': USER_AGENT } }
@@ -330,7 +327,6 @@
_lastChartData = d; _lastChartData = d;
if (!tzInput.value && d.timezone) { if (!tzInput.value && d.timezone) {
tzInput.value = d.timezone; tzInput.value = d.timezone;
tzHint.textContent = 'Auto-detected from coordinates.';
} }
setStatus(''); setStatus('');
confirmBtn.disabled = false; confirmBtn.disabled = false;

View File

@@ -64,9 +64,9 @@
<div class="sky-field"> <div class="sky-field">
<label for="id_nf_tz">Timezone</label> <label for="id_nf_tz">Timezone</label>
<input id="id_nf_tz" name="tz" type="text" <input id="id_nf_tz" name="tz" type="text"
placeholder="auto-detected from location" placeholder="auto-detected from coordinates"
readonly tabindex="-1"
{% if saved_birth_tz %}value="{{ saved_birth_tz }}"{% endif %}> {% if saved_birth_tz %}value="{{ saved_birth_tz }}"{% endif %}>
<small id="id_nf_tz_hint"></small>
</div> </div>
</form> </form>
@@ -114,7 +114,6 @@
const latInput = document.getElementById('id_nf_lat'); const latInput = document.getElementById('id_nf_lat');
const lonInput = document.getElementById('id_nf_lon'); const lonInput = document.getElementById('id_nf_lon');
const tzInput = document.getElementById('id_nf_tz'); const tzInput = document.getElementById('id_nf_tz');
const tzHint = document.getElementById('id_nf_tz_hint');
const suggestions = document.getElementById('id_nf_suggestions'); const suggestions = document.getElementById('id_nf_suggestions');
const PREVIEW_URL = overlay.dataset.previewUrl; const PREVIEW_URL = overlay.dataset.previewUrl;
@@ -191,7 +190,6 @@
latInput.value = parseFloat(place.lat).toFixed(4); latInput.value = parseFloat(place.lat).toFixed(4);
lonInput.value = parseFloat(place.lon).toFixed(4); lonInput.value = parseFloat(place.lon).toFixed(4);
tzInput.value = ''; tzInput.value = '';
tzHint.textContent = '';
hideSuggestions(); hideSuggestions();
schedulePreview(); schedulePreview();
} }
@@ -209,7 +207,6 @@
latInput.value = pos.coords.latitude.toFixed(4); latInput.value = pos.coords.latitude.toFixed(4);
lonInput.value = pos.coords.longitude.toFixed(4); lonInput.value = pos.coords.longitude.toFixed(4);
tzInput.value = ''; tzInput.value = '';
tzHint.textContent = '';
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latInput.value}&lon=${lonInput.value}`, { fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latInput.value}&lon=${lonInput.value}`, {
headers: { 'User-Agent': USER_AGENT }, headers: { 'User-Agent': USER_AGENT },
}) })
@@ -267,7 +264,6 @@
_lastChartData = data; _lastChartData = data;
if (!tzInput.value && data.timezone) { if (!tzInput.value && data.timezone) {
tzInput.value = data.timezone; tzInput.value = data.timezone;
tzHint.textContent = 'Auto-detected from coordinates.';
} }
setStatus(''); setStatus('');
confirmBtn.disabled = false; confirmBtn.disabled = false;

View File

@@ -71,8 +71,8 @@
<div class="sky-field"> <div class="sky-field">
<label for="id_nf_tz">Timezone</label> <label for="id_nf_tz">Timezone</label>
<input id="id_nf_tz" name="tz" type="text" <input id="id_nf_tz" name="tz" type="text"
placeholder="auto-detected from location"> placeholder="auto-detected from coordinates"
<small id="id_nf_tz_hint"></small> readonly tabindex="-1">
</div> </div>
</form> </form>
@@ -122,7 +122,6 @@
const latInput = document.getElementById('id_nf_lat'); const latInput = document.getElementById('id_nf_lat');
const lonInput = document.getElementById('id_nf_lon'); const lonInput = document.getElementById('id_nf_lon');
const tzInput = document.getElementById('id_nf_tz'); const tzInput = document.getElementById('id_nf_tz');
const tzHint = document.getElementById('id_nf_tz_hint');
const suggestions = document.getElementById('id_nf_suggestions'); const suggestions = document.getElementById('id_nf_suggestions');
const PREVIEW_URL = overlay.dataset.previewUrl; const PREVIEW_URL = overlay.dataset.previewUrl;
@@ -166,7 +165,7 @@
if (data.place) placeInput.value = data.place; if (data.place) placeInput.value = data.place;
if (data.lat) latInput.value = data.lat; if (data.lat) latInput.value = data.lat;
if (data.lon) lonInput.value = data.lon; if (data.lon) lonInput.value = data.lon;
if (data.tz) { tzInput.value = data.tz; tzHint.textContent = 'Auto-detected from coordinates.'; } if (data.tz) { tzInput.value = data.tz; }
} }
// ── Open / Close ────────────────────────────────────────────────────────── // ── Open / Close ──────────────────────────────────────────────────────────
@@ -251,7 +250,6 @@
latInput.value = parseFloat(place.lat).toFixed(4); latInput.value = parseFloat(place.lat).toFixed(4);
lonInput.value = parseFloat(place.lon).toFixed(4); lonInput.value = parseFloat(place.lon).toFixed(4);
tzInput.value = ''; tzInput.value = '';
tzHint.textContent = '';
hideSuggestions(); hideSuggestions();
_saveForm(); _saveForm();
schedulePreview(); schedulePreview();
@@ -270,7 +268,6 @@
latInput.value = pos.coords.latitude.toFixed(4); latInput.value = pos.coords.latitude.toFixed(4);
lonInput.value = pos.coords.longitude.toFixed(4); lonInput.value = pos.coords.longitude.toFixed(4);
tzInput.value = ''; tzInput.value = '';
tzHint.textContent = '';
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latInput.value}&lon=${lonInput.value}`, { fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latInput.value}&lon=${lonInput.value}`, {
headers: { 'User-Agent': USER_AGENT }, headers: { 'User-Agent': USER_AGENT },
}) })
@@ -335,7 +332,6 @@
// Back-fill timezone field from proxy response (first render) // Back-fill timezone field from proxy response (first render)
if (!tzInput.value && data.timezone) { if (!tzInput.value && data.timezone) {
tzInput.value = data.timezone; tzInput.value = data.timezone;
tzHint.textContent = 'Auto-detected from coordinates.';
} }
setStatus(''); setStatus('');
@@ -429,7 +425,6 @@
latInput.value = ''; latInput.value = '';
lonInput.value = ''; lonInput.value = '';
tzInput.value = ''; tzInput.value = '';
tzHint.textContent = '';
_lastChartData = null; _lastChartData = null;
confirmBtn.disabled = true; confirmBtn.disabled = true;
setStatus(''); setStatus('');