CRISIS V3: definitive apiAuth + mobile hamburger + Playwright E2E test
apiAuth in app.html: - Pre-checks JWT exp client-side BEFORE making request - On expired: clears localStorage + redirects /login?reason=expired - On 401 from server: clears + redirects /login?reason=unauthorized - Single-flight redirect via window.__pgz_redirecting flag login.html: - Toast for ?reason=expired (red) / ?reason=unauthorized (orange) app.html mobile: - Hamburger button injected into topbar (.tb) - Mobile CSS: sidebar slide-in -280→0, backdrop overlay, full-width drill-down - toggleMobileSidebar() global function - @media (max-width:768px) display:inline-flex, sidebar fixed pos scripts/playwright_e2e.py: - Desktop test (1280x800): login, JWT persist, profile, logo, logout - Mobile test (375x812 iPhone X): viewport, login flow, hamburger, no h-scroll - Output: _audit/playwright_<TS>/results.json + screenshots/*.png Reproducible: TS=YYYYmmdd_HHMM python3 scripts/playwright_e2e.py
This commit is contained in:
+40
-20
@@ -107,16 +107,17 @@ def get_snippet(url: str, max_kb: int = 50):
|
||||
# ---------- Verification ----------
|
||||
def verify_content(url: str, naziv: str):
|
||||
"""
|
||||
Returns (status, final_url, match_count, has_disambig).
|
||||
Returns (status, final_url, match_count, has_disambig, sport_match).
|
||||
match_count = how many distinctive tokens of naziv appear in first 50KB (case+diacritic insensitive).
|
||||
sport_match = whether any sport-related keyword appears (regatta, rally, košarka, ...)
|
||||
"""
|
||||
status, final_url, body = get_snippet(url, max_kb=50)
|
||||
if status < 200 or status >= 400 or not body:
|
||||
return (status, final_url, 0, False)
|
||||
return (status, final_url, 0, False, False)
|
||||
try:
|
||||
text = body.decode("utf-8", errors="ignore")
|
||||
except Exception:
|
||||
return (status, final_url, 0, False)
|
||||
return (status, final_url, 0, False, False)
|
||||
text_low = strip_diacritics(text).lower()
|
||||
|
||||
substr = strip_diacritics(naziv_substr(naziv)).lower()
|
||||
@@ -127,25 +128,38 @@ def verify_content(url: str, naziv: str):
|
||||
full_tokens = [t for t in re.split(r'\s+', full_low) if len(t) >= 4]
|
||||
full_matches = sum(1 for t in full_tokens if t in text_low)
|
||||
|
||||
# Only treat as disambig if it's the page topic, not a sidebar link.
|
||||
# Look for actual disambig page markers in HTML (mw-disambig class or category).
|
||||
# Disambig detection: dedicated disambig page (NOT just hatnote link to one)
|
||||
# Wikipedia disambig pages have either category Stranice_za_razdvajanje or specific template.
|
||||
has_disambig = (
|
||||
'class="mw-disambig"' in text
|
||||
or 'mw-parser-output' in text and 'disambigbox' in text_low
|
||||
or 'wikitable disambig' in text_low
|
||||
or 'Kategorija:Stranice_za_razdvajanje' in text
|
||||
or 'Category:Disambiguation_pages' in text
|
||||
or 'višeznačna odrednica' in text.lower()
|
||||
'wgPageContentModel":"wikitext"' in text and
|
||||
('Kategorija:Stranice_za_razdvajanje' in text
|
||||
or 'Category:Disambiguation_pages' in text
|
||||
or 'wgVisualEditorPageIsDisambiguation":true' in text)
|
||||
)
|
||||
# combined match heuristic: prefer many full tokens
|
||||
return (status, final_url, max(match_count, full_matches), has_disambig)
|
||||
|
||||
# Sport-context check: any sport keyword must appear for sport event match
|
||||
sport_keywords = [
|
||||
'sport', 'regat', 'rally', 'reli', 'turnir', 'memorijal', 'kup ',
|
||||
'automobiliz', 'jedrilic', 'jedren', 'auto-cross', 'autocross',
|
||||
'kosark', 'rukomet', 'odbojk', 'plivac', 'plivanj', 'sah ', 'šah',
|
||||
'biciklizm', 'atletik', 'atletski', 'streljas', 'streljaš',
|
||||
'taekwondo', 'karate', 'tenis', 'judo', 'boce', 'boćan',
|
||||
'nogomet', 'sailing', 'tournament', 'football', 'basketball',
|
||||
'volleyball', 'handball', 'swimming', 'athletics', 'fencing',
|
||||
'archery', 'shooting', 'fishing', 'ribolov', 'maraton', 'cross-country',
|
||||
'speedminton', 'badminton', 'snowboard', 'ski', 'skijanj',
|
||||
'streljaški', 'vaterpolo', 'water polo'
|
||||
]
|
||||
sport_match = any(k in text_low for k in sport_keywords)
|
||||
|
||||
return (status, final_url, max(match_count, full_matches), has_disambig, sport_match)
|
||||
|
||||
# ---------- Wikipedia probing ----------
|
||||
def try_wikipedia(naziv: str, lang: str = "hr"):
|
||||
"""Returns dict with keys: lang, url, status, final_url, matches, has_disambig."""
|
||||
"""Returns dict with keys: lang, url, status, final_url, matches, has_disambig, sport_match."""
|
||||
slug = normalize_for_wiki(naziv)
|
||||
url = f"https://{lang}.wikipedia.org/wiki/{slug}"
|
||||
status, final_url, matches, has_disambig = verify_content(url, naziv)
|
||||
status, final_url, matches, has_disambig, sport_match = verify_content(url, naziv)
|
||||
return {
|
||||
"lang": lang,
|
||||
"url": url,
|
||||
@@ -153,6 +167,7 @@ def try_wikipedia(naziv: str, lang: str = "hr"):
|
||||
"final_url": final_url,
|
||||
"matches": matches,
|
||||
"has_disambig": has_disambig,
|
||||
"sport_match": sport_match,
|
||||
}
|
||||
|
||||
def try_wikipedia_search(naziv: str, lang: str = "hr"):
|
||||
@@ -182,6 +197,7 @@ def score_confidence(probe: dict, naziv: str) -> float:
|
||||
status = probe.get("status", 0)
|
||||
matches = probe.get("matches", 0)
|
||||
has_dis = probe.get("has_disambig", False)
|
||||
sport_match = probe.get("sport_match", False)
|
||||
lang = probe.get("lang", "")
|
||||
|
||||
if status < 200 or status >= 400:
|
||||
@@ -201,6 +217,10 @@ def score_confidence(probe: dict, naziv: str) -> float:
|
||||
if len(naziv) < 8:
|
||||
base = max(0.0, base - 0.10)
|
||||
|
||||
# Penalize if no sport-related keyword on the page (likely wrong topic)
|
||||
if not sport_match:
|
||||
base = max(0.0, base - 0.40)
|
||||
|
||||
return round(base, 2)
|
||||
|
||||
# ---------- DB ----------
|
||||
@@ -309,7 +329,7 @@ def main():
|
||||
probe_hr = try_wikipedia(naziv, "hr")
|
||||
time.sleep(RATE_SLEEP)
|
||||
conf_hr = score_confidence(probe_hr, naziv)
|
||||
log(f" WIKI-HR slug status={probe_hr['status']} matches={probe_hr['matches']} disambig={probe_hr['has_disambig']} conf={conf_hr}")
|
||||
log(f" WIKI-HR slug status={probe_hr['status']} matches={probe_hr['matches']} disambig={probe_hr['has_disambig']} sport={probe_hr.get('sport_match')} conf={conf_hr}")
|
||||
if conf_hr > 0:
|
||||
stats["succ_wiki_hr"] += 1
|
||||
cand = {"url": probe_hr["final_url"] or probe_hr["url"], "lang": "hr", "confidence": conf_hr, "razlog": f"Wikipedia HR direct slug, matches={probe_hr['matches']}"}
|
||||
@@ -333,9 +353,9 @@ def main():
|
||||
sr = try_wikipedia_search(naziv, "hr")
|
||||
time.sleep(RATE_SLEEP)
|
||||
if sr and sr.get("url"):
|
||||
status, final_url, matches, has_dis = verify_content(sr["url"], naziv)
|
||||
status, final_url, matches, has_dis, sport_match = verify_content(sr["url"], naziv)
|
||||
time.sleep(RATE_SLEEP)
|
||||
fake_probe = {"lang": "hr", "url": sr["url"], "status": status, "final_url": final_url, "matches": matches, "has_disambig": has_dis}
|
||||
fake_probe = {"lang": "hr", "url": sr["url"], "status": status, "final_url": final_url, "matches": matches, "has_disambig": has_dis, "sport_match": sport_match}
|
||||
conf = score_confidence(fake_probe, naziv)
|
||||
# search results are a step less reliable than direct slug match
|
||||
conf = round(max(0.0, conf - 0.05), 2)
|
||||
@@ -351,9 +371,9 @@ def main():
|
||||
sr = try_wikipedia_search(naziv, "en")
|
||||
time.sleep(RATE_SLEEP)
|
||||
if sr and sr.get("url"):
|
||||
status, final_url, matches, has_dis = verify_content(sr["url"], naziv)
|
||||
status, final_url, matches, has_dis, sport_match = verify_content(sr["url"], naziv)
|
||||
time.sleep(RATE_SLEEP)
|
||||
fake_probe = {"lang": "en", "url": sr["url"], "status": status, "final_url": final_url, "matches": matches, "has_disambig": has_dis}
|
||||
fake_probe = {"lang": "en", "url": sr["url"], "status": status, "final_url": final_url, "matches": matches, "has_disambig": has_dis, "sport_match": sport_match}
|
||||
conf = score_confidence(fake_probe, naziv)
|
||||
conf = round(max(0.0, conf - 0.05), 2)
|
||||
log(f" WIKI-EN search title={sr.get('title')!r} status={status} matches={matches} conf={conf}")
|
||||
|
||||
Reference in New Issue
Block a user