Client polls this endpoint for approval. When approved, it issues a trusted device token cookie and logs the user in (if 2FA is not enabled).
()
| 10838 | "precipitation_sum": at("precipitation_sum", idx, 0), |
| 10839 | "precipitation_probability": at("precipitation_probability_max", idx, 0), |
| 10840 | "wind_speed_max": at("wind_speed_10m_max", idx, 0), |
| 10841 | "wind_gusts_max": at("wind_gusts_10m_max", idx, 0), |
| 10842 | "uv_index_max": at("uv_index_max", idx), |
| 10843 | }) |
| 10844 | return rows |
| 10845 | |
| 10846 | |
| 10847 | @app.route("/weather") |
| 10848 | @login_required |
| 10849 | def weather_page(): |
| 10850 | return render_template("weather.html", title=_("Weather")) |
| 10851 | |
| 10852 | |
| 10853 | @app.route("/api/weather/search") |
| 10854 | @login_required |
| 10855 | def api_weather_search(): |
| 10856 | lang = (request.args.get("lang") or get_lang()).strip().lower() |
| 10857 | lang = "el" if lang == "el" else "en" |
| 10858 | query = (request.args.get("q") or "").strip() |
| 10859 | if len(query) < 2 or len(query) > 100: |
| 10860 | return jsonify({"ok": False, "error": "invalid_search"}), 400 |
| 10861 | if _rate_limit_hit("weather_search", _client_ip() or "?", limit=40, window_sec=60): |
| 10862 | return jsonify({"ok": False, "error": "rate_limited"}), 429 |
| 10863 | key = (lang, query.casefold()) |
| 10864 | now = time.time() |
| 10865 | with _WEATHER_LOCK: |
| 10866 | cached = _WEATHER_SEARCH_CACHE.get(key) |
| 10867 | if cached and now - cached[0] < _WEATHER_SEARCH_TTL: |
| 10868 | return jsonify({"ok": True, "results": cached[1], "cached": True}) |
| 10869 | params = urllib.parse.urlencode({"name": query, "count": 10, "language": lang, "format": "json"}) |
| 10870 | try: |
| 10871 | raw = _weather_json("https://geocoding-api.open-meteo.com/v1/search?" + params) |
| 10872 | results = [] |
| 10873 | for item in (raw.get("results") or [])[:10]: |
| 10874 | try: |
| 10875 | lat, lon = float(item.get("latitude")), float(item.get("longitude")) |
| 10876 | except Exception: |
| 10877 | continue |
| 10878 | name = str(item.get("name") or "").strip() |
| 10879 | parts = [str(item.get(k) or "").strip() for k in ("admin1", "country")] |
| 10880 | parts = [part for part in parts if part and part.casefold() != name.casefold()] |
| 10881 | detail = ", ".join(parts) |
| 10882 | label = ", ".join([part for part in (name, detail) if part]) |
nothing calls this directly
no test coverage detected