PICK SKY DEL: server purge of seat Character + race guards stop the btn from re-injecting; readonly opacity bump (0.6 → 0.85) — TDD

Two related Sky Select bugs the old DEL flow couldn't address. (1) DEL btn lingered after a clear because an in-flight schedulePreview's .then() could resolve AFTER the OK callback ran, calling _ensureDelBtn() against a freshly-cleared wheel-col. (2) Sky data rehydrated on refresh because clicking SAVE SKY confirms a Character row on the seat — the DEL handler only purged localStorage & in-memory state, leaving the durable Character row to drive subsequent renders.

Server: new epic.sky_delete(room_id) view (POST → JsonResponse {deleted:True}) deletes every Character on the requesting gamer's seat where retired_at is null — drafts (confirmed_at NULL) and confirmed rows alike. 405 on GET, 403 for outsiders, never touches User.sky_chart_data (Dashsky/My Sky applet's DEL owns that side).

JS (_sky_overlay.html): DEL OK callback now (a) bumps a _fetchSeq counter so any in-flight schedulePreview .then()/.catch() short-circuits when its captured seq != current — kills the re-injection race; (b) clearTimeout-s _chartDebounce + _placeDebounce so a typed-just-before-DEL keystroke can't fire schedulePreview after the clear; (c) POSTs to DELETE_URL (overlay.dataset.deleteUrl wired via {% url 'epic:sky_delete' room.id %}) so the seat's Character row is dropped server-side; (d) clears LS + DOM state as before.

SCSS: .sky-field input[readonly] opacity 0.6 → 0.85, & dropped the redundant .sky-coords > div input { opacity:0.6 } that was previously winning the cascade by virtue of being declared later. The browser's default ::placeholder is ~0.54, so 0.85 × 0.54 ≈ 0.46 — close to the birth-place placeholder's ~0.54 effective opacity per the user's "appreciably higher tho not opacity 1" target. Values land at 0.85 (clearly readable but still de-emphasized vs. the editable place input).

Tests: 4 new ITs in PickSkyRenderingTest cover (a) POST clears confirmed Character, returns JSON {deleted:True}; (b) 405 on GET; (c) 403 for non-seat-owner; (d) User.sky_chart_data untouched by in-room DEL. PickSkyDelTest FT picks up an extra assertion: id_sky_delete_btn must be absent from DOM after OK (the bug-1 regression guard). 55-test sky suite green.

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:39:07 -04:00
parent 1111df8465
commit 846f9ff461
6 changed files with 118 additions and 5 deletions

View File

@@ -9,6 +9,7 @@
id="id_sky_overlay"
data-preview-url="{% url 'epic:sky_preview' room.id %}"
data-save-url="{% url 'epic:sky_save' room.id %}"
data-delete-url="{% url 'epic:sky_delete' room.id %}"
data-sea-partial-url="{% url 'epic:sea_partial' room.id %}"
data-user-seat-role="{{ user_seat_role }}">
@@ -126,12 +127,16 @@
const PREVIEW_URL = overlay.dataset.previewUrl;
const SAVE_URL = overlay.dataset.saveUrl;
const DELETE_URL = overlay.dataset.deleteUrl;
const NOMINATIM = 'https://nominatim.openstreetmap.org/search';
const USER_AGENT = 'EarthmanRPG/1.0 (https://earthmanrpg.me)';
let _lastChartData = null;
let _placeDebounce = null;
let _chartDebounce = null;
// Bumped on every clear so an in-flight schedulePreview() resolving after
// DEL doesn't paint the wheel back & re-inject the DEL btn.
let _fetchSeq = 0;
const PLACE_DELAY = 400; // ms — Nominatim polite rate
const CHART_DELAY = 300; // ms — chart preview debounce
@@ -321,12 +326,18 @@
setStatus('Calculating…');
confirmBtn.disabled = true;
const seq = ++_fetchSeq;
fetch(`${PREVIEW_URL}?${params}`, { credentials: 'same-origin' })
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => {
// Stale fetch — DEL ran (or another preview superseded). Bail before
// we paint the wheel & re-inject the DEL btn against cleared state.
if (seq !== _fetchSeq) return;
_lastChartData = data;
// Back-fill timezone field from proxy response (first render)
@@ -344,6 +355,7 @@
_ensureDelBtn();
})
.catch(err => {
if (seq !== _fetchSeq) return;
setStatus(`Could not fetch chart: ${err.message}`, 'error');
confirmBtn.disabled = true;
});
@@ -420,6 +432,22 @@
_delBtn.addEventListener('click', () => {
if (!window.showGuard) return;
window.showGuard(_delBtn, 'Forget sky?', () => {
// 1. Invalidate any in-flight preview so its .then() can't paint the
// wheel back & re-inject this btn after we've cleared.
_fetchSeq++;
// 2. Cancel pending debounces so a typed-just-before-DEL keystroke
// can't fire schedulePreview after the clear.
clearTimeout(_chartDebounce);
clearTimeout(_placeDebounce);
// 3. Server purge — drops any Character (draft or confirmed) on this
// seat, so a refresh after SAVE-then-DEL doesn't rehydrate state
// from the durable Character row.
fetch(DELETE_URL, {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-CSRFToken': _getCsrf() },
}).catch(() => {});
// 4. Client DOM/state reset.
while (svgEl.firstChild) svgEl.removeChild(svgEl.firstChild);
form.reset();
latInput.value = '';