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
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>
139 lines
6.0 KiB
JavaScript
139 lines
6.0 KiB
JavaScript
// Okno postępu przy pisaniu horoskopu.
|
|
//
|
|
// Problem: generowanie trwa minutami, a zwykły POST formularza nie daje żadnego
|
|
// sygnału — aplikacja wygląda na zawieszoną. Zamiast udawanego paska postępu
|
|
// czytamy strumień RZECZYWISTYCH zdarzeń z serwera (NDJSON) i wypisujemy je
|
|
// jako log: budowa promptu, limity modelu, każda tura generowania.
|
|
//
|
|
// Degradacja: jeśli przeglądarka nie umie strumieniować `fetch`, nie przechwytujemy
|
|
// wysyłki — formularz idzie klasycznie i wszystko działa jak wcześniej, tylko bez okna.
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const canStream = typeof fetch === 'function' && typeof ReadableStream === 'function' &&
|
|
typeof TextDecoder === 'function';
|
|
const form = document.querySelector('form[action="/interpret"], form[action="/timeline"]');
|
|
if (!canStream || !form) return;
|
|
|
|
const profile = form.getAttribute('action') === '/timeline' ? 'period' : 'natal';
|
|
const btn = form.querySelector('button[value="horoscope"]');
|
|
if (!btn) return;
|
|
|
|
// --- okno ---------------------------------------------------------------
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'progress-overlay';
|
|
overlay.hidden = true;
|
|
overlay.innerHTML =
|
|
'<div class="progress-box" role="dialog" aria-modal="true" aria-label="Postęp generowania">' +
|
|
'<div class="progress-head">' +
|
|
'<span class="progress-spinner" aria-hidden="true"></span>' +
|
|
'<strong id="progressTitle">Piszę horoskop…</strong>' +
|
|
'<span class="progress-clock" id="progressClock">0:00</span>' +
|
|
'</div>' +
|
|
'<ol class="progress-log" id="progressLog"></ol>' +
|
|
'<p class="muted small">Nie zamykaj tej karty — generowanie trwa na serwerze.</p>' +
|
|
'<div class="actions"><button type="button" class="ghost" id="progressClose" hidden>Zamknij</button></div>' +
|
|
'</div>';
|
|
document.body.appendChild(overlay);
|
|
|
|
const logEl = overlay.querySelector('#progressLog');
|
|
const clockEl = overlay.querySelector('#progressClock');
|
|
const titleEl = overlay.querySelector('#progressTitle');
|
|
const closeEl = overlay.querySelector('#progressClose');
|
|
let timer = null;
|
|
|
|
function addLine(text, kind) {
|
|
const li = document.createElement('li');
|
|
if (kind) li.className = 'log-' + kind;
|
|
const now = new Date();
|
|
li.textContent = String(now.getHours()).padStart(2, '0') + ':' +
|
|
String(now.getMinutes()).padStart(2, '0') + ':' +
|
|
String(now.getSeconds()).padStart(2, '0') + ' ' + text;
|
|
logEl.appendChild(li);
|
|
logEl.scrollTop = logEl.scrollHeight;
|
|
}
|
|
|
|
function startClock() {
|
|
const t0 = Date.now();
|
|
timer = setInterval(function () {
|
|
const s = Math.floor((Date.now() - t0) / 1000);
|
|
clockEl.textContent = Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
|
|
}, 1000);
|
|
}
|
|
|
|
function finish(title, allowClose) {
|
|
if (timer) { clearInterval(timer); timer = null; }
|
|
titleEl.textContent = title;
|
|
overlay.querySelector('.progress-spinner').style.visibility = 'hidden';
|
|
if (allowClose) closeEl.hidden = false;
|
|
}
|
|
|
|
closeEl.addEventListener('click', function () { overlay.hidden = true; });
|
|
|
|
// --- przechwycenie wysyłki ----------------------------------------------
|
|
btn.addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
|
|
if (!form.reportValidity()) return; // te same reguły co przy zwykłej wysyłce
|
|
|
|
const data = new FormData(form);
|
|
data.set('profile', profile);
|
|
|
|
logEl.innerHTML = '';
|
|
closeEl.hidden = true;
|
|
overlay.querySelector('.progress-spinner').style.visibility = '';
|
|
titleEl.textContent = 'Piszę horoskop…';
|
|
overlay.hidden = false;
|
|
startClock();
|
|
addLine('Wysyłam żądanie…');
|
|
|
|
fetch('/horoscope/stream', { method: 'POST', body: data })
|
|
.then(function (response) {
|
|
if (!response.ok || !response.body) throw new Error('HTTP ' + response.status);
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
|
|
function pump() {
|
|
return reader.read().then(function (chunk) {
|
|
if (chunk.done) return;
|
|
buffer += decoder.decode(chunk.value, { stream: true });
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop(); // ostatni może być niepełny
|
|
lines.forEach(function (raw) {
|
|
if (!raw.trim()) return;
|
|
let ev;
|
|
try { ev = JSON.parse(raw); } catch (e) { return; }
|
|
if (ev.type === 'ping') return; // sam heartbeat, nie logujemy
|
|
if (ev.type === 'result') {
|
|
addLine(ev.message || 'Gotowe.', 'ok');
|
|
if (ev.html) {
|
|
const host = document.getElementById('promptResult');
|
|
if (host) host.innerHTML = ev.html;
|
|
}
|
|
finish('Gotowe', true);
|
|
overlay.hidden = true;
|
|
const host = document.getElementById('promptResult');
|
|
if (host) host.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
// Gotowy horoskop ogłaszamy zdarzeniem, zamiast zapisywać go tutaj:
|
|
// to okno postępu, a nie magazyn. Nasłuchuje predictions.js (PRE-22).
|
|
document.dispatchEvent(new CustomEvent('astrololo:horoscope', {
|
|
detail: { profile: profile }
|
|
}));
|
|
return;
|
|
}
|
|
addLine(ev.message || ev.type, ev.type === 'error' ? 'err'
|
|
: ev.type === 'warn' ? 'warn' : null);
|
|
if (ev.type === 'error') finish('Nie udało się', true);
|
|
});
|
|
return pump();
|
|
});
|
|
}
|
|
return pump();
|
|
})
|
|
.catch(function (e) {
|
|
addLine('Połączenie przerwane: ' + e.message, 'err');
|
|
addLine('Możesz spróbować ponownie — nic nie zostało utracone.');
|
|
finish('Nie udało się', true);
|
|
});
|
|
});
|
|
});
|