338ec24e90
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m20s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Testy astrodemo (pull_request) Successful in 9m25s
Testy / Testy astroklient (pull_request) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 12s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astrodemo (push) Successful in 9m25s
Testy / Testy astroklient (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Astroklient przedstawiał się na stronie głównej jako „astrololo" — w tytule karty i w nagłówku. To najpoważniejszy z możliwych śladów: mówi klientowi wprost, że trzyma okrojoną wersję czegoś większego, i podaje frazę, po której da się wyszukać, czego mu nie dano. Cała reszta izolacji przestaje mieć znaczenie, gdy produkt sam się przedstawia nazwą rodzica. Wzięło się stąd, że nazwa była wpisana na sztywno w szablony, a szablony są WSPÓLNE — dokładnie ta sama przyczyna, co przy zaszytym w base.html warunku na administratora. Nazwa mieszka teraz w module `produkt.py`, który nakładka produktu nadpisuje; szablony o nią pytają, zamiast ją znać. astrodemo NIE było dotknięte: ma własne kopie szablonów i pokazuje „astrodemo". DRUGIE ŹRÓDŁO WYCIEKU, mniej oczywiste: nazwa siedziała też w identyfikatorach JavaScriptu, które widzi każdy, kto otworzy narzędzia deweloperskie — zdarzenie `astrololo:coords` i klucz `astrololo.form.v1` w localStorage. Przemianowane na neutralne `kosmogram:*`; te identyfikatory nie mają powodu nosić nazwy rodziny produktów. SKUTEK UBOCZNY: zmiana klucza w localStorage kasuje raz zapamiętane wersje robocze formularza. Sprawdzone, że NIE wyciekają: nagłówki X-Astrololo-* jadą wyłącznie między warstwami, a dokumentacja OpenAPI (gdzie zostaje tytuł aplikacji FastAPI) oddaje 404 także zalogowanemu. TEST rozdziela to, co widzi człowiek, od identyfikatorów wewnętrznych. Nazwa rdzenia wolno występować w nazwach loggerów, materiale do wyprowadzania kluczy i nagłówkach międzywarstwowych; nie wolno w szablonach, zasobach ani w treści odpowiedzi. Zakaz na wszystko byłby albo obejściem przez zmianę nazw kluczy kryptograficznych, albo martwą regułą wyłączaną przy pierwszej okazji. Sprawdzony w obie strony: po cofnięciu nakładki test pada. Testy: astroklient 15, presentation 368, astrodemo 28, logic 342, data 42, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
183 lines
8.3 KiB
JavaScript
183 lines
8.3 KiB
JavaScript
// Wyszukiwarka lokalizacji + mapa (OpenStreetMap / Leaflet, bez klucza API).
|
|
// Wpina się do formularza z polami input[name=lat] / input[name=lon]:
|
|
// - wpisanie nazwy/adresu/POI -> /geocode -> lista trafień -> klik ustawia lat/lon,
|
|
// - klik na mapie lub przeciągnięcie pineski -> ustawia lat/lon + /reverse pokazuje nazwę,
|
|
// - „Tu i teraz" (now.js) emituje event 'kosmogram:coords' -> mapa się synchronizuje.
|
|
// Bez internetu wyszukiwarka/kafelki nie działają, ale ręczne lat/lon dalej tak.
|
|
(function () {
|
|
function ready(fn) {
|
|
if (document.readyState !== 'loading') fn();
|
|
else document.addEventListener('DOMContentLoaded', fn);
|
|
}
|
|
|
|
ready(function () {
|
|
var picker = document.getElementById('geoPicker');
|
|
if (!picker || typeof L === 'undefined') return; // brak Leafleta = po cichu pas
|
|
var latEl = document.querySelector('input[name=lat]');
|
|
var lonEl = document.querySelector('input[name=lon]');
|
|
if (!latEl || !lonEl) return;
|
|
|
|
var searchEl = document.getElementById('geoSearch');
|
|
var searchBtn = document.getElementById('geoSearchBtn');
|
|
var mapToggle = document.getElementById('geoMapToggle');
|
|
var resultsEl = document.getElementById('geoResults');
|
|
var mapEl = document.getElementById('map');
|
|
|
|
// ---- strefa czasowa z lokalizacji (PRE-03) ----
|
|
// Offset GMT liczymy ze współrzędnych + daty (DST-świadomie), żeby użytkownik
|
|
// nie musiał go znać ani pamiętać o czasie letnim. Podpowiedź trafia do pola
|
|
// tz_offset (PRE-19), które zostaje edytowalne — to pomoc, nie kaganiec.
|
|
var tzEl = document.querySelector('input[name=tz_offset]');
|
|
var dateEl = document.querySelector('input[name=date]');
|
|
var timeEl = document.querySelector('input[name=time]');
|
|
var tzNote = null;
|
|
if (tzEl && tzEl.parentNode) {
|
|
tzNote = document.createElement('span');
|
|
tzNote.className = 'muted small';
|
|
tzNote.id = 'tzNote';
|
|
tzEl.parentNode.appendChild(tzNote);
|
|
}
|
|
|
|
var num = function (el) { var v = parseFloat(el.value); return isNaN(v) ? 0 : v; };
|
|
var curLat = function () { return num(latEl); };
|
|
var curLon = function () { return num(lonEl); };
|
|
|
|
var pin = L.divIcon({ className: 'geo-pin', html: '📍', iconSize: [24, 24], iconAnchor: [12, 24] });
|
|
var map = L.map(mapEl).setView([curLat(), curLon()], 12);
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(map);
|
|
var marker = L.marker([curLat(), curLon()], { draggable: true, icon: pin }).addTo(map);
|
|
|
|
// Kontener mógł być renderowany zanim ustaliła się jego szerokość — poprawka.
|
|
setTimeout(function () { map.invalidateSize(); }, 60);
|
|
|
|
// Ustawienie `.value` z kodu NIE wywołuje zdarzeń, więc synchronizacja
|
|
// między zakładkami (formsync.js) przegapiłaby zmiany z mapy. Zgłaszamy je
|
|
// jawnie przez `change`.
|
|
function setVal(el, value) {
|
|
if (!el) return;
|
|
el.value = value;
|
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
function setCoords(lat, lon, fromMap) {
|
|
setVal(latEl, lat.toFixed(4));
|
|
setVal(lonEl, lon.toFixed(4));
|
|
marker.setLatLng([lat, lon]);
|
|
if (!fromMap) map.setView([lat, lon], Math.max(map.getZoom(), 12));
|
|
if (fromMap) reverseName(lat, lon);
|
|
resolveTz(); // nowe współrzędne → odśwież strefę/offset
|
|
}
|
|
|
|
// Współrzędne + data → offset GMT (DST-świadomy). Wypełnia tz_offset i pokazuje,
|
|
// jaką strefę wykryto. Bez pola/daty/biblioteki po cichu pasuje — zostaje
|
|
// ręczny offset. Offset trzymamy potem jako stałą liczbę: to on decyduje przy
|
|
// przeliczaniu, więc dokładne współrzędne nie przerzucą już czasu letniego.
|
|
function resolveTz() {
|
|
if (!tzEl || !dateEl || !dateEl.value) return;
|
|
var lat = curLat(), lon = curLon();
|
|
if (!lat && !lon) return; // 0,0 = brak lokalizacji
|
|
var url = '/timezone?lat=' + lat + '&lon=' + lon +
|
|
'&date=' + encodeURIComponent(dateEl.value) +
|
|
'&time=' + encodeURIComponent((timeEl && timeEl.value) || '12:00');
|
|
fetch(url)
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (d) {
|
|
if (!d) { if (tzNote) tzNote.textContent = ''; return; }
|
|
setVal(tzEl, d.offset); // change → formsync podłapie
|
|
if (tzNote) tzNote.textContent = 'Wykryto: ' + d.label;
|
|
})
|
|
.catch(function () {});
|
|
}
|
|
|
|
function reverseName(lat, lon) {
|
|
// Czyścimy pole OD RAZU. Dopóki nie znamy nazwy nowego punktu, stara jest
|
|
// po prostu nieprawdziwa — pokazywałaby inne miejsce niż współrzędne obok.
|
|
// Dotyczy też sytuacji, gdy /reverse nie odpowie (brak sieci): lepiej puste
|
|
// pole i poprawne współrzędne niż nazwa, która kłamie.
|
|
if (searchEl) setVal(searchEl, '');
|
|
fetch('/reverse?lat=' + lat + '&lon=' + lon)
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (d) { if (d && d.name && searchEl) setVal(searchEl, d.name); })
|
|
.catch(function () {});
|
|
}
|
|
|
|
marker.on('dragend', function () {
|
|
var p = marker.getLatLng();
|
|
setCoords(p.lat, p.lng, true);
|
|
});
|
|
map.on('click', function (e) { setCoords(e.latlng.lat, e.latlng.lng, true); });
|
|
|
|
// ---- wyszukiwanie po nazwie ----
|
|
function renderResults(items) {
|
|
resultsEl.innerHTML = '';
|
|
if (!items.length) { resultsEl.hidden = true; return; }
|
|
items.forEach(function (it) {
|
|
var li = document.createElement('li');
|
|
li.textContent = it.name;
|
|
li.addEventListener('click', function () {
|
|
setCoords(it.lat, it.lon, false);
|
|
setVal(searchEl, it.name);
|
|
resultsEl.hidden = true;
|
|
});
|
|
resultsEl.appendChild(li);
|
|
});
|
|
resultsEl.hidden = false;
|
|
}
|
|
|
|
function doSearch() {
|
|
var q = searchEl.value.trim();
|
|
if (q.length < 3) { resultsEl.hidden = true; return; }
|
|
fetch('/geocode?q=' + encodeURIComponent(q))
|
|
.then(function (r) { return r.ok ? r.json() : { results: [] }; })
|
|
.then(function (d) { renderResults(d.results || []); })
|
|
.catch(function () { renderResults([]); });
|
|
}
|
|
|
|
var timer = null;
|
|
searchBtn.addEventListener('click', doSearch);
|
|
searchEl.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter') { e.preventDefault(); doSearch(); } // nie wysyłaj formularza
|
|
});
|
|
searchEl.addEventListener('input', function () {
|
|
clearTimeout(timer); timer = setTimeout(doSearch, 500); // debounce (limit Nominatim)
|
|
});
|
|
|
|
// ---- zwijanie/rozwijanie mapy ----
|
|
mapToggle.addEventListener('click', function () {
|
|
var hidden = mapEl.hasAttribute('hidden');
|
|
if (hidden) {
|
|
mapEl.removeAttribute('hidden');
|
|
mapToggle.textContent = 'Ukryj mapę';
|
|
setTimeout(function () { map.invalidateSize(); map.setView([curLat(), curLon()]); }, 60);
|
|
} else {
|
|
mapEl.setAttribute('hidden', '');
|
|
mapToggle.textContent = 'Pokaż mapę';
|
|
}
|
|
});
|
|
|
|
// ---- synchronizacja z „Tu i teraz" ----
|
|
// Poza przesunięciem pineski MUSIMY odświeżyć pole tekstowe: po „Tu i teraz"
|
|
// zostawała w nim stara, ręcznie wpisana nazwa, więc formularz pokazywał jedno
|
|
// miejsce, a współrzędne wskazywały już inne. reverseName pyta o nazwę punktu,
|
|
// na który faktycznie ustawiła się mapa.
|
|
document.addEventListener('kosmogram:coords', function () {
|
|
marker.setLatLng([curLat(), curLon()]);
|
|
map.setView([curLat(), curLon()], Math.max(map.getZoom(), 12));
|
|
reverseName(curLat(), curLon());
|
|
resolveTz(); // „Tu i teraz" → nowe miejsce → nowa strefa
|
|
});
|
|
|
|
// Zmiana daty przelicza offset — czas letni zależy od daty (inny w czerwcu
|
|
// niż w styczniu), więc bez tego offset zostałby z poprzedniej pory roku.
|
|
if (dateEl) dateEl.addEventListener('change', resolveTz);
|
|
|
|
// Na wejściu podpowiadamy strefę TYLKO gdy offset wygląda na nieustawiony
|
|
// (0 lub pusto) — żeby nie nadpisać wartości, którą użytkownik wpisał ręcznie
|
|
// i wysłał. Przy każdej późniejszej zmianie miejsca/daty już aktualizujemy.
|
|
if (tzEl && (tzEl.value === '' || parseFloat(tzEl.value) === 0)) resolveTz();
|
|
});
|
|
})();
|