feat(prezentacja): zakładka „Skompiluj” — zbiorczy raport (PRE-23) #31
Binary file not shown.
@@ -102,6 +102,53 @@ def chart_compute(
|
|||||||
return templates.TemplateResponse(request, "chart.html", ctx)
|
return templates.TemplateResponse(request, "chart.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- Skompiluj: zbiorczy raport (PRE-23) ----------------
|
||||||
|
@app.get("/compile", response_class=HTMLResponse)
|
||||||
|
def compile_form(request: Request):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request, "compile.html",
|
||||||
|
{"result": None, "form": default_form(), "location_label": DEFAULT_LOCATION_LABEL},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/compile", response_class=HTMLResponse)
|
||||||
|
def compile_build(
|
||||||
|
request: Request,
|
||||||
|
person: str = Form(""),
|
||||||
|
date: str = Form(...),
|
||||||
|
time: str = Form(...),
|
||||||
|
tz_offset: float = Form(0.0),
|
||||||
|
lat: float = Form(0.0),
|
||||||
|
lon: float = Form(0.0),
|
||||||
|
house_system: str = Form("whole_sign"),
|
||||||
|
zodiac: str = Form("tropical"),
|
||||||
|
):
|
||||||
|
"""Składa raport: horoskop liczymy TU NA NOWO, a części od AI (interpretacja
|
||||||
|
natalna i predykcje okresowe) dokłada przeglądarka z magazynu (PRE-22/23).
|
||||||
|
|
||||||
|
Dlaczego horoskop liczymy ponownie, zamiast go zapamiętywać: to czysta funkcja
|
||||||
|
danych wejściowych — tanio ją powtórzyć, a odpada trzymanie w przeglądarce
|
||||||
|
dużego wyniku, który mógłby się rozjechać z aktualnym formularzem.
|
||||||
|
"""
|
||||||
|
form = {"person": person, "date": date, "time": time, "tz_offset": tz_offset,
|
||||||
|
"lat": lat, "lon": lon, "house_system": house_system, "zodiac": zodiac}
|
||||||
|
ctx: dict = {"form": form, "result": None, "error": None, "moment": None}
|
||||||
|
try:
|
||||||
|
iso_utc, label = _build_utc(date, time, tz_offset)
|
||||||
|
ctx["moment"] = label
|
||||||
|
ctx["result"] = logic.positions(
|
||||||
|
when_utc_iso=iso_utc, lat=lat, lon=lon,
|
||||||
|
house_system=house_system, zodiac=zodiac,
|
||||||
|
)
|
||||||
|
from app import chartwheel
|
||||||
|
ctx["wheel_svg"] = chartwheel.render(ctx["result"])
|
||||||
|
except (httpx.HTTPError,) as e:
|
||||||
|
ctx["error"] = _logic_error(e)
|
||||||
|
except ValueError as e:
|
||||||
|
ctx["error"] = f"Niepoprawne dane wejściowe: {e}"
|
||||||
|
return templates.TemplateResponse(request, "compile.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
# ---------------- Sygnifikatory (wyszukiwarka w bazach) ----------------
|
# ---------------- Sygnifikatory (wyszukiwarka w bazach) ----------------
|
||||||
@app.get("/significators", response_class=HTMLResponse)
|
@app.get("/significators", response_class=HTMLResponse)
|
||||||
def significators_form(request: Request):
|
def significators_form(request: Request):
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// 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 (natal.js),
|
||||||
|
// 3. predykcje okresowe — z zakładki Kalendarz (predictions.js).
|
||||||
|
//
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Zapamiętana interpretacja natalna (PRE-23).
|
||||||
|
//
|
||||||
|
// Odpowiednik predictions.js, tylko że natalna jest JEDNA — dotyczy momentu
|
||||||
|
// urodzenia, nie wybranego okresu. Ponowne wygenerowanie podmienia poprzednią.
|
||||||
|
//
|
||||||
|
// Po co magazyn: zakładka „Skompiluj" składa raport z trzech kawałków liczonych
|
||||||
|
// w różnych miejscach (horoskop, interpretacja natalna, predykcje okresowe).
|
||||||
|
// Bez zapamiętania trzeba by je generować od nowa na jednej stronie.
|
||||||
|
//
|
||||||
|
// localStorage, tak jak reszta stanu (PRE-21/22) — serwer zostaje bezstanowy.
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var KEY = 'astrololo.natal.v1';
|
||||||
|
|
||||||
|
function read() {
|
||||||
|
try {
|
||||||
|
var v = JSON.parse(localStorage.getItem(KEY) || 'null');
|
||||||
|
return (v && typeof v === 'object' && v.text) ? v : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null; // uszkodzony wpis nie może zablokować strony
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
try { localStorage.removeItem(KEY); } catch (e) { /* trudno */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function val(sel) {
|
||||||
|
var el = document.querySelector(sel);
|
||||||
|
return el ? el.value.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
var text = val('#horoscopeText');
|
||||||
|
if (!text) return false; // nie ma czego zapamiętać
|
||||||
|
var entry = {
|
||||||
|
text: text,
|
||||||
|
person: val('input[name=person]'),
|
||||||
|
date: val('input[name=date]'),
|
||||||
|
time: val('input[name=time]'),
|
||||||
|
tz_offset: val('input[name=tz_offset]'),
|
||||||
|
lat: val('input[name=lat]'),
|
||||||
|
lon: val('input[name=lon]'),
|
||||||
|
saved_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
localStorage.setItem(KEY, JSON.stringify(entry));
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false; // przepełniony magazyn — zgłaszamy wyżej
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function note(msg) {
|
||||||
|
var el = document.getElementById('natalNote');
|
||||||
|
if (el) el.textContent = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCurrent() {
|
||||||
|
var text = val('#horoscopeText');
|
||||||
|
if (!text) return;
|
||||||
|
if (save()) {
|
||||||
|
note('Interpretacja natalna zapamiętana — trafi do raportu (zakładka Skompiluj).');
|
||||||
|
} else {
|
||||||
|
// Interpretacje bywają długie; cicha strata byłaby najgorsza.
|
||||||
|
note('Nie udało się zapamiętać — magazyn przeglądarki pełny. '
|
||||||
|
+ 'Usuń starsze predykcje i spróbuj ponownie.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ready(fn) {
|
||||||
|
if (document.readyState !== 'loading') fn();
|
||||||
|
else document.addEventListener('DOMContentLoaded', fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
ready(function () {
|
||||||
|
if (!document.getElementById('natalNote')) return; // nie ta zakładka
|
||||||
|
|
||||||
|
// 1) interpretacja policzona przez okno postępu (strumień)
|
||||||
|
document.addEventListener('astrololo:horoscope', function (e) {
|
||||||
|
if (!e.detail || e.detail.profile === 'natal') saveCurrent();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2) wariant bez strumienia: strona wróciła z gotowym wynikiem po zwykłym
|
||||||
|
// POST. Zapis nadpisuje jeden slot, więc odświeżenie niczego nie mnoży.
|
||||||
|
if (document.querySelector('#horoscopeText')) saveCurrent();
|
||||||
|
|
||||||
|
var status = read();
|
||||||
|
if (status) {
|
||||||
|
note('W raporcie jest zapamiętana interpretacja natalna z '
|
||||||
|
+ String(status.saved_at).slice(0, 10) + '.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.astrololoNatal = { read: read, clear: clear, key: KEY };
|
||||||
|
})();
|
||||||
@@ -152,3 +152,15 @@ textarea.prompt:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|||||||
.progress-log li.log-ok { color: #7fd18b; }
|
.progress-log li.log-ok { color: #7fd18b; }
|
||||||
.progress-log li.log-warn { color: #e0c060; }
|
.progress-log li.log-warn { color: #e0c060; }
|
||||||
.progress-log li.log-err { color: #ef6b6b; }
|
.progress-log li.log-err { color: #ef6b6b; }
|
||||||
|
|
||||||
|
/* Zakładka „Skompiluj" (PRE-23) — składanie raportu z trzech źródeł. */
|
||||||
|
.readiness { list-style: none; margin: .3rem 0 1rem; padding: 0; }
|
||||||
|
.readiness li { padding: .2rem 0; }
|
||||||
|
.readiness .rdy-ok { color: #7fd18b; }
|
||||||
|
.readiness .rdy-missing { color: #e0c060; }
|
||||||
|
.report-head { margin: 1.5rem 0 .5rem; }
|
||||||
|
.report-head h2 { margin: 0; font-size: 1.4rem; }
|
||||||
|
/* Treść od AI bywa długa — white-space:pre-wrap zachowuje akapity modelu,
|
||||||
|
bez przepisywania ich na HTML. */
|
||||||
|
.report-text { white-space: pre-wrap; background: var(--panel); border: 1px solid var(--line);
|
||||||
|
border-radius: 10px; padding: .8rem 1rem; margin-top: .4rem; line-height: 1.6; }
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<a href="/interpret" class="{% block nav_interp %}{% endblock %}">Interpretacje</a>
|
<a href="/interpret" class="{% block nav_interp %}{% endblock %}">Interpretacje</a>
|
||||||
<a href="/timeline" class="{% block nav_timeline %}{% endblock %}">Kalendarz</a>
|
<a href="/timeline" class="{% block nav_timeline %}{% endblock %}">Kalendarz</a>
|
||||||
<a href="/significators" class="{% block nav_sig %}{% endblock %}">Sygnifikatory</a>
|
<a href="/significators" class="{% block nav_sig %}{% endblock %}">Sygnifikatory</a>
|
||||||
|
<a href="/compile" class="{% block nav_compile %}{% endblock %}">Skompiluj</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Skompiluj{% endblock %}
|
||||||
|
{% block nav_compile %}active{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p class="sub">Składa w jedno: policzony horoskop, interpretację natalną z AI oraz wszystkie
|
||||||
|
zapamiętane predykcje okresowe. Dane pobiera z pozostałych zakładek — nie trzeba nic przepisywać.</p>
|
||||||
|
|
||||||
|
{# Gotowość materiału — wypełniana po stronie przeglądarki (compile.js), bo część
|
||||||
|
kawałków (AI) mieszka w magazynie lokalnym, nie na serwerze. #}
|
||||||
|
<div id="readiness"></div>
|
||||||
|
|
||||||
|
<form method="post" action="/compile">
|
||||||
|
<div class="grid">
|
||||||
|
<label title="Trafia do nagłówka raportu PDF (PRE-24). Nie jest nigdzie wysyłane poza aplikację ani zapisywane na serwerze.">Imię i nazwisko
|
||||||
|
<input type="text" name="person" autocomplete="name" placeholder="np. Jan Kowalski"
|
||||||
|
value="{{ form.person or '' }}">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="grid">
|
||||||
|
<label>Data
|
||||||
|
<input type="date" name="date" value="{{ form.date or '' }}" required>
|
||||||
|
</label>
|
||||||
|
<label>Godzina (lokalna)
|
||||||
|
<input type="time" name="time" value="{{ form.time or '' }}" required>
|
||||||
|
</label>
|
||||||
|
<label title="Przesunięcie czasu lokalnego względem GMT/UTC. Krok 15 minut (0,25 h) — bo część stref ma przesunięcie o pół lub ćwierć godziny, np. Indie +5:30, Nepal +5:45.">Offset względem GMT (h)
|
||||||
|
<input type="number" name="tz_offset" step="0.25" min="-12" max="14"
|
||||||
|
value="{{ form.tz_offset if form.tz_offset is not none else 0 }}">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="grid">
|
||||||
|
<label>Szerokość (lat, + N)
|
||||||
|
<input type="number" name="lat" step="0.0001" value="{{ form.lat if form.lat is not none else 0 }}">
|
||||||
|
</label>
|
||||||
|
<label>Długość (lon, + E)
|
||||||
|
<input type="number" name="lon" step="0.0001" value="{{ form.lon if form.lon is not none else 0 }}">
|
||||||
|
</label>
|
||||||
|
<label>System domów
|
||||||
|
<select name="house_system">
|
||||||
|
{% set hs = form.house_system or 'whole_sign' %}
|
||||||
|
<option value="whole_sign" {{ 'selected' if hs == 'whole_sign' else '' }}>Whole Sign</option>
|
||||||
|
<option value="equal" {{ 'selected' if hs == 'equal' else '' }}>Equal</option>
|
||||||
|
<option value="porphyry" {{ 'selected' if hs == 'porphyry' else '' }}>Porphyry</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Zodiak
|
||||||
|
<select name="zodiac">
|
||||||
|
{% set zd = form.zodiac or 'tropical' %}
|
||||||
|
<option value="tropical" {{ 'selected' if zd == 'tropical' else '' }}>Tropikalny</option>
|
||||||
|
<option value="sidereal_lahiri" {{ 'selected' if zd == 'sidereal_lahiri' else '' }}>Syderyczny (Lahiri)</option>
|
||||||
|
<option value="sidereal_fagan_bradley" {{ 'selected' if zd == 'sidereal_fagan_bradley' else '' }}>Syderyczny (Fagan-Bradley)</option>
|
||||||
|
<option value="sidereal_krishnamurti" {{ 'selected' if zd == 'sidereal_krishnamurti' else '' }}>Syderyczny (Krishnamurti)</option>
|
||||||
|
<option value="draconic" {{ 'selected' if zd == 'draconic' else '' }}>Draconic</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit">Złóż podsumowanie</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if error %}<div class="error">{{ error }}</div>{% endif %}
|
||||||
|
|
||||||
|
{% if result %}
|
||||||
|
{# ── 1. Nagłówek: kto ──────────────────────────────────────────────── #}
|
||||||
|
<div class="report-head">
|
||||||
|
<h2 id="reportPerson">{{ form.person or 'Bez podanego imienia' }}</h2>
|
||||||
|
<p class="muted small">
|
||||||
|
{{ form.date }} {{ form.time }} · offset {{ form.tz_offset }} h względem GMT ·
|
||||||
|
{{ '%.4f'|format(form.lat|float) }}, {{ '%.4f'|format(form.lon|float) }}
|
||||||
|
{% if moment %}· {{ moment }}{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ── 2. Kosmogram ─────────────────────────────────────────────────── #}
|
||||||
|
{% if wheel_svg %}
|
||||||
|
<figure class="wheel-fig">
|
||||||
|
{{ wheel_svg | safe }}
|
||||||
|
<figcaption class="muted small">Kosmogram — kliknij, aby powiększyć.</figcaption>
|
||||||
|
</figure>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# ── 3. Dane policzone ────────────────────────────────────────────── #}
|
||||||
|
<div class="meta">Horoskop · silnik {{ result.engine }}
|
||||||
|
{% if result.house_system %}· domy {{ result.house_system }}{% endif %}
|
||||||
|
{% if result.zodiac %}· zodiak {{ result.zodiac }}{% endif %}</div>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Sym.</th><th>Obiekt</th><th>Znak</th><th>W znaku</th><th>Dom</th><th>Kier.</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in result.positions %}
|
||||||
|
<tr>
|
||||||
|
<td class="glyph">{{ p.glyph or '' }}{% if p.sign_glyph %} {{ p.sign_glyph }}{% endif %}</td>
|
||||||
|
<td>{{ p.name }}</td><td>{{ p.sign }}</td><td class="mono">{{ p.in_sign }}</td>
|
||||||
|
<td>{{ p.house if p.house is defined else '—' }}</td>
|
||||||
|
<td class="{{ 'retro' if p.direction == 'Rx' else '' }}">{{ p.direction }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if result.angles %}
|
||||||
|
<table class="angles">
|
||||||
|
<thead><tr><th>Oś</th><th>Znak</th><th>W znaku</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for key in ["Asc", "MC", "Dsc", "IC"] %}
|
||||||
|
{% set a = result.angles[key] %}
|
||||||
|
<tr><td>{{ a.name }}</td><td>{{ a.sign }}</td><td class="mono">{{ a.in_sign }}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# ── 4. i 5. Części od AI — wstawia przeglądarka z magazynu ───────── #}
|
||||||
|
<div id="reportNatal"></div>
|
||||||
|
<div id="reportPredictions"></div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Magazyny wczytujemy dla ich API odczytu. Ich własne UI samo się wyłącza —
|
||||||
|
każdy sprawdza, czy jego kontener jest na stronie (tu go nie ma). #}
|
||||||
|
<script src="/static/natal.js"></script>
|
||||||
|
<script src="/static/predictions.js"></script>
|
||||||
|
<script src="/static/compile.js"></script>
|
||||||
|
{% endblock %}
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if location_label %}<p class="muted small">Wstępnie wpisano lokalizację: <strong>{{ location_label }}</strong> ({{ form.lat }}, {{ form.lon }}). Zmień pola lub kliknij „Tu i teraz".</p>{% endif %}
|
{% if location_label %}<p class="muted small">Wstępnie wpisano lokalizację: <strong>{{ location_label }}</strong> ({{ form.lat }}, {{ form.lon }}). Zmień pola lub kliknij „Tu i teraz".</p>{% endif %}
|
||||||
{% include "_prompt_block.html" %}
|
{% include "_prompt_block.html" %}
|
||||||
|
<p class="muted small" id="natalNote"></p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
||||||
<button type="submit" name="action" value="report">Szukaj interpretacji</button>
|
<button type="submit" name="action" value="report">Szukaj interpretacji</button>
|
||||||
@@ -99,4 +100,6 @@
|
|||||||
<script src="/static/copy.js"></script>
|
<script src="/static/copy.js"></script>
|
||||||
<script src="/static/models.js"></script>
|
<script src="/static/models.js"></script>
|
||||||
<script src="/static/progress.js"></script>
|
<script src="/static/progress.js"></script>
|
||||||
|
{# po progress.js — nasłuchuje zdarzenia o gotowej interpretacji (PRE-23) #}
|
||||||
|
<script src="/static/natal.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""Zakładka „Skompiluj" — zbiorczy raport (PRE-23).
|
||||||
|
|
||||||
|
Raport składa się z trzech kawałków liczonych w różnych miejscach: horoskopu
|
||||||
|
(serwer), interpretacji natalnej i predykcji okresowych (magazyn przeglądarki).
|
||||||
|
Pilnujemy, żeby zakładka umiała je zebrać, a przede wszystkim — żeby POWIEDZIAŁA,
|
||||||
|
czego brakuje. Niekompletny raport, o którym dowiadujesz się dopiero z PDF-a,
|
||||||
|
byłby najgorszym wynikiem.
|
||||||
|
"""
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||||
|
TPL = (APP / "templates" / "compile.html").read_text(encoding="utf-8")
|
||||||
|
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
|
||||||
|
JS = (APP / "static" / "compile.js").read_text(encoding="utf-8")
|
||||||
|
NATAL_JS = (APP / "static" / "natal.js").read_text(encoding="utf-8")
|
||||||
|
INTERPRET = (APP / "templates" / "interpret.html").read_text(encoding="utf-8")
|
||||||
|
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _client():
|
||||||
|
import os
|
||||||
|
|
||||||
|
os.environ.pop("APP_PASSWORD", None) # bez bramki logowania w teście
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
# ───────────────────────────────── nawigacja i trasa ─────────────────────
|
||||||
|
|
||||||
|
def test_tab_is_in_the_menu():
|
||||||
|
assert 'href="/compile"' in BASE and "Skompiluj" in BASE
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_renders_empty_form():
|
||||||
|
r = _client().get("/compile")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert "Złóż podsumowanie" in r.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_routes_exist():
|
||||||
|
assert '@app.get("/compile"' in MAIN and '@app.post("/compile"' in MAIN
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────── kolejność w raporcie ────────────────────────
|
||||||
|
|
||||||
|
def test_report_order_matches_the_brief():
|
||||||
|
"""Prośba partnerów: najpierw imię i nazwisko, potem wprowadzone dane, potem
|
||||||
|
RYSUNEK kosmogramu, a dopiero po nim interpretacja natalna i predykcje."""
|
||||||
|
person = TPL.index('id="reportPerson"')
|
||||||
|
wheel = TPL.index("wheel_svg")
|
||||||
|
natal = TPL.index('id="reportNatal"')
|
||||||
|
preds = TPL.index('id="reportPredictions"')
|
||||||
|
assert person < wheel < natal < preds
|
||||||
|
|
||||||
|
|
||||||
|
def test_person_name_heads_the_report():
|
||||||
|
assert 'id="reportPerson"' in TPL
|
||||||
|
assert "form.person" in TPL
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────── zbieranie materiału z magazynów ────────────────
|
||||||
|
|
||||||
|
def test_reads_stores_through_their_api_not_raw_storage():
|
||||||
|
"""Format danych ma jednego właściciela — moduł, który je zapisuje.
|
||||||
|
Sięganie tu wprost do localStorage rozjechałoby się przy pierwszej zmianie."""
|
||||||
|
assert "window.astrololoNatal" in JS
|
||||||
|
assert "window.astrololoPredictions" in JS
|
||||||
|
# chodzi o brak BEZPOŚREDNIEGO dostępu, nie o samo słowo (pada w komentarzu)
|
||||||
|
assert "localStorage.getItem" not in JS
|
||||||
|
assert "localStorage.setItem" not in JS
|
||||||
|
|
||||||
|
|
||||||
|
def test_stores_are_loaded_on_the_tab():
|
||||||
|
for src in ("natal.js", "predictions.js", "compile.js"):
|
||||||
|
assert src in TPL, f"brak {src} na zakładce"
|
||||||
|
|
||||||
|
|
||||||
|
def test_stores_load_before_the_assembler():
|
||||||
|
"""Szukamy TAGÓW skryptów, nie samych nazw — te padają też w komentarzach."""
|
||||||
|
def tag(name):
|
||||||
|
return TPL.index('src="/static/' + name + '"')
|
||||||
|
|
||||||
|
assert tag("natal.js") < tag("compile.js")
|
||||||
|
assert tag("predictions.js") < tag("compile.js")
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────── mówi, czego brakuje ─────────────────────────
|
||||||
|
|
||||||
|
def test_shows_what_is_missing():
|
||||||
|
assert 'id="readiness"' in TPL
|
||||||
|
assert "rdy-missing" in JS
|
||||||
|
for hint in ("Interpretacje", "Kalendarz"):
|
||||||
|
assert hint in JS, f"brak podpowiedzi, gdzie uzupełnić: {hint}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_three_parts_are_checked():
|
||||||
|
for label in ("Policzony horoskop", "Interpretacja natalna", "Predykcje okresowe"):
|
||||||
|
assert label in JS
|
||||||
|
|
||||||
|
|
||||||
|
# ───────────────────────────── natalna zapamiętywana ─────────────────────
|
||||||
|
|
||||||
|
def test_natal_is_captured_on_interpret_tab():
|
||||||
|
assert "natal.js" in INTERPRET
|
||||||
|
assert 'id="natalNote"' in INTERPRET
|
||||||
|
|
||||||
|
|
||||||
|
def test_natal_store_keeps_a_single_entry():
|
||||||
|
"""Natalna jest JEDNA — dotyczy momentu urodzenia, nie okresu. Ponowne
|
||||||
|
wygenerowanie ma podmienić, nie dokładać."""
|
||||||
|
assert "setItem" in NATAL_JS and "push" not in NATAL_JS
|
||||||
|
|
||||||
|
|
||||||
|
def test_natal_only_stores_natal_profile():
|
||||||
|
assert "'natal'" in NATAL_JS
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_storage_is_reported():
|
||||||
|
assert "pełny" in NATAL_JS
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────── bezpieczeństwo ──────────────────────────
|
||||||
|
|
||||||
|
def test_ai_text_is_escaped_before_injection():
|
||||||
|
"""Treść od modelu wstawiamy do DOM — bez ucieczki byłby to wektor wstrzyknięcia."""
|
||||||
|
assert "function esc" in JS
|
||||||
|
assert "replace(/</g, '<')" in JS
|
||||||
|
|
||||||
|
|
||||||
|
def test_does_nothing_on_other_tabs():
|
||||||
|
assert "if (!document.getElementById('readiness')) return;" in JS
|
||||||
@@ -42,10 +42,11 @@ def test_person_value_survives_submit(name):
|
|||||||
|
|
||||||
|
|
||||||
def test_handlers_accept_and_echo_person():
|
def test_handlers_accept_and_echo_person():
|
||||||
"""Trzy handlery renderujące formularz przyjmują `person` i wkładają go
|
"""Każdy handler renderujący formularz przyjmuje `person` i wkłada go
|
||||||
z powrotem do `form`."""
|
z powrotem do `form` — inaczej imię znikałoby po przeliczeniu.
|
||||||
assert MAIN_PY.count("person: str = Form") == 3
|
Cztery: Horoskop, Interpretacje, Kalendarz, Skompiluj (PRE-23)."""
|
||||||
assert MAIN_PY.count('"person": person') == 3
|
assert MAIN_PY.count("person: str = Form") == 4
|
||||||
|
assert MAIN_PY.count('"person": person') == 4
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────── synchronizacja między zakładkami ────────────────
|
# ─────────────────────── synchronizacja między zakładkami ────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user