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>
151 lines
5.1 KiB
JavaScript
151 lines
5.1 KiB
JavaScript
// Zapamiętane predykcje okresowe (PRE-22).
|
||
//
|
||
// Kalendarz pozwala policzyć horoskop na wybrany okres. Partnerzy chcą kilku
|
||
// okresów naraz — i żeby wszystkie trafiły potem do raportu (zakładka
|
||
// „Skompiluj", PRE-23/24). Tu jest magazyn i lista.
|
||
//
|
||
// Trzymamy w localStorage, tak jak wspólne dane formularza (PRE-21): serwer
|
||
// zostaje bezstanowy, żadne dane urodzeniowe ani treści z baz nie lądują po
|
||
// stronie usługi. Zakładka „Skompiluj" odczyta to samo miejsce.
|
||
(function () {
|
||
'use strict';
|
||
|
||
var KEY = 'astrololo.predictions.v1';
|
||
|
||
function read() {
|
||
try {
|
||
var v = JSON.parse(localStorage.getItem(KEY) || '[]');
|
||
return Array.isArray(v) ? v : [];
|
||
} catch (e) {
|
||
return []; // uszkodzony wpis nie może zablokować strony
|
||
}
|
||
}
|
||
|
||
function write(list) {
|
||
try {
|
||
localStorage.setItem(KEY, JSON.stringify(list));
|
||
return true;
|
||
} catch (e) {
|
||
return false; // najczęściej przepełniony magazyn
|
||
}
|
||
}
|
||
|
||
function byPeriod(a, b) {
|
||
return String(a.from_date).localeCompare(String(b.from_date));
|
||
}
|
||
|
||
// Klucz tożsamości to OKRES. Ponowne policzenie tego samego zakresu ma
|
||
// podmienić wpis, a nie dokładać duplikat — inaczej lista puchłaby przy
|
||
// każdej próbie z innym modelem.
|
||
function upsert(entry) {
|
||
var list = read().filter(function (p) {
|
||
return !(p.from_date === entry.from_date && p.to_date === entry.to_date);
|
||
});
|
||
list.push(entry);
|
||
list.sort(byPeriod);
|
||
return write(list) ? list : null;
|
||
}
|
||
|
||
function remove(id) {
|
||
var list = read().filter(function (p) { return p.id !== id; });
|
||
write(list);
|
||
return list;
|
||
}
|
||
|
||
function val(sel) {
|
||
var el = document.querySelector(sel);
|
||
return el ? el.value.trim() : '';
|
||
}
|
||
|
||
/** Wyciąga świeżo policzony horoskop okresowy ze strony. */
|
||
function capture() {
|
||
var text = val('#horoscopeText');
|
||
var from = val('input[name=from_date]');
|
||
var to = val('input[name=to_date]');
|
||
if (!text || !from || !to) return null;
|
||
return {
|
||
id: from + '..' + to,
|
||
from_date: from,
|
||
to_date: to,
|
||
person: val('input[name=person]'),
|
||
text: text,
|
||
saved_at: new Date().toISOString()
|
||
};
|
||
}
|
||
|
||
function fmtDate(iso) {
|
||
return String(iso || '').slice(0, 10);
|
||
}
|
||
|
||
function render() {
|
||
var host = document.getElementById('savedPredictions');
|
||
if (!host) return;
|
||
var list = read();
|
||
if (!list.length) {
|
||
host.innerHTML = '<p class="muted small">Brak zapamiętanych predykcji. ' +
|
||
'Policz horoskop na wybrany okres — zapamięta się sam i trafi do raportu.</p>';
|
||
return;
|
||
}
|
||
var rows = list.map(function (p) {
|
||
var chars = (p.text || '').length;
|
||
return '<tr>' +
|
||
'<td class="nowrap">' + fmtDate(p.from_date) + ' – ' + fmtDate(p.to_date) + '</td>' +
|
||
'<td class="muted small">' + fmtDate(p.saved_at) + '</td>' +
|
||
'<td class="muted small">' + chars.toLocaleString('pl-PL') + ' znaków</td>' +
|
||
'<td><button type="button" class="ghost" data-drop="' +
|
||
String(p.id).replace(/"/g, '"') + '">Usuń</button></td>' +
|
||
'</tr>';
|
||
}).join('');
|
||
host.innerHTML =
|
||
'<div class="meta">Zapamiętane predykcje (' + list.length + ') — wejdą do raportu</div>' +
|
||
'<table class="angles"><thead><tr><th>Okres</th><th>Zapisano</th>' +
|
||
'<th>Objętość</th><th></th></tr></thead><tbody>' + rows + '</tbody></table>';
|
||
|
||
Array.prototype.forEach.call(host.querySelectorAll('[data-drop]'), function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
remove(btn.getAttribute('data-drop'));
|
||
render();
|
||
});
|
||
});
|
||
}
|
||
|
||
function saveCurrent() {
|
||
var entry = capture();
|
||
if (!entry) return;
|
||
var note = document.getElementById('predictionsNote');
|
||
if (upsert(entry)) {
|
||
if (note) note.textContent = 'Zapamiętano predykcję ' +
|
||
entry.from_date + ' – ' + entry.to_date + '.';
|
||
} else if (note) {
|
||
// Horoskopy bywają długie; gdy magazyn się przepełni, trzeba powiedzieć
|
||
// wprost, zamiast po cichu zgubić wynik.
|
||
note.textContent = 'Nie udało się zapamiętać — magazyn przeglądarki pełny. ' +
|
||
'Usuń starsze predykcje i spróbuj ponownie.';
|
||
}
|
||
render();
|
||
}
|
||
|
||
function ready(fn) {
|
||
if (document.readyState !== 'loading') fn();
|
||
else document.addEventListener('DOMContentLoaded', fn);
|
||
}
|
||
|
||
ready(function () {
|
||
if (!document.getElementById('savedPredictions')) return; // nie ta zakładka
|
||
render();
|
||
|
||
// 1) horoskop policzony przez okno postępu (strumień)
|
||
document.addEventListener('astrololo:horoscope', function (e) {
|
||
if (!e.detail || e.detail.profile === 'period') saveCurrent();
|
||
});
|
||
|
||
// 2) wariant bez strumienia: strona wróciła z gotowym wynikiem po zwykłym
|
||
// POST. Zapis jest idempotentny (klucz = okres), więc odświeżenie strony
|
||
// niczego nie duplikuje.
|
||
if (document.querySelector('#horoscopeText')) saveCurrent();
|
||
});
|
||
|
||
// Udostępniamy magazyn zakładce „Skompiluj" (PRE-23) — jedno źródło prawdy.
|
||
window.kosmogramPredictions = { read: read, remove: remove, key: KEY };
|
||
})();
|