Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d579de34a | |||
| db3d1e5117 | |||
| 4a86d34f5a | |||
| fb317172ff | |||
| f6323cac10 | |||
| 243d02d55b |
@@ -6,8 +6,8 @@ i nie w bazie.
|
||||
|
||||
## API
|
||||
- `POST /api/query` → `QueryRequest` → `QueryResponse`
|
||||
- `POST /chart/positions` → `{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05) + aspekty główne (LOG-06). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`.
|
||||
- `POST /chart/report` → `{when_utc, lat, lon, limit?}` → wynik obliczeń wyszukany w bazie: fasety sygnifikatorów **w znaku / w domu / w aspekcie**, z rozwinięciem skrótów i odsiewaniem duplikatów (duplikat = ten sam sygnifikator i opis) — zalążek LOG-15/16/18/19
|
||||
- `POST /chart/positions` → `{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05) + aspekty główne z applying/separating (LOG-06). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`.
|
||||
- `POST /chart/report` → `{when_utc, lat, lon, limit?}` → wynik obliczeń wyszukany w bazie: fasety sygnifikatorów **w znaku / w domu / w aspekcie**, z rozwinięciem skrótów, odsiewaniem duplikatów (ten sam sygnifikator i opis), rankingiem siły (LOG-21) oraz opcją group (grupowanie identycznych opisów)
|
||||
- `POST /chart/compare` → jak wyżej → raport różnic dwóch silników (LOG-26; wymaga silnika B)
|
||||
- `GET /health` (sprawdza też warstwę bazodanową)
|
||||
|
||||
|
||||
@@ -34,10 +34,29 @@ def separation(a: float, b: float) -> float:
|
||||
return min(d, 360.0 - d)
|
||||
|
||||
|
||||
def _is_applying(la: float, lb: float, sa: float, sb: float, angle: float, dt: float = 0.01) -> bool | None:
|
||||
"""Czy aspekt aplikuje (dokładność 0° dopiero nastąpi)?
|
||||
|
||||
Porównujemy odchyłkę od dokładnego kąta teraz i po małym kroku czasu
|
||||
(pozycje przesunięte o prędkość·dt). Malejąca odchyłka = applying.
|
||||
dt celowo małe (0,01 doby), by szybki Księżyc nie „przeskoczył" dokładności.
|
||||
Zwraca None, gdy brak prędkości (nie da się rozstrzygnąć).
|
||||
"""
|
||||
if sa is None or sb is None:
|
||||
return None
|
||||
dev_now = abs(separation(la, lb) - angle)
|
||||
dev_next = abs(separation(la + sa * dt, lb + sb * dt) - angle)
|
||||
return dev_next < dev_now
|
||||
|
||||
|
||||
def find_aspects(
|
||||
positions: list[dict], orb: float = DEFAULT_ORB, luminary_bonus: float = LUMINARY_BONUS
|
||||
) -> list[dict]:
|
||||
"""positions: dicty z 'name' i 'decimal' (długość). Zwraca listę aspektów."""
|
||||
"""positions: dicty z 'name', 'decimal' (długość) i opcjonalnie 'speed' (°/dobę).
|
||||
|
||||
Zwraca listę aspektów głównych; gdy znane są prędkości, każdy aspekt ma
|
||||
applying (bool) i skrót 'as': 'A'/'S' (aplikacyjny/separacyjny).
|
||||
"""
|
||||
out: list[dict] = []
|
||||
n = len(positions)
|
||||
for i in range(n):
|
||||
@@ -51,9 +70,16 @@ def find_aspects(
|
||||
for asp, angle in MAJOR.items():
|
||||
dev = abs(sep - angle)
|
||||
if dev <= allowed:
|
||||
out.append({
|
||||
applying = _is_applying(
|
||||
float(la), float(lb), a.get("speed"), b.get("speed"), angle
|
||||
)
|
||||
row = {
|
||||
"obj1": a["name"], "obj2": b["name"],
|
||||
"aspect": asp, "orb": round(dev, 2),
|
||||
})
|
||||
"aspect": asp, "orb": round(dev, 2), "allowed": round(allowed, 2),
|
||||
}
|
||||
if applying is not None:
|
||||
row["applying"] = applying
|
||||
row["as"] = "A" if applying else "S"
|
||||
out.append(row)
|
||||
break # jedna para = jeden aspekt
|
||||
return out
|
||||
|
||||
@@ -83,6 +83,7 @@ class ReportRequest(BaseModel):
|
||||
lat: float = 0.0
|
||||
lon: float = 0.0
|
||||
limit: int = 5000
|
||||
group: bool = False # grupowanie identycznych opisów
|
||||
|
||||
|
||||
@app.post("/chart/report")
|
||||
@@ -99,7 +100,7 @@ def chart_report(req: ReportRequest) -> dict:
|
||||
try:
|
||||
report = build_report(
|
||||
chart["positions"], DataClient(),
|
||||
aspects=chart.get("aspects"), per_object_limit=req.limit,
|
||||
aspects=chart.get("aspects"), per_object_limit=req.limit, group=req.group,
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
return {"engine": engine.name, "objects": [], "data_error": f"Warstwa danych niedostępna: {e}"}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Punktacja siły trafień — do rankingowania faset (zalążek LOG-21).
|
||||
|
||||
v1 liczy siłę z sygnałów OBLICZALNYCH:
|
||||
- typ fasety (aspekt zwykle mocniejszy od znaku/domu),
|
||||
- rodzaj aspektu (koniunkcja/opozycja mocniejsze od sekstyla),
|
||||
- ciasnota orbu (im bliżej dokładności, tym mocniej).
|
||||
|
||||
Wszystko konfigurowalne. HOOK NA PRZYSZŁOŚĆ: gdy w SIGNIFICATORS KEY zostaną
|
||||
wypełnione kolumny `countas*`/`level*`, można je tu domieszać per sygnifikator.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# bazowe wagi faset (łatwe do strojenia)
|
||||
FACET_BASE = {"sign": 5.0, "house": 5.0, "aspect": 6.0}
|
||||
|
||||
# względna siła aspektów głównych
|
||||
ASPECT_WEIGHT = {
|
||||
"conjunction": 1.0, "opposition": 0.95, "square": 0.85,
|
||||
"trine": 0.85, "sextile": 0.65,
|
||||
}
|
||||
# aspekt aplikacyjny (A) jest silniejszy niż separacyjny (S) — notes3
|
||||
APPLYING_BONUS = 1.15
|
||||
|
||||
|
||||
def score_facet(facet: dict) -> float:
|
||||
"""Siła fasety w skali ~0–12. Deterministyczna, konfigurowalna."""
|
||||
base = FACET_BASE.get(facet.get("type"), 1.0)
|
||||
if facet.get("type") == "aspect":
|
||||
weight = ASPECT_WEIGHT.get(facet.get("aspect"), 0.7)
|
||||
orb = facet.get("orb")
|
||||
allowed = facet.get("allowed") or 10.0
|
||||
tight = max(0.0, 1.0 - orb / allowed) if orb is not None and allowed else 0.0
|
||||
score = base * weight * (1.0 + tight)
|
||||
if facet.get("applying"):
|
||||
score *= APPLYING_BONUS
|
||||
return round(score, 2)
|
||||
return round(base, 2)
|
||||
@@ -15,6 +15,7 @@ from typing import Any, Protocol
|
||||
|
||||
from app.abbreviations import expand
|
||||
from app.engine.formats import SIGN_ABBR, SIGNS
|
||||
from app.scoring import score_facet
|
||||
|
||||
PLANET_ABBR = {
|
||||
"Sun": "Su", "Moon": "Mo", "Mercury": "Me", "Venus": "Ve", "Mars": "Ma",
|
||||
@@ -87,11 +88,30 @@ def _facet_samples(rows: list[dict], tokens: list[str]) -> list[dict]:
|
||||
return out
|
||||
|
||||
|
||||
def _group_by_effect(samples: list[dict]) -> list[dict]:
|
||||
"""Grupuje próbki po opisie: ten sam efekt = jedna grupa z listą sygnifikatorów."""
|
||||
groups: dict[str, dict] = {}
|
||||
order: list[str] = []
|
||||
for s in samples:
|
||||
key = _norm(s["effect"])
|
||||
g = groups.get(key)
|
||||
if g is None:
|
||||
g = {"effect": s["effect"], "count": 0, "significators": []}
|
||||
groups[key] = g
|
||||
order.append(key)
|
||||
g["count"] += 1
|
||||
g["significators"].append(s["expanded"])
|
||||
result = [groups[k] for k in order]
|
||||
result.sort(key=lambda g: g["count"], reverse=True)
|
||||
return result
|
||||
|
||||
|
||||
def build_report(
|
||||
positions: list[dict],
|
||||
data: DataSource,
|
||||
aspects: list[dict] | None = None,
|
||||
per_object_limit: int = 5000,
|
||||
group: bool = False,
|
||||
) -> dict:
|
||||
"""positions: pozycje z build_chart (name, sign, direction, house).
|
||||
|
||||
@@ -147,12 +167,22 @@ def build_report(
|
||||
asp_samples = _facet_samples(rows, [asp_tok, other_tok])
|
||||
if not asp_samples: # pokazujemy tylko aspekty z trafieniami
|
||||
continue
|
||||
as_suffix = f" ({asp['as']})" if asp.get("as") else ""
|
||||
facets.append({
|
||||
"type": "aspect", "label": f"{ASP_NAME[asp['aspect']]} z {other}",
|
||||
"type": "aspect", "label": f"{ASP_NAME[asp['aspect']]} z {other}{as_suffix}",
|
||||
"token": f"{asp_tok} + {other_tok}",
|
||||
"aspect": asp["aspect"], "orb": asp.get("orb"), "allowed": asp.get("allowed"),
|
||||
"applying": asp.get("applying"),
|
||||
"count": len(asp_samples), "samples": asp_samples,
|
||||
})
|
||||
|
||||
# punktacja siły (LOG-21), opcjonalne grupowanie po opisie, ranking faset
|
||||
for f in facets:
|
||||
f["score"] = score_facet(f)
|
||||
if group:
|
||||
f["groups"] = _group_by_effect(f["samples"])
|
||||
facets.sort(key=lambda f: f["score"], reverse=True)
|
||||
|
||||
items.append({
|
||||
"object": name,
|
||||
"sign": sign,
|
||||
|
||||
@@ -33,3 +33,58 @@ def test_luminary_bonus_widens_orb():
|
||||
def test_one_aspect_per_pair():
|
||||
pos = [{"name": "Sun", "decimal": 0.0}, {"name": "Moon", "decimal": 2.0}]
|
||||
assert len(find_aspects(pos)) == 1
|
||||
|
||||
|
||||
def test_applying_when_faster_body_catches_up():
|
||||
# Księżyc 5° za Słońcem, szybszy -> koniunkcja aplikacyjna
|
||||
pos = [
|
||||
{"name": "Sun", "decimal": 40.0, "speed": 0.96},
|
||||
{"name": "Moon", "decimal": 35.0, "speed": 13.0},
|
||||
]
|
||||
a = find_aspects(pos)[0]
|
||||
assert a["applying"] is True and a["as"] == "A"
|
||||
|
||||
|
||||
def test_separating_when_moving_apart():
|
||||
# Księżyc 5° przed Słońcem i szybszy -> koniunkcja separacyjna
|
||||
pos = [
|
||||
{"name": "Sun", "decimal": 40.0, "speed": 0.96},
|
||||
{"name": "Moon", "decimal": 45.0, "speed": 13.0},
|
||||
]
|
||||
a = find_aspects(pos)[0]
|
||||
assert a["applying"] is False and a["as"] == "S"
|
||||
|
||||
|
||||
def test_no_as_flag_without_speeds():
|
||||
pos = [{"name": "Sun", "decimal": 0.0}, {"name": "Moon", "decimal": 2.0}]
|
||||
assert "as" not in find_aspects(pos)[0]
|
||||
|
||||
|
||||
# Referencja A/S z notes3 (astro-seek) dla horoskopu 30.04.1984 07:35 UT, Warszawa.
|
||||
REFERENCE_AS = {
|
||||
("Sun", "Moon", "conjunction"): "A",
|
||||
("Sun", "Jupiter", "trine"): "A",
|
||||
("Sun", "Saturn", "opposition"): "A",
|
||||
("Sun", "Neptune", "trine"): "S",
|
||||
("Sun", "Pluto", "opposition"): "S",
|
||||
("Moon", "Mercury", "conjunction"): "S",
|
||||
("Moon", "Venus", "conjunction"): "S",
|
||||
("Moon", "Neptune", "trine"): "A",
|
||||
("Moon", "Pluto", "opposition"): "S",
|
||||
("Mercury", "Venus", "conjunction"): "S",
|
||||
("Mercury", "Neptune", "trine"): "S",
|
||||
("Mercury", "Pluto", "opposition"): "S",
|
||||
("Venus", "Neptune", "trine"): "A",
|
||||
("Venus", "Pluto", "opposition"): "A",
|
||||
("Jupiter", "Saturn", "sextile"): "A",
|
||||
("Neptune", "Pluto", "sextile"): "S",
|
||||
}
|
||||
|
||||
|
||||
def test_applying_separating_matches_astroseek_reference(own_engine, reference_moment):
|
||||
from app.engine.chart import build_chart
|
||||
|
||||
chart = build_chart(own_engine, reference_moment)
|
||||
got = {(a["obj1"], a["obj2"], a["aspect"]): a.get("as") for a in chart["aspects"]}
|
||||
mismatches = {k: (got.get(k), v) for k, v in REFERENCE_AS.items() if got.get(k) != v}
|
||||
assert not mismatches, f"rozbieżności A/S vs astro-seek: {mismatches}"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Testy punktacji siły faset (LOG-21)."""
|
||||
from app.scoring import score_facet
|
||||
|
||||
|
||||
def test_sign_and_house_base():
|
||||
assert score_facet({"type": "sign"}) == 5.0
|
||||
assert score_facet({"type": "house"}) == 5.0
|
||||
|
||||
|
||||
def test_tighter_aspect_scores_higher():
|
||||
tight = score_facet({"type": "aspect", "aspect": "conjunction", "orb": 0.0, "allowed": 10.0})
|
||||
loose = score_facet({"type": "aspect", "aspect": "conjunction", "orb": 9.0, "allowed": 10.0})
|
||||
assert tight > loose
|
||||
assert tight == 12.0 # 6.0 * 1.0 * (1 + 1)
|
||||
|
||||
|
||||
def test_conjunction_beats_sextile_at_same_orb():
|
||||
conj = score_facet({"type": "aspect", "aspect": "conjunction", "orb": 2.0, "allowed": 10.0})
|
||||
sext = score_facet({"type": "aspect", "aspect": "sextile", "orb": 2.0, "allowed": 10.0})
|
||||
assert conj > sext
|
||||
@@ -61,6 +61,28 @@ def test_aspect_facet():
|
||||
assert asp and asp[0]["count"] == 1 and "Moon" in asp[0]["label"]
|
||||
|
||||
|
||||
def test_grouping_by_effect():
|
||||
positions = [{"name": "Mars", "sign": "Scorpio", "direction": "Rx", "house": 5}]
|
||||
data = FakeData({"[Ma": [
|
||||
{"significator": "[Ma in 5th H.", "actioneffect": "miscarriage"},
|
||||
{"significator": "[Sa in 5th H.", "actioneffect": "miscarriage"}, # inny sig, ten sam opis
|
||||
{"significator": "[Ma in 5th H.", "actioneffect": "fever"},
|
||||
]})
|
||||
house = [f for f in build_report(positions, data, group=True)["objects"][0]["facets"]
|
||||
if f["type"] == "house"][0]
|
||||
groups = {g["effect"]: g for g in house["groups"]}
|
||||
assert groups["miscarriage"]["count"] == 2
|
||||
assert len(groups["miscarriage"]["significators"]) == 2
|
||||
assert "score" in house
|
||||
|
||||
|
||||
def test_facets_carry_score():
|
||||
positions = [{"name": "Sun", "sign": "Taurus", "direction": "D", "house": 11}]
|
||||
data = FakeData({"[Su": [{"significator": "[Su in [Tau", "actioneffect": "e"}]})
|
||||
facets = build_report(positions, data)["objects"][0]["facets"]
|
||||
assert all("score" in f for f in facets)
|
||||
|
||||
|
||||
def test_ordinal():
|
||||
assert _ordinal(1) == "1st" and _ordinal(2) == "2nd" and _ordinal(3) == "3rd"
|
||||
assert _ordinal(4) == "4th" and _ordinal(11) == "11th" and _ordinal(12) == "12th"
|
||||
|
||||
@@ -44,9 +44,11 @@ class LogicClient:
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def report(self, when_utc_iso: str, lat: float, lon: float, limit: int = 5000) -> dict[str, Any]:
|
||||
def report(
|
||||
self, when_utc_iso: str, lat: float, lon: float, limit: int = 5000, group: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""Sygnifikatory z obliczeń szukane w bazie — woła logic /chart/report."""
|
||||
payload = {"when_utc": when_utc_iso, "lat": lat, "lon": lon, "limit": limit}
|
||||
payload = {"when_utc": when_utc_iso, "lat": lat, "lon": lon, "limit": limit, "group": group}
|
||||
with httpx.Client(timeout=max(settings.http_timeout, 30.0)) as client:
|
||||
r = client.post(f"{self.base_url}/chart/report", json=payload)
|
||||
r.raise_for_status()
|
||||
|
||||
@@ -114,13 +114,15 @@ def interpret_run(
|
||||
tz_offset: float = Form(0.0),
|
||||
lat: float = Form(0.0),
|
||||
lon: float = Form(0.0),
|
||||
group: bool = Form(False),
|
||||
):
|
||||
form = {"date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon}
|
||||
form = {"date": date, "time": time, "tz_offset": tz_offset,
|
||||
"lat": lat, "lon": lon, "group": group}
|
||||
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.report(when_utc_iso=iso_utc, lat=lat, lon=lon)
|
||||
ctx["result"] = logic.report(when_utc_iso=iso_utc, lat=lat, lon=lon, group=group)
|
||||
except httpx.HTTPError as e:
|
||||
ctx["error"] = _logic_error(e)
|
||||
except ValueError as e:
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// „Tu i teraz": uzupełnia datę/godzinę/strefę z przeglądarki oraz — jeśli to
|
||||
// możliwe — lokalizację (lat/lon).
|
||||
//
|
||||
// UWAGA: geolokalizacja przeglądarki działa tylko w "secure context"
|
||||
// (https:// lub localhost). Na http://<ip> przeglądarka NIE pyta o zgodę,
|
||||
// tylko po cichu odmawia — dlatego pokazujemy jawny komunikat w #geoNote.
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const btn = document.getElementById('nowBtn');
|
||||
if (!btn) return;
|
||||
|
||||
const note = document.getElementById('geoNote');
|
||||
const say = msg => { if (note) note.textContent = msg; };
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
const d = new Date();
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
document.querySelector('input[name=date]').value =
|
||||
d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
||||
document.querySelector('input[name=time]').value = pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||||
document.querySelector('input[name=tz_offset]').value = (-d.getTimezoneOffset() / 60);
|
||||
|
||||
if (!('geolocation' in navigator)) {
|
||||
say('Ta przeglądarka nie udostępnia geolokalizacji — wpisz lat/lon ręcznie.');
|
||||
return;
|
||||
}
|
||||
if (!window.isSecureContext) {
|
||||
say('Lokalizacja z przeglądarki wymaga HTTPS lub localhost (otwarto przez http://) — wpisz lat/lon ręcznie.');
|
||||
return;
|
||||
}
|
||||
say('Pobieram lokalizację…');
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function (pos) {
|
||||
document.querySelector('input[name=lat]').value = pos.coords.latitude.toFixed(4);
|
||||
document.querySelector('input[name=lon]').value = pos.coords.longitude.toFixed(4);
|
||||
say('Lokalizacja pobrana ✓');
|
||||
},
|
||||
function (err) {
|
||||
say('Nie udało się pobrać lokalizacji: ' + (err && err.message ? err.message : 'odmowa dostępu'));
|
||||
},
|
||||
{ timeout: 8000 }
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -61,4 +61,6 @@ tr:last-child td { border-bottom: none; }
|
||||
.samples td.nowrap { color: var(--accent); padding-right: 1rem; }
|
||||
.facet { margin: .4rem 0 .6rem 1rem; }
|
||||
.facet-head { color: var(--ink); font-size: .9rem; }
|
||||
.badge { display: inline-block; font-size: .72rem; padding: .05rem .4rem; border-radius: 6px;
|
||||
background: #2a2d4a; color: var(--accent); margin-left: .3rem; }
|
||||
.samples td.sig { color: var(--accent); padding-right: 1rem; cursor: help; }
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<div class="actions">
|
||||
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
||||
<button type="submit">Policz horoskop</button>
|
||||
<span id="geoNote" class="muted small"></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -86,10 +87,10 @@
|
||||
{% if result.aspects %}
|
||||
<div class="meta">Aspekty główne ({{ result.aspects | length }})</div>
|
||||
<table class="angles">
|
||||
<thead><tr><th>Obiekt 1</th><th>Aspekt</th><th>Obiekt 2</th><th>Orb</th></tr></thead>
|
||||
<thead><tr><th>Obiekt 1</th><th>Aspekt</th><th>Obiekt 2</th><th>Orb</th><th title="A = aplikacyjny (dokładność nastąpi), S = separacyjny (już minęła)">A/S</th></tr></thead>
|
||||
<tbody>
|
||||
{% for a in result.aspects %}
|
||||
<tr><td>{{ a.obj1 }}</td><td>{{ a.aspect }}</td><td>{{ a.obj2 }}</td><td class="mono">{{ '%.2f'|format(a.orb) }}°</td></tr>
|
||||
<tr><td>{{ a.obj1 }}</td><td>{{ a.aspect }}</td><td>{{ a.obj2 }}</td><td class="mono">{{ '%.2f'|format(a.orb) }}°</td><td>{{ a['as'] if a['as'] is defined else '—' }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -110,15 +111,5 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
// „Tu i teraz": uzupełnia datę/godzinę bieżącą i offset lokalny przeglądarki.
|
||||
document.getElementById('nowBtn').addEventListener('click', function () {
|
||||
const d = new Date();
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
document.querySelector('input[name=date]').value =
|
||||
d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
||||
document.querySelector('input[name=time]').value = pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||||
document.querySelector('input[name=tz_offset]').value = (-d.getTimezoneOffset() / 60);
|
||||
});
|
||||
</script>
|
||||
<script src="/static/now.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -23,9 +23,13 @@
|
||||
<input type="number" name="lon" step="0.0001" value="{{ form.lon if form.lon is not none else 0 }}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="opts">
|
||||
<label><input type="checkbox" name="group" value="true" {{ 'checked' if form.group else '' }}> grupuj identyczne opisy</label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
||||
<button type="submit">Szukaj interpretacji</button>
|
||||
<span id="geoNote" class="muted small"></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -51,8 +55,20 @@
|
||||
{% for f in o.facets %}
|
||||
<div class="facet">
|
||||
<div class="facet-head">{{ f.label }} — <strong>{{ f.count }}</strong> dopasowań
|
||||
<span class="badge" title="siła (LOG-21)">siła {{ f.score }}</span>
|
||||
<span class="muted small">[{{ f.token }}]</span></div>
|
||||
{% if f.samples %}
|
||||
{% if f.groups %}
|
||||
<table class="samples">
|
||||
<tbody>
|
||||
{% for g in f.groups %}
|
||||
<tr>
|
||||
<td>{{ g.effect }}{% if g.count > 1 %} <span class="badge">×{{ g.count }}</span>{% endif %}</td>
|
||||
<td class="sig small">{{ g.significators | join('; ') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% elif f.samples %}
|
||||
<table class="samples">
|
||||
<tbody>
|
||||
{% for s in f.samples %}
|
||||
@@ -67,14 +83,5 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
document.getElementById('nowBtn').addEventListener('click', function () {
|
||||
const d = new Date();
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
document.querySelector('input[name=date]').value =
|
||||
d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
||||
document.querySelector('input[name=time]').value = pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||||
document.querySelector('input[name=tz_offset]').value = (-d.getTimezoneOffset() / 60);
|
||||
});
|
||||
</script>
|
||||
<script src="/static/now.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user