auto-sync: 2026-04-26 16:30:01

This commit is contained in:
Stream
2026-04-26 16:30:02 +03:00
parent 56bbba1cc1
commit 6028495007
4 changed files with 132 additions and 1 deletions

View File

@@ -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}

View File

@@ -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"

View File

@@ -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 = `
<div class="flight-card">
<div class="flight-id">Трек #${feature.get('track_id')}</div>
<div class="row"><span class="key">Flight ID</span><span class="val">${feature.get('flight_id') || '—'}</span></div>
<div class="row"><span class="key">Точек</span><span class="val">${feature.get('point_count') || '—'}</span></div>
<div class="row"><span class="key">Высота мин</span><span class="val">${minAlt ? Math.round(minAlt) + ' м' : '—'}</span></div>
<div class="row"><span class="key">Высота макс</span><span class="val">${maxAlt ? Math.round(maxAlt) + ' м' : '—'}</span></div>
</div>
`;
} else {
showFlightDetail(feature, feature.get('noise_color'));
}
}, { layerFilter: l => l === tracksLayer, hitTolerance: 6 });
// ── Клик на слой плотности ──

View File

@@ -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