d094a145b1
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4m44s
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 / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m43s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Drugi z pięciu kroków budowy trzech produktów. Czysty refaktor — zachowanie
aplikacji się nie zmienia, liczba testów rośnie tylko o nowe.
DLACZEGO WARUNEK W SZABLONIE NIE WYSTARCZA. Dotąd generowanie było chowane przez
`{% if can(request, 'ai') %}`. To sprawia, że funkcji nie WIDAĆ, ale nie że jej
NIE MA: plik szablonu dalej leży w obrazie i dalej zawiera jej nazwy, więc `grep`
po kontenerze pokazuje wszystko, czego warunek nie pokazał na ekranie. Astroklient
ma nie mieć śladu, nie mieć wyłączoną funkcję — więc granica musi być KATALOGIEM.
CO SIĘ PRZENIOSŁO do app/dodatki/: dwa szablony, pięć plików statycznych, trasa
strumienia, cztery metody klienta warstwy logicznej, wpis w katalogu funkcji,
wpisy tras i zasobów, akcje formularza i reguły CSS nazywające funkcję
(`textarea.prompt`).
JAK APLIKACJA O TO PYTA. Neutralny most `app/rozszerzenia.py` odpowiada wyłącznie
na pytanie „czy coś jest podpięte i co wnosi". Sam nie wymienia ani jednej nazwy —
pilnuje tego osobny test, bo most jest w KAŻDYM obrazie. Dlatego też katalog
nazywa się `dodatki`, a nie `ai`: nazwa w instrukcji importu byłaby dokładnie tym
śladem, którego wydzielanie ma się pozbyć.
TRZY MIEJSCA, KTÓRE OKAZAŁY SIĘ TRUDNE:
1. Pola formularza. FastAPI czyta je z SYGNATURY, a wspólny handler nie może
wymieniać `prompt_budget` ani `llm_provider`. Rozwiązane zależnością: moduł
deklaruje własne pola u siebie, handler wie tylko, że dostaje słownik.
2. Klient warstwy logicznej jest wspólny, więc metody `prompt`/`horoscope`/
`llm_models` musiały z niego wyjść. Wspólny klient daje teraz samą drogę
w dół (`wywolaj`, `pobierz`, `strumien`) — z szyfrowaniem łącza i tokenem
międzywarstwowym; co nią pojedzie, jest sprawą modułu.
3. Podtytuł ekranu Skompiluj miał wariant „z AI" i wariant bez. Zamiast warunku
jedno zdanie prawdziwe niezależnie od tego, jakie moduły są w obrazie.
DOCKERFILE. `COPY . .` wnosiło do obrazu także testy — a plik testowy nazywa
funkcje wprost. Teraz wchodzi wyłącznie `app/`.
TEST GRANICY. `test_ai_tylko_w_module.py` przechodzi po wszystkim poza modułem
i szuka siedemnastu słów. Znalazł dwanaście resztek, których nie widziałem:
komentarze w main.py i security.py, przykład `models.js` w komentarzu features.py,
komentarze w compile.js i w trzech arkuszach, oraz regułę `textarea.prompt`,
która przy rozbijaniu CSS trafiła do arkuszy ekranów zamiast do modułu.
Ma też kontrolę pozytywną (te słowa MAJĄ padać w module — inaczej test
przechodziłby, gdyby funkcję wydrążono) i test odłączalności mostu.
SPRAWDZONE NA KOPII BEZ MODUŁU: aplikacja startuje, wszystkie ekrany oddają 200,
katalog ma 13 funkcji zamiast 14, trasy strumienia nie ma, spreparowane
`action=prompt` wraca do akcji domyślnej bez śladu, a `grep` po drzewie nie
znajduje ani jednego ze słów.
Testy: presentation 362, logic 342, data 37, render 41.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
177 lines
6.8 KiB
JavaScript
177 lines
6.8 KiB
JavaScript
// Zakładka „Skompiluj" — składanie raportu (PRE-23).
|
||
//
|
||
// Raport ma trzy kawałki liczone w różnych miejscach:
|
||
// 1. horoskop — liczy serwer przy wysłaniu tego formularza,
|
||
// 2. interpretacja natalna — z zakładki Interpretacje,
|
||
// 3. predykcje okresowe — z zakładki Kalendarz.
|
||
// Kawałki 2 i 3 wnosi moduł opcjonalny. Gdy go nie ma, magazynów nie ma również
|
||
// i czytanie ich zwraca pusto — raport składa się wtedy z samego horoskopu.
|
||
//
|
||
// Punkty 2 i 3 mieszkają w magazynie przeglądarki, więc to tutaj je dokładamy.
|
||
// Odczytujemy je przez API tamtych modułów, a nie sięgając wprost do
|
||
// localStorage — dzięki temu format danych ma JEDNEGO właściciela.
|
||
//
|
||
// Pokazujemy też, CZEGO BRAKUJE. Bez tego użytkownik złożyłby niekompletny raport
|
||
// i dowiedział się o tym dopiero po otwarciu PDF-a.
|
||
(function () {
|
||
'use strict';
|
||
|
||
function esc(s) {
|
||
return String(s == null ? '' : s)
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
function readNatal() {
|
||
return (window.astrololoNatal && window.astrololoNatal.read()) || null;
|
||
}
|
||
|
||
function readPredictions() {
|
||
return (window.astrololoPredictions && window.astrololoPredictions.read()) || [];
|
||
}
|
||
|
||
function item(ok, label, hint) {
|
||
return '<li class="' + (ok ? 'rdy-ok' : 'rdy-missing') + '">' +
|
||
(ok ? '✓ ' : '✗ ') + esc(label) +
|
||
(ok ? '' : ' <span class="muted small">— ' + esc(hint) + '</span>') + '</li>';
|
||
}
|
||
|
||
function renderReadiness(hasChart) {
|
||
var host = document.getElementById('readiness');
|
||
if (!host) return;
|
||
var natal = readNatal();
|
||
var preds = readPredictions();
|
||
var parts = [
|
||
item(hasChart, 'Policzony horoskop', 'wypełnij dane i kliknij „Złóż podsumowanie"'),
|
||
item(!!natal, 'Interpretacja natalna',
|
||
'wygeneruj ją na zakładce Interpretacje — zapamięta się sama'),
|
||
item(preds.length > 0,
|
||
'Predykcje okresowe' + (preds.length ? ' (' + preds.length + ')' : ''),
|
||
'policz horoskop na wybrany okres w Kalendarzu')
|
||
];
|
||
host.innerHTML = '<div class="meta">Materiał do raportu</div><ul class="readiness">' +
|
||
parts.join('') + '</ul>';
|
||
}
|
||
|
||
function renderNatal() {
|
||
var host = document.getElementById('reportNatal');
|
||
if (!host) return;
|
||
var natal = readNatal();
|
||
if (!natal) {
|
||
host.innerHTML = '<div class="meta">Interpretacja natalna</div>' +
|
||
'<p class="muted small">Brak — wygeneruj ją na zakładce Interpretacje.</p>';
|
||
return;
|
||
}
|
||
host.innerHTML = '<div class="meta">Interpretacja natalna ' +
|
||
'<span class="muted small">· zapamiętana ' + esc(String(natal.saved_at).slice(0, 10)) +
|
||
'</span></div><div class="report-text">' + esc(natal.text) + '</div>';
|
||
}
|
||
|
||
function renderPredictions() {
|
||
var host = document.getElementById('reportPredictions');
|
||
if (!host) return;
|
||
var preds = readPredictions();
|
||
if (!preds.length) {
|
||
host.innerHTML = '<div class="meta">Predykcje okresowe</div>' +
|
||
'<p class="muted small">Brak — policz horoskop na wybrany okres w Kalendarzu.</p>';
|
||
return;
|
||
}
|
||
var blocks = preds.map(function (p) {
|
||
return '<div class="meta">' + esc(p.from_date) + ' – ' + esc(p.to_date) + '</div>' +
|
||
'<div class="report-text">' + esc(p.text) + '</div>';
|
||
}).join('');
|
||
host.innerHTML = '<div class="meta">Predykcje okresowe (' + preds.length + ')</div>' + blocks;
|
||
}
|
||
|
||
function ready(fn) {
|
||
if (document.readyState !== 'loading') fn();
|
||
else document.addEventListener('DOMContentLoaded', fn);
|
||
}
|
||
|
||
// ── PDF (PRE-24) ────────────────────────────────────────────────────
|
||
// Te kawałki mieszkają w przeglądarce, więc raport składamy TU i wysyłamy
|
||
// do serwera, który dokłada kosmogram i woła usługę render.
|
||
function formData() {
|
||
function v(name) {
|
||
var el = document.querySelector('[name=' + name + ']');
|
||
return el ? el.value : '';
|
||
}
|
||
function checked(name) {
|
||
var el = document.querySelector('[name=' + name + ']');
|
||
return !!(el && el.checked);
|
||
}
|
||
function multi(name) {
|
||
var out = [];
|
||
document.querySelectorAll('[name=' + name + ']:checked').forEach(function (el) { out.push(el.value); });
|
||
return out;
|
||
}
|
||
// Opcje (stacje, tabele, porównanie domów) MUSZĄ jechać do PDF-a — inaczej
|
||
// PDF liczyłby okrojony horoskop i miałby braki względem podsumowania.
|
||
return {
|
||
date: v('date'), time: v('time'), tz_offset: v('tz_offset'),
|
||
lat: v('lat'), lon: v('lon'),
|
||
house_system: v('house_system'), zodiac: v('zodiac'),
|
||
house_systems: multi('house_systems'),
|
||
aspect_orb: v('aspect_orb'), aspect_luminary_bonus: v('aspect_luminary_bonus'),
|
||
aspect_minor: checked('aspect_minor'),
|
||
stations: checked('stations'), tables: checked('tables'),
|
||
place: (document.querySelector('#geoSearch') || {}).value || ''
|
||
};
|
||
}
|
||
|
||
function download(blob) {
|
||
var url = URL.createObjectURL(blob);
|
||
var a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'raport.pdf';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
// Zwalniamy obiekt dopiero po kliknięciu — wcześniej przeglądarka nie zdąży
|
||
// rozpocząć pobierania.
|
||
setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
|
||
}
|
||
|
||
function buildPdf(btn, note) {
|
||
var person = (document.querySelector('[name=person]') || {}).value || '';
|
||
var payload = {
|
||
person: person,
|
||
data: formData(),
|
||
natal: readNatal() || {},
|
||
predictions: readPredictions()
|
||
};
|
||
btn.disabled = true;
|
||
note.textContent = 'Składam PDF — LaTeX potrzebuje chwili przy obszernym tekście…';
|
||
fetch('/compile/pdf', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
}).then(function (r) {
|
||
if (!r.ok) {
|
||
return r.json().catch(function () { return {}; }).then(function (d) {
|
||
throw new Error(d.detail || ('HTTP ' + r.status));
|
||
});
|
||
}
|
||
return r.blob();
|
||
}).then(function (blob) {
|
||
download(blob);
|
||
note.textContent = 'Gotowe — PDF pobrany.';
|
||
}).catch(function (e) {
|
||
note.textContent = 'Nie udało się złożyć PDF-a: ' + e.message;
|
||
}).finally(function () {
|
||
btn.disabled = false;
|
||
});
|
||
}
|
||
|
||
ready(function () {
|
||
if (!document.getElementById('readiness')) return; // nie ta zakładka
|
||
// Horoskop jest w dokumencie tylko po wysłaniu formularza.
|
||
renderReadiness(!!document.getElementById('reportPerson'));
|
||
renderNatal();
|
||
renderPredictions();
|
||
|
||
var btn = document.getElementById('pdfBtn');
|
||
var note = document.getElementById('pdfNote');
|
||
if (btn && note) btn.addEventListener('click', function () { buildPdf(btn, note); });
|
||
});
|
||
})();
|