From 602849500773b93e23d220f47bba646a25eb8d19 Mon Sep 17 00:00:00 2001 From: Stream Date: Sun, 26 Apr 2026 16:30:02 +0300 Subject: [PATCH] auto-sync: 2026-04-26 16:30:01 --- installer/registry.jsonl | 1 + tasks/flightradar24/prototype/app.py | 81 +++++++++++++++++++ tasks/flightradar24/prototype/index.html | 50 +++++++++++- .../flightradar24/prototype/requirements.txt | 1 + 4 files changed, 132 insertions(+), 1 deletion(-) diff --git a/installer/registry.jsonl b/installer/registry.jsonl index 13ef73d..c24b0ad 100644 --- a/installer/registry.jsonl +++ b/installer/registry.jsonl @@ -10,3 +10,4 @@ {"ts":"2026-04-26T10:02:04Z","session":"20260425-100247_fr24_schedule-aircraft-type-coalesce_78a6","host":"fr24","status":"success","agent":"stream"} {"ts":"2026-04-26T10:06:37Z","session":"20260426-100232_fr24_m4-outlier-filter-preprocess_ce04","host":"fr24","status":"success","agent":"stream","files":["/home/fr24/projects/fr24/ingest/preprocess/main.py"]} {"ts":"2026-04-26T11:09:18Z","session":"20260426-110213_mva154_plane-install_d8bb","host":"mva154","status":"success","agent":"stream","files":["/home/slin/plane-selfhost/plane-app/plane.env"]} +{"ts":"2026-04-26T13:26:25Z","action":"cleanup","deleted_orphaned_sessions":0,"deleted_logs":0,"retention_days":30} diff --git a/tasks/flightradar24/prototype/app.py b/tasks/flightradar24/prototype/app.py index b258287..5e597f4 100644 --- a/tasks/flightradar24/prototype/app.py +++ b/tasks/flightradar24/prototype/app.py @@ -19,6 +19,8 @@ from pathlib import Path from datetime import datetime, timezone import orjson +import psycopg2 +import psycopg2.extras from flask import Flask, jsonify, render_template_string, request, send_from_directory, Response from dotenv import load_dotenv @@ -28,6 +30,18 @@ from air_corridors_model import compute_corridors from flask_compress import Compress load_dotenv() + + +def get_db_conn(): + """Подключение к PostgreSQL (fr24 БД)""" + return psycopg2.connect( + host=os.getenv("DB_HOST", "localhost"), + port=int(os.getenv("DB_PORT", 5432)), + dbname=os.getenv("DB_NAME", "fr24"), + user=os.getenv("DB_USER", "fr24"), + password=os.getenv("DB_PASSWORD", "change-me"), + ) + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") logger = logging.getLogger(__name__) @@ -605,6 +619,73 @@ def api_air_corridors(): return Response(raw, status=200, headers={"Content-Type": "application/json"}) +@app.route("/api/tracks", methods=["GET"]) +def get_tracks(): + """Треки из PostgreSQL в формате GeoJSON""" + date_filter = request.args.get("date") # YYYY-MM-DD + limit = min(int(request.args.get("limit", 500)), 2000) + + try: + conn = get_db_conn() + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + + where = "WHERE 1=1" + params = [] + if date_filter: + where += " AND DATE(tp.observed_at) = %s" + params.append(date_filter) + + cur.execute(f""" + SELECT + t.track_id, + t.flight_id, + t.point_count, + t.min_altitude_m, + t.max_altitude_m, + json_agg( + json_build_array(ST_X(tp.geom), ST_Y(tp.geom)) + ORDER BY tp.point_order + ) as coords + FROM fr24.tracks t + JOIN fr24.track_points tp ON tp.track_id = t.track_id + {where} + GROUP BY t.track_id, t.flight_id, t.point_count, t.min_altitude_m, t.max_altitude_m + ORDER BY t.track_id + LIMIT %s + """, params + [limit]) + + rows = cur.fetchall() + cur.close() + conn.close() + + features = [] + for row in rows: + features.append({ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": row["coords"] + }, + "properties": { + "track_id": row["track_id"], + "flight_id": row["flight_id"], + "point_count": row["point_count"], + "min_altitude_m": float(row["min_altitude_m"]) if row["min_altitude_m"] else None, + "max_altitude_m": float(row["max_altitude_m"]) if row["max_altitude_m"] else None, + } + }) + + return jsonify({ + "type": "FeatureCollection", + "features": features, + "meta": {"count": len(features), "date": date_filter} + }) + + except Exception as e: + logger.error(f"/api/tracks error: {e}") + return jsonify({"error": str(e)}), 500 + + if __name__ == "__main__": port = int(os.getenv("PORT", 5555)) debug = os.getenv("DEBUG", "true").lower() == "true" diff --git a/tasks/flightradar24/prototype/index.html b/tasks/flightradar24/prototype/index.html index 263e02a..ba892e1 100644 --- a/tasks/flightradar24/prototype/index.html +++ b/tasks/flightradar24/prototype/index.html @@ -1317,10 +1317,43 @@ function setAirport(airport, btn) { function toggleTracks() { const visible = tracksLayer.getVisible(); + if (!visible && tracksSource.getFeatures().length === 0) { + loadTracksFromDB(); + } tracksLayer.setVisible(!visible); document.getElementById('btn-tracks').classList.toggle('active', !visible); } +async function loadTracksFromDB() { + const dateEl = document.getElementById('date-select'); + const dateParam = dateEl && dateEl.value ? `&date=${dateEl.value}` : ''; + try { + const resp = await fetch(`/noisemap/api/tracks?limit=500${dateParam}`); + const geojson = await resp.json(); + if (geojson.error) { console.error('tracks error:', geojson.error); return; } + tracksSource.clear(); + const format = new ol.format.GeoJSON(); + geojson.features.forEach(f => { + const coords = f.geometry.coordinates.map(c => ol.proj.fromLonLat(c)); + if (coords.length < 2) return; + const avgAlt = ((f.properties.min_altitude_m || 0) + (f.properties.max_altitude_m || 0)) / 2; + const feat = new ol.Feature({ + geometry: new ol.geom.LineString(coords), + track_id: f.properties.track_id, + flight_id: f.properties.flight_id, + point_count: f.properties.point_count, + min_altitude_m: f.properties.min_altitude_m, + max_altitude_m: f.properties.max_altitude_m, + altitude_m: avgAlt, + }); + tracksSource.addFeature(feat); + }); + console.log(`Загружено треков из БД: ${geojson.meta?.count}`); + } catch(e) { + console.error('loadTracksFromDB error:', e); + } +} + // ───────────────────────────────────────────────────────────────── // Статистика // ───────────────────────────────────────────────────────────────── @@ -1598,7 +1631,22 @@ map.on('click', (evt) => { map.forEachFeatureAtPixel(evt.pixel, (feature) => { if (found || !feature.get('flight_id')) return; found = true; - showFlightDetail(feature, feature.get('noise_color')); + // Трек из БД (есть track_id, нет noise_color) + if (feature.get('track_id')) { + const minAlt = feature.get('min_altitude_m'); + const maxAlt = feature.get('max_altitude_m'); + document.getElementById('flight-detail').innerHTML = ` +
+
Трек #${feature.get('track_id')}
+
Flight ID${feature.get('flight_id') || '—'}
+
Точек${feature.get('point_count') || '—'}
+
Высота мин${minAlt ? Math.round(minAlt) + ' м' : '—'}
+
Высота макс${maxAlt ? Math.round(maxAlt) + ' м' : '—'}
+
+ `; + } else { + showFlightDetail(feature, feature.get('noise_color')); + } }, { layerFilter: l => l === tracksLayer, hitTolerance: 6 }); // ── Клик на слой плотности ── diff --git a/tasks/flightradar24/prototype/requirements.txt b/tasks/flightradar24/prototype/requirements.txt index 5414dff..cd210e8 100644 --- a/tasks/flightradar24/prototype/requirements.txt +++ b/tasks/flightradar24/prototype/requirements.txt @@ -2,3 +2,4 @@ flask>=3.0.0 requests>=2.31.0 python-dotenv>=1.0.0 urllib3>=2.0.0 +psycopg2-binary>=2.9.0