Files
astrololo/services/presentation/app/moduly/dodatki/static/predictions.js
T
gitea fd79513ce2
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Failing after 3s
Testy / Testy astrodemo (pull_request) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 2s
Testy / Kontrola składni wszystkich warstw (pull_request) Failing after 3s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
build / build (push) Successful in 9s
astrololo: ekrany i eksport jako moduły, katalog z rejestracji (3/5)
Trzeci z pięciu kroków. Zmieniłem jego zakres wobec planu i warto wiedzieć
dlaczego: pierwotnie miała to być deduplikacja pięciu kopii link_crypto.py, ale
po zrobieniu PR 2 widać, że astroklienta blokuje co innego — ekrany, których nie
ma mieć, siedzą wewnątrz jednego main.py. Deduplikacja kryptografii jest realnym
długiem, ale niczego nie blokuje.

PODZIAŁ. main.py (1023 linie) rozpadł się na podstawa.py (wspólne obiekty
i pomocnicy), jedenaście modułów w app/ekrany/ i main.py, który jest już samym
ZŁOŻENIEM: lista importów JEST definicją produktu. Podział zrobiony mechanicznie,
z osobnym sprawdzeniem, że żadna sekcja nie wołała pomocnika z innej (nie wołała).

KATALOG Z REJESTRACJI. Dotąd wszystkie funkcje były wypisane w features.py, więc
obraz produktu, który części z nich nie ma, i tak niósł ich nazwy — spis funkcji,
których nie ma jak włączyć. Teraz ekran zgłasza siebie, swoje trasy i swoje
zasoby przy imporcie własnego modułu, a features.py nie wymienia ani jednego
ekranu. Kolejność w nawigacji jest jawna (`kolejnosc`), żeby nie rządziła nią
kolejność importów.

To samo dotyczy nawigacji administratora: odsyłacz do ekranu kont był wpisany na
sztywno w base.html, więc w węższym produkcie zostawał martwy link i nazwa
ekranu, którego nie ma.

EKSPORT JAKO MODUŁ. Zgodnie z ustaleniem eksport jest funkcją administracyjną,
więc musi dać się usunąć. app/moduly/eksport/ zabiera arkusz, trasę PDF-a i akcję
formularza. „Można, ale nie temu kontu" i „nie ma takiej możliwości" to dwie
różne gwarancje, a eksport wynosi najwięcej treści baz naraz.

MOST ODKRYWA MODUŁY. Skoro modułów jest więcej niż jeden, most nie może ich znać
z nazwy — nazwa nieobecnego modułu jechałaby do obrazu, w którym go nie ma.
Przechodzi więc po podkatalogach app/moduly/ i pyta każdy, co wnosi. Katalog
generowania przeniesiony z app/dodatki na app/moduly/dodatki.

ZNALEZIONE PRZY OKAZJI. Po wydzieleniu eksportu okazało się, że jego ścieżki
SZCZĘŚLIWEJ nie sprawdzał żaden test — badano wyłącznie odmowę dla konta bez
uprawnienia. Moduł dostaje zależności z wywołania montującego, więc brak jednej
z nich wyszedłby dopiero przy pierwszym kliknięciu. Dopisany test funkcjonalny
(realny arkusz, sprawdzany aż do nagłówka ZIP-a) i brakujące zależności.

Cztery komentarze w plikach współdzielonych wymieniały zakładkę „Skompiluj",
w tym wheelzoom.js ze wzmianką o „przyszłej zakładce" — to samo zgłoszenie, które
audyt podnosił wcześniej.

TEST ZŁOŻENIA. Buduje węższy produkt NAPRAWDĘ: kopiuje drzewo, usuwa cztery
ekrany i oba moduły, uruchamia aplikację w OSOBNYM PROCESIE (importy są
zapamiętywane, więc sprawdzanie tego w procesie, który moduł już zaimportował,
dawałoby wynik fałszywie pozytywny) i sprawdza, że wstaje, że zachowane ekrany
oddają 200, że usunięte oddają 404 (nie 403 i nie 500), że katalog opisuje ten
obraz, i że w nawigacji nie ma martwych odsyłaczy.

Napisałem najpierw ostrzejszy test — „nazwa ekranu nie pada poza jego modułem" —
i go wyrzuciłem: zgłaszał wzmianki o Horoskopie w plikach współdzielonych, choć
astroklient Horoskop MA. Ślad ma znaczenie wyłącznie wobec konkretnego złożenia,
więc sprawdzenie należy do produktu, nie do mechanizmu.

Testy: presentation 368, logic 342, data 37, render 41.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:32:49 +02:00

151 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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, '&quot;') + '">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.astrololoPredictions = { read: read, remove: remove, key: KEY };
})();