Skip to content
API Documentation
BastionGuard™
NetworkPage.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™ Secure Connection
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 * GPL-3.0
5 */
6
7#include "NetworkPage.hpp"
9
10#include <algorithm>
11#include <cmath>
12#include <cstdio>
13#include <ctime>
14#include <filesystem>
15#include <fstream>
16#include <glibmm/i18n.h>
17#include <gtk/gtk.h>
18#include <sstream>
19#include <unistd.h>
20
21namespace {
22 constexpr double kDefaultLat = 20.0;
23 constexpr double kDefaultLon = 10.0;
24 constexpr double kDefaultZoom = 2.2;
25 constexpr double kFocusedZoom = 3.0;
26 constexpr double kMaxZoom = 20.0;
27 constexpr double kMinZoom = 1.0;
28 constexpr auto kHoverHideDelay = std::chrono::milliseconds(140);
29 constexpr auto kUserViewportHold = std::chrono::seconds(8);
30
32 gint64 start_us = 0;
33 int hits = 1;
34 };
35
36 struct HoverPayload {
37 NetworkPage* self = nullptr;
38 std::string ip;
39 std::string host;
40 std::string app_name;
41 std::string country_code;
42 std::string country_name;
43 std::string city;
44 std::string isp;
45 uint32_t hits{0};
46 };
47
48 static gboolean animated_marker_tick(GtkWidget* widget,
49 GdkFrameClock*,
50 gpointer)
51 {
52 gtk_widget_queue_draw(widget);
53 return G_SOURCE_CONTINUE;
54 }
55
56 static void animated_marker_data_destroy(gpointer data)
57 {
58 delete static_cast<AnimatedMarkerData*>(data);
59 }
60
61 static void hover_payload_destroy(gpointer data, GClosure*)
62 {
63 delete static_cast<HoverPayload*>(data);
64 }
65
66 static void animated_marker_draw(GtkDrawingArea*,
67 cairo_t* cr,
68 int width,
69 int height,
70 gpointer user_data)
71 {
72 auto* data = static_cast<AnimatedMarkerData*>(user_data);
73 if (!data) return;
74
75 const gint64 now = g_get_monotonic_time();
76 const double t = (now - data->start_us) / 1000000.0;
77
78 const double cx = width * 0.5;
79 const double cy = height * 0.5;
80
81 const double base_r =
82 std::clamp(5.0 + std::log2(static_cast<double>(std::max(1, data->hits)) + 1.0) * 1.8,
83 5.0,
84 11.0);
85
86 const double pulse01 = (std::sin(t * 2.8) + 1.0) * 0.5;
87 const double ring_r = base_r + 4.0 + pulse01 * 10.0;
88 const double ring_alpha = 0.10 + (1.0 - pulse01) * 0.22;
89 const double glow_r = base_r + 7.0 + pulse01 * 4.0;
90 const double glow_alpha = 0.12 + pulse01 * 0.10;
91
92 cairo_save(cr);
93
94 cairo_pattern_t* glow =
95 cairo_pattern_create_radial(cx, cy, base_r * 0.3, cx, cy, glow_r);
96 cairo_pattern_add_color_stop_rgba(glow, 0.0, 1.0, 0.42, 0.28, glow_alpha);
97 cairo_pattern_add_color_stop_rgba(glow, 1.0, 1.0, 0.42, 0.28, 0.0);
98 cairo_set_source(cr, glow);
99 cairo_arc(cr, cx, cy, glow_r, 0.0, 2.0 * M_PI);
100 cairo_fill(cr);
101 cairo_pattern_destroy(glow);
102
103 cairo_set_source_rgba(cr, 1.0, 0.53, 0.30, ring_alpha);
104 cairo_set_line_width(cr, 2.0);
105 cairo_arc(cr, cx, cy, ring_r, 0.0, 2.0 * M_PI);
106 cairo_stroke(cr);
107
108 cairo_set_source_rgba(cr, 1.0, 0.36, 0.26, 0.98);
109 cairo_arc(cr, cx, cy, base_r, 0.0, 2.0 * M_PI);
110 cairo_fill(cr);
111
112 cairo_set_source_rgba(cr, 1.0, 0.95, 0.92, 0.95);
113 cairo_set_line_width(cr, 1.5);
114 cairo_arc(cr, cx, cy, base_r, 0.0, 2.0 * M_PI);
115 cairo_stroke(cr);
116
117 cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.30);
118 cairo_arc(cr,
119 cx - base_r * 0.30,
120 cy - base_r * 0.32,
121 std::max(1.5, base_r * 0.33),
122 0.0,
123 2.0 * M_PI);
124 cairo_fill(cr);
125
126 cairo_restore(cr);
127 }
128
129 static GtkWidget* create_animated_dot_widget(int hits)
130 {
131 auto* area = gtk_drawing_area_new();
132
133 const int size = std::clamp(
134 32 + static_cast<int>(std::log2(static_cast<double>(std::max(1, hits)) + 1.0) * 8.0),
135 32,
136 54
137 );
138
139 auto* data = new AnimatedMarkerData{};
140 data->start_us = g_get_monotonic_time();
141 data->hits = hits;
142
143 gtk_widget_set_size_request(area, size, size);
144 gtk_drawing_area_set_content_width(GTK_DRAWING_AREA(area), size);
145 gtk_drawing_area_set_content_height(GTK_DRAWING_AREA(area), size);
146 gtk_drawing_area_set_draw_func(
147 GTK_DRAWING_AREA(area),
149 data,
151 );
152
153 gtk_widget_add_tick_callback(area, animated_marker_tick, nullptr, nullptr);
154 gtk_widget_set_halign(area, GTK_ALIGN_CENTER);
155 gtk_widget_set_valign(area, GTK_ALIGN_CENTER);
156
157 return area;
158 }
159
160 static void map_fx_draw(GtkDrawingArea*,
161 cairo_t* cr,
162 int width,
163 int height,
164 gpointer user_data)
165 {
166 // user_data contiene il NetworkPage*. Se null, non disegniamo nulla
167 // (overlay trasparente).
168 auto* page = static_cast<NetworkPage*>(user_data);
169 if (!page) return;
170 page->drawConnectionArcs(cr, width, height);
171 }
172} // namespace
173
174// drawConnectionArcs e' definito come metodo di NetworkPage piu' sotto —
175// ha bisogno di accedere ai pin privati e al viewport shumate.
176
178: Gtk::Box(Gtk::Orientation::VERTICAL, 0)
179{
180 get_style_context()->add_class("bsc-page");
181 get_style_context()->add_class("bsc-network-page");
182
183 buildUI();
184
185 m_geoDispatcher.connect([this]() { onGeoResolved(); });
186
188 [this](const std::string& ip, const bsc::ui_util::GeoEntry& e) {
189 {
190 std::lock_guard lk(m_pendingMu);
191 m_pendingGeo.emplace_back(ip, e);
192 }
193 m_geoDispatcher.emit();
194 });
195
196 // Richiedi la posizione del PC in background. Il risultato arrivera'
197 // via onGeoResolved() con ip="__self__".
199
200 // Pulse timer per animazioni (marker self + puntini destinazione +
201 // cleanup pin stale). 66 ms = ~15 Hz: fluido per animazioni lente,
202 // leggero sulla CPU.
203 //
204 // NOTA: niente cleanup periodico di pin "stale" — nella v3 l'utente
205 // vuole vedere TUTTI i pin raccolti durante la sessione (e anche
206 // quelli delle sessioni precedenti, fino a 7 giorni). La vecchia
207 // finestra 30 s era troppo aggressiva.
208 m_pulseTimerId = g_timeout_add(66, +[](gpointer data) -> gboolean {
209 auto* self = static_cast<NetworkPage*>(data);
210 self->m_pulseFrame = (self->m_pulseFrame + 1) % 10000;
211 self->triggerFxRedraw();
212 return G_SOURCE_CONTINUE;
213 }, this);
214}
215
217{
218 if (m_pulseTimerId != 0) {
219 g_source_remove(m_pulseTimerId);
220 m_pulseTimerId = 0;
221 }
222
223 m_markerLayer = nullptr;
224 m_viewport = nullptr;
225 m_simpleMap = nullptr;
226 m_mapFxArea = nullptr;
227 m_mapOverlay = nullptr;
228 m_hoverCard = nullptr;
229 m_hoverCardTitle = nullptr;
230 m_hoverCardBody = nullptr;
231
232 if (m_hoverHideSource != 0) {
233 g_source_remove(m_hoverHideSource);
235 }
236}
237
239{
240 set_margin_start(12);
241 set_margin_end(12);
242 set_margin_top(12);
243 set_margin_bottom(12);
244
245 auto header = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
246 header->get_style_context()->add_class("bsc-page-header");
247
248 m_headerLabel.set_markup("<b>" + Glib::ustring(_("Monitor di rete")) + "</b>");
249 m_headerLabel.set_halign(Gtk::Align::START);
250 m_headerLabel.get_style_context()->add_class("bsc-page-title");
251 header->append(m_headerLabel);
252
253 m_summaryLabel.set_halign(Gtk::Align::START);
254 m_summaryLabel.set_hexpand(true);
255 m_summaryLabel.get_style_context()->add_class("bsc-page-subtitle");
256 m_summaryLabel.set_margin_start(12);
257 header->append(m_summaryLabel);
258
259 m_clearBtn.set_label(_("Pulisci"));
260 m_clearBtn.get_style_context()->add_class("bsc-toolbar-button");
261 m_clearBtn.signal_clicked().connect([this]() { clear(); });
262 header->append(m_clearBtn);
263
264 append(*header);
265
266 m_mapHost.set_hexpand(true);
267 m_mapHost.set_vexpand(false);
268 m_mapHost.set_margin_top(8);
269 m_mapHost.set_margin_bottom(8);
270 m_mapHost.set_size_request(-1, 475);
271 m_mapHost.get_style_context()->add_class("bsc-map");
273 append(m_mapHost);
274
275 m_store = Gtk::ListStore::create(m_columns);
276 m_treeView.set_model(m_store);
277 m_treeView.set_headers_visible(true);
278 m_treeView.set_hexpand(true);
279 m_treeView.set_vexpand(true);
280 m_treeView.get_style_context()->add_class("bsc-treeview");
281 m_treeView.get_style_context()->add_class("bsc-network-treeview");
282
283 m_treeView.append_column(_("Paese"), m_columns.col_country);
284 m_treeView.append_column(_("Città"), m_columns.col_city);
285 m_treeView.append_column(_("IP"), m_columns.col_ip);
286 m_treeView.append_column(_("Host"), m_columns.col_host);
287 m_treeView.append_column(_("App"), m_columns.col_app);
288 m_treeView.append_column(_("ISP/AS"), m_columns.col_isp);
289 m_treeView.append_column(_("Hit"), m_columns.col_hits);
290
291 for (guint i = 0; i < m_treeView.get_n_columns(); ++i) {
292 if (auto* c = m_treeView.get_column(i)) {
293 c->set_resizable(true);
294 c->set_reorderable(false);
295 }
296 }
297
298 m_scroll.set_child(m_treeView);
299 m_scroll.set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC);
300 m_scroll.set_hexpand(true);
301 m_scroll.set_vexpand(true);
302 m_scroll.get_style_context()->add_class("bsc-scrolled");
303 append(m_scroll);
304
306}
307
309{
310 auto* frame = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
311 gtk_widget_add_css_class(frame, "bsc-network-popover");
312 gtk_widget_set_size_request(frame, 320, -1);
313 gtk_widget_set_halign(frame, GTK_ALIGN_START);
314 gtk_widget_set_valign(frame, GTK_ALIGN_START);
315 gtk_widget_set_margin_start(frame, 28);
316 gtk_widget_set_margin_top(frame, 28);
317 gtk_widget_set_visible(frame, FALSE);
318 gtk_widget_set_can_target(frame, FALSE);
319
320 m_hoverCardTitle = gtk_label_new(nullptr);
321 gtk_widget_add_css_class(m_hoverCardTitle, "bsc-network-popover-title");
322 gtk_label_set_xalign(GTK_LABEL(m_hoverCardTitle), 0.0f);
323 gtk_widget_set_can_target(m_hoverCardTitle, FALSE);
324 gtk_box_append(GTK_BOX(frame), m_hoverCardTitle);
325
326 m_hoverCardBody = gtk_label_new(nullptr);
327 gtk_widget_add_css_class(m_hoverCardBody, "bsc-network-popover-meta");
328 gtk_label_set_xalign(GTK_LABEL(m_hoverCardBody), 0.0f);
329 gtk_label_set_wrap(GTK_LABEL(m_hoverCardBody), TRUE);
330 gtk_widget_set_can_target(m_hoverCardBody, FALSE);
331 gtk_box_append(GTK_BOX(frame), m_hoverCardBody);
332
333 return frame;
334}
335
336void NetworkPage::showHoverCard(const std::string& country_code,
337 const std::string& country_name,
338 const std::string& city,
339 const std::string& ip,
340 const std::string& host,
341 const std::string& app_name,
342 const std::string& isp,
343 uint32_t hits)
344{
345 if (!m_hoverCard || !m_hoverCardTitle || !m_hoverCardBody) return;
346
347 if (m_hoverHideSource != 0) {
348 g_source_remove(m_hoverHideSource);
350 }
351
352 std::string title;
353 if (!country_code.empty() && !country_name.empty()) {
354 title = country_code + " " + country_name;
355 } else if (!country_name.empty()) {
356 title = country_name;
357 } else {
358 title = "Connessione";
359 }
360
361 std::ostringstream body;
362 if (!city.empty()) body << city << "\n";
363 body << "IP: " << ip;
364 if (!host.empty()) body << "\nHost: " << host;
365 if (!app_name.empty()) body << "\nApp: " << app_name;
366 if (!isp.empty()) body << "\nISP/AS: " << isp;
367 body << "\nHit: " << hits;
368
369 gtk_label_set_text(GTK_LABEL(m_hoverCardTitle), title.c_str());
370 gtk_label_set_text(GTK_LABEL(m_hoverCardBody), body.str().c_str());
371 gtk_widget_set_visible(m_hoverCard, TRUE);
372}
373
375{
376 if (!m_hoverCard) return;
377
378 if (m_hoverHideSource != 0) {
379 g_source_remove(m_hoverHideSource);
381 }
382
383 m_hoverHideSource = g_timeout_add(
384 static_cast<guint>(kHoverHideDelay.count()),
385 +[](gpointer data) -> gboolean {
386 auto* self = static_cast<NetworkPage*>(data);
387 self->m_hoverHideSource = 0;
388 if (self->m_hoverCard) {
389 gtk_widget_set_visible(self->m_hoverCard, FALSE);
390 }
391 return G_SOURCE_REMOVE;
392 },
393 this
394 );
395}
396
398{
399 m_mapOverlay = gtk_overlay_new();
400
401 m_simpleMap = GTK_WIDGET(shumate_simple_map_new());
402 m_viewport = shumate_simple_map_get_viewport(SHUMATE_SIMPLE_MAP(m_simpleMap));
403 m_markerLayer = shumate_marker_layer_new(m_viewport);
404
405 auto* registry = shumate_map_source_registry_new();
406 if (registry != nullptr) {
407 shumate_map_source_registry_populate_defaults(registry);
408
409 auto* source =
410 SHUMATE_MAP_SOURCE(g_list_model_get_object(G_LIST_MODEL(registry), 0));
411 if (source != nullptr) {
412 shumate_simple_map_set_map_source(SHUMATE_SIMPLE_MAP(m_simpleMap), source);
413 g_object_unref(source);
414 }
415 g_object_unref(registry);
416 }
417
418 shumate_simple_map_set_show_zoom_buttons(SHUMATE_SIMPLE_MAP(m_simpleMap), TRUE);
419 shumate_simple_map_add_overlay_layer(
420 SHUMATE_SIMPLE_MAP(m_simpleMap),
421 SHUMATE_LAYER(m_markerLayer));
422
423 shumate_viewport_set_min_zoom_level(m_viewport, kMinZoom);
424 shumate_viewport_set_max_zoom_level(m_viewport, kMaxZoom);
425 shumate_location_set_location(SHUMATE_LOCATION(m_viewport), kDefaultLat, kDefaultLon);
426 shumate_viewport_set_zoom_level(m_viewport, kDefaultZoom);
427
428 gtk_widget_set_focusable(m_simpleMap, TRUE);
429 gtk_widget_set_hexpand(m_simpleMap, TRUE);
430 gtk_widget_set_vexpand(m_simpleMap, FALSE);
431 gtk_widget_set_size_request(m_simpleMap, 720, 475);
432 gtk_widget_add_css_class(m_simpleMap, "bsc-map-widget");
433
434 gtk_overlay_set_child(GTK_OVERLAY(m_mapOverlay), m_simpleMap);
435
436 // Zoom con rotella/trackpad del mouse. Il controller e' agganciato
437 // all'overlay in fase CAPTURE, non solo al widget Shumate: cosi' funziona
438 // anche quando il cursore e' sopra marker, hover-card o overlay grafico.
439 auto* zoom_scroll = gtk_event_controller_scroll_new(
440 static_cast<GtkEventControllerScrollFlags>(
441 GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE));
442 gtk_event_controller_set_propagation_phase(
443 GTK_EVENT_CONTROLLER(zoom_scroll), GTK_PHASE_CAPTURE);
444 g_signal_connect(
445 zoom_scroll,
446 "scroll",
447 G_CALLBACK(+[](GtkEventControllerScroll*, double dx, double dy, gpointer data) -> gboolean {
448 auto* self = static_cast<NetworkPage*>(data);
449 if (!self->m_viewport) return GDK_EVENT_PROPAGATE;
450 const double delta = (std::abs(dy) >= std::abs(dx)) ? dy : dx;
451 if (delta == 0.0) return GDK_EVENT_PROPAGATE;
452
453 self->noteUserViewportInteraction();
454
455 double zoom = shumate_viewport_get_zoom_level(self->m_viewport);
456 const double step = std::abs(delta) < 1.0 ? 0.25 : 0.45;
457 zoom += (delta < 0.0) ? step : -step;
458 zoom = std::clamp(zoom, kMinZoom, kMaxZoom);
459 shumate_viewport_set_zoom_level(self->m_viewport, zoom);
460 self->triggerFxRedraw();
461 return GDK_EVENT_STOP;
462 }),
463 this
464 );
465 gtk_widget_add_controller(m_mapOverlay, GTK_EVENT_CONTROLLER(zoom_scroll));
466
467 m_mapFxArea = gtk_drawing_area_new();
468 gtk_widget_set_hexpand(m_mapFxArea, TRUE);
469 gtk_widget_set_vexpand(m_mapFxArea, TRUE);
470 gtk_widget_set_can_target(m_mapFxArea, FALSE);
471 // Passiamo `this` come user_data cosi' map_fx_draw puo' accedere ai pin.
472 gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(m_mapFxArea),
473 map_fx_draw, this, nullptr);
474 gtk_overlay_add_overlay(GTK_OVERLAY(m_mapOverlay), m_mapFxArea);
475
477 gtk_overlay_add_overlay(GTK_OVERLAY(m_mapOverlay), m_hoverCard);
478
479 gtk_widget_set_focusable(m_mapOverlay, TRUE);
480 gtk_widget_set_hexpand(m_mapOverlay, TRUE);
481 gtk_widget_set_vexpand(m_mapOverlay, FALSE);
482 gtk_widget_set_size_request(m_mapOverlay, 720, 475);
483
484 gtk_box_append(GTK_BOX(m_mapHost.gobj()), m_mapOverlay);
485}
486
488{
489 auto* marker = shumate_marker_new();
490
491 // ── Ancoraggio del marker ────────────────────────────────────────
492 // Problema visto nelle screenshot utente: il cerchio rosso della
493 // destinazione risultava disegnato sopra la posizione reale (dove
494 // arriva l'arco arancione), creando un gap di ~20-30 px.
495 //
496 // Causa: il vecchio layout usava un Gtk::Box verticale [badge, dot]
497 // ancorato via valign=END sulla coordinata. Ma Shumate centra il
498 // widget sulla coordinata (non allinea il bordo), quindi il BOX
499 // veniva centrato e il cerchio — che era META' in basso del box —
500 // finiva sotto la coordinata, mentre il badge — meta' sopra —
501 // finiva sopra. Risultato: i due elementi stavano AI LATI della
502 // coordinata, con l'arco che puntava al punto medio tra i due.
503 //
504 // Fix: usiamo Gtk::Overlay col cerchio come widget base CENTRATO
505 // (cosi' il suo centro coincide con la coordinata geografica), e
506 // il badge paese come OVERLAY posizionato con margin-bottom positivo
507 // per galleggiare sopra il cerchio senza spostarlo.
508 auto* overlay = gtk_overlay_new();
509 auto* dot = create_animated_dot_widget(pin.hits);
510 gtk_widget_set_halign(dot, GTK_ALIGN_CENTER);
511 gtk_widget_set_valign(dot, GTK_ALIGN_CENTER);
512 gtk_overlay_set_child(GTK_OVERLAY(overlay), dot);
513
514 if (!pin.country_code.empty()) {
515 auto* badge = gtk_label_new(pin.country_code.c_str());
516 gtk_widget_add_css_class(badge, "caption");
517 gtk_widget_add_css_class(badge, "numeric");
518 gtk_widget_add_css_class(badge, "bsc-network-marker-badge");
519 gtk_widget_set_opacity(badge, 0.94);
520
521 // Badge galleggia SOPRA il cerchio, sfalsato a destra cosi' non
522 // lo copre. Con halign=START e valign=START + margin, il badge
523 // si posiziona in alto-sinistra dentro l'overlay, ma sopra il
524 // cerchio (dimensione overlay = dimensione cerchio centrato).
525 // Per uscire dai bounds del cerchio e mostrarsi sopra/a lato,
526 // usiamo margin negativi — GTK lo permette sugli overlay child.
527 gtk_widget_set_halign(badge, GTK_ALIGN_CENTER);
528 gtk_widget_set_valign(badge, GTK_ALIGN_START);
529 gtk_widget_set_margin_top(badge, -18);
530 gtk_overlay_add_overlay(GTK_OVERLAY(overlay), badge);
531 }
532
533 auto* payload = new HoverPayload{};
534 payload->self = this;
535 payload->ip = pin.ip;
536 payload->host = pin.host;
537 payload->app_name = pin.app_name;
538 payload->country_code = pin.country_code;
539 payload->country_name = pin.country_name;
540 payload->city = pin.city;
541 payload->isp = pin.isp;
542 payload->hits = pin.hits;
543
544 auto* motion = gtk_event_controller_motion_new();
545
546 g_signal_connect_data(
547 motion,
548 "enter",
549 G_CALLBACK(+[](GtkEventControllerMotion*, double, double, gpointer data) {
550 auto* payload = static_cast<HoverPayload*>(data);
551 payload->self->showHoverCard(
552 payload->country_code,
553 payload->country_name,
554 payload->city,
555 payload->ip,
556 payload->host,
557 payload->app_name,
558 payload->isp,
559 payload->hits
560 );
561 }),
562 payload,
563 hover_payload_destroy,
564 G_CONNECT_DEFAULT
565 );
566
567 g_signal_connect(
568 motion,
569 "leave",
570 G_CALLBACK(+[](GtkEventControllerMotion*, gpointer data) {
571 static_cast<NetworkPage*>(data)->hideHoverCard();
572 }),
573 this
574 );
575
576 gtk_widget_add_controller(overlay, GTK_EVENT_CONTROLLER(motion));
577
578 shumate_marker_set_child(marker, overlay);
579 // valign CENTER (era END): con CENTER il widget viene centrato verticalmente
580 // sulla coordinata. Combinato col cerchio centrato dentro l'overlay, il
581 // CENTRO del cerchio coincide ora con la coordinata geografica esatta —
582 // e quindi con il punto di arrivo dell'arco arancione.
583 gtk_widget_set_halign(GTK_WIDGET(marker), GTK_ALIGN_CENTER);
584 gtk_widget_set_valign(GTK_WIDGET(marker), GTK_ALIGN_CENTER);
585 shumate_marker_set_selectable(marker, FALSE);
586 shumate_location_set_location(SHUMATE_LOCATION(marker), pin.lat, pin.lon);
587
588 return GTK_WIDGET(marker);
589}
590
591void NetworkPage::maybeRecenterMap(const std::vector<Pin>& pins)
592{
593 if (m_viewport == nullptr) return;
594 if (shouldHoldUserViewport()) return;
595
596 // Raccoglie lat/lon di TUTTI i punti che devono essere visibili: pin
597 // destinazione validi + la posizione del PC locale (se risolta).
598 // Cosi' il fit automatico non nasconde mai il punto blu — era uno
599 // dei bug nelle screenshot precedenti dove il marker blu scompariva
600 // dietro altri marker o fuori viewport.
601 struct LL { double lat, lon; };
602 std::vector<LL> all;
603 all.reserve(pins.size() + 1);
604
605 for (const auto& p : pins) {
606 if (p.geo_resolved && !(p.lat == 0.0 && p.lon == 0.0)) {
607 all.push_back({p.lat, p.lon});
608 }
609 }
610 if (m_selfResolved && !(m_selfLat == 0.0 && m_selfLon == 0.0)) {
611 all.push_back({m_selfLat, m_selfLon});
612 }
613
614 if (all.empty()) {
615 shumate_location_set_location(
616 SHUMATE_LOCATION(m_viewport),
617 kDefaultLat,
618 kDefaultLon
619 );
620 shumate_viewport_set_zoom_level(m_viewport, kDefaultZoom);
621 return;
622 }
623
624 double min_lat = all.front().lat;
625 double max_lat = all.front().lat;
626 double min_lon = all.front().lon;
627 double max_lon = all.front().lon;
628
629 for (const auto& q : all) {
630 min_lat = std::min(min_lat, q.lat);
631 max_lat = std::max(max_lat, q.lat);
632 min_lon = std::min(min_lon, q.lon);
633 max_lon = std::max(max_lon, q.lon);
634 }
635
636 const double center_lat = (min_lat + max_lat) * 0.5;
637 const double center_lon = (min_lon + max_lon) * 0.5;
638 const double span = std::max(max_lat - min_lat, max_lon - min_lon);
639
640 // Auto-zoom "cluster aware": piu' i pin sono concentrati, piu' zoommiamo.
641 // Numeri tarati sullo schermo 720×475 del map widget.
642 //
643 // Prima della v3 (con finestra 30s) bastava uno zoom basso perche'
644 // tipicamente c'erano 2-5 pin sparsi. Ora l'utente accumula
645 // connessioni per giorni: uno zoom piu' ravvicinato evidenzia meglio
646 // le concentrazioni geografiche (la citta' del PC vs un singolo
647 // data center all'estero) senza far sparire il contesto mondiale.
648 double zoom;
649 if (span < 0.5) zoom = 9.0; // stessa citta'
650 else if (span < 2.0) zoom = 7.0; // stessa regione
651 else if (span < 5.0) zoom = 6.0; // paese piccolo
652 else if (span < 12.0) zoom = 5.0; // paese grande
653 else if (span < 25.0) zoom = 4.2; // multi-paese
654 else if (span < 50.0) zoom = 3.6; // continente
655 else if (span < 100.0) zoom = 3.0; // emisfero
656 else zoom = kDefaultZoom; // mondo
657
658 zoom = std::clamp(zoom, kMinZoom, kMaxZoom);
659
660 shumate_location_set_location(
661 SHUMATE_LOCATION(m_viewport),
662 center_lat,
663 center_lon
664 );
665 shumate_viewport_set_zoom_level(m_viewport, zoom);
666}
667
669{
670 if (m_markerLayer == nullptr) return;
671
673
674 std::vector<Pin> pins_copy;
675 {
676 std::lock_guard lk(m_mu);
677 pins_copy.reserve(m_pins.size());
678 for (const auto& [_, p] : m_pins) pins_copy.push_back(p);
679 }
680
681 std::sort(
682 pins_copy.begin(),
683 pins_copy.end(),
684 [](const Pin& a, const Pin& b) { return a.hits < b.hits; }
685 );
686
687 shumate_marker_layer_remove_all(m_markerLayer);
688
689 // Prima tutti i marker destinazione (arancioni)...
690 for (const auto& p : pins_copy) {
691 if (!p.geo_resolved || (p.lat == 0.0 && p.lon == 0.0)) continue;
692
693 auto* marker_widget = createMarkerWidget(p);
694 shumate_marker_layer_add_marker(
696 SHUMATE_MARKER(marker_widget)
697 );
698 }
699
700 // ...poi il marker PC locale per ULTIMO, cosi' sta SOPRA le destinazioni
701 // anche quando coincidono spazialmente (es. connessione a localhost
702 // o a un server nello stesso paese). Il punto blu e' l'ancora visiva
703 // della mappa e non deve mai venire nascosto.
704 if (m_selfResolved) {
705 auto* self_marker = createSelfMarkerWidget();
706 shumate_marker_layer_add_marker(
707 m_markerLayer, SHUMATE_MARKER(self_marker));
708 }
709
710 maybeRecenterMap(pins_copy);
711}
712
714{
715 m_lastUserViewportInteraction = std::chrono::steady_clock::now();
716}
717
719{
720 if (m_lastUserViewportInteraction == std::chrono::steady_clock::time_point{}) return false;
721 return (std::chrono::steady_clock::now() - m_lastUserViewportInteraction) < kUserViewportHold;
722}
723
725{
726 const std::string& ip = info.dest_ip;
727 if (ip.empty()) return;
728
729 bool need_geo = false;
730 {
731 std::lock_guard lk(m_mu);
732 const bool safe_local = bsc::ui_util::is_safe_local_connection(info.dest_ip, info.src_ip);
733 const std::string conn_app_name = bsc::ui_util::best_connection_display_name(
734 info.app_comm, info.app_path, info.app_args, info.dest_ip, info.src_ip);
735
736 auto& p = m_pins[ip];
737 if (p.ip.empty()) {
738 p.ip = ip;
739 p.host = info.dest_host.empty() && safe_local
740 ? std::string{"Connessione locale sicura"}
741 : info.dest_host;
742 p.app_name = conn_app_name;
743 if (safe_local) {
744 p.country_name = "Locale sicuro";
745 p.city = "Loopback";
746 p.isp = "Sistema locale";
747 p.geo_resolved = false;
748 need_geo = false;
749 } else {
750 need_geo = true;
751 }
752 } else {
753 if (!conn_app_name.empty() && p.app_name.find(conn_app_name) == std::string::npos) {
754 p.app_name = p.app_name.empty()
755 ? conn_app_name
756 : conn_app_name + ", " + p.app_name;
757 }
758 if (p.host.empty() && !info.dest_host.empty())
759 p.host = info.dest_host;
760 }
761
762 ++p.hits;
763 p.last_seen = std::chrono::steady_clock::now();
764 p.last_seen_epoch = static_cast<int64_t>(std::time(nullptr));
765 }
766
767 if (need_geo) {
768 if (auto geo = bsc::ui_util::GeoResolver::instance().resolve(ip)) {
769 std::lock_guard lk(m_mu);
770 auto it = m_pins.find(ip);
771 if (it != m_pins.end()) {
772 it->second.country_code = geo->country_code;
773 it->second.country_name = geo->country_name;
774 it->second.city = geo->city;
775 it->second.isp = geo->isp.empty() ? geo->asn : geo->isp;
776 it->second.lat = geo->lat;
777 it->second.lon = geo->lon;
778 it->second.geo_resolved = true;
779 }
780 }
781 }
782
783 {
784 std::lock_guard lk(m_mu);
785 auto it = m_pins.find(ip);
786 if (it != m_pins.end()) {
787 updateRow(it->second);
788 }
789 }
790
793
794 // Salvataggio periodico (ogni 20 connessioni) per resistere a crash.
795 // Il salvataggio e' fast: tipicamente < 5 ms con <100 pin, ma non
796 // vogliamo farlo ad ogni singola connessione.
797 static thread_local int save_counter = 0;
798 if (++save_counter >= 20) {
799 save_counter = 0;
800 saveLog();
801 }
802}
803
805{
806 std::vector<std::pair<std::string, bsc::ui_util::GeoEntry>> pending;
807 {
808 std::lock_guard lk(m_pendingMu);
809 pending.swap(m_pendingGeo);
810 }
811
812 bool self_updated = false;
813 {
814 std::lock_guard lk(m_mu);
815 for (const auto& [ip, geo] : pending) {
816 // Risoluzione speciale: posizione del PC locale.
817 if (ip == "__self__") {
818 m_selfLat = geo.lat;
819 m_selfLon = geo.lon;
820 m_selfResolved = true;
821 self_updated = true;
822 continue;
823 }
824
825 auto it = m_pins.find(ip);
826 if (it == m_pins.end()) continue;
827
828 it->second.country_code = geo.country_code;
829 it->second.country_name = geo.country_name;
830 it->second.city = geo.city;
831 it->second.isp = geo.isp.empty() ? geo.asn : geo.isp;
832 it->second.lat = geo.lat;
833 it->second.lon = geo.lon;
834 it->second.geo_resolved = true;
835
836 updateRow(it->second);
837 }
838 }
839
842
843 // Se la posizione locale e' appena arrivata, forza anche un redraw
844 // degli archi (altrimenti restano invisibili finche' non arriva una
845 // nuova connessione).
846 if (self_updated) triggerFxRedraw();
847}
848
850{
851 Glib::ustring country_cell;
852 if (pin.country_code.empty()) {
853 country_cell = pin.country_name.empty()
854 ? Glib::ustring("?")
855 : Glib::ustring(pin.country_name);
856 } else {
857 country_cell = Glib::ustring(pin.country_code + " " + pin.country_name);
858 }
859
860 auto it = m_rows.find(pin.ip);
861 if (it != m_rows.end()) {
862 auto row = *(it->second);
863 row[m_columns.col_country] = country_cell;
864 row[m_columns.col_city] = pin.city;
865 row[m_columns.col_ip] = pin.ip;
866 row[m_columns.col_host] = pin.host;
867 row[m_columns.col_app] = pin.app_name;
868 row[m_columns.col_isp] = pin.isp;
869 row[m_columns.col_hits] = pin.hits;
870 return;
871 }
872
873 auto iter = m_store->append();
874 auto row = *iter;
875 row[m_columns.col_country] = country_cell;
876 row[m_columns.col_city] = pin.city;
877 row[m_columns.col_ip] = pin.ip;
878 row[m_columns.col_host] = pin.host;
879 row[m_columns.col_app] = pin.app_name;
880 row[m_columns.col_isp] = pin.isp;
881 row[m_columns.col_hits] = pin.hits;
882
883 m_rows.emplace(pin.ip, iter);
884}
885
887{
888 std::size_t total_ips = 0;
889 std::size_t total_hits = 0;
890 std::size_t countries = 0;
891 std::unordered_map<std::string, int> seen_countries;
892
893 {
894 std::lock_guard lk(m_mu);
895 total_ips = m_pins.size();
896
897 for (const auto& [_, p] : m_pins) {
898 total_hits += p.hits;
899 if (!p.country_code.empty() && !seen_countries.count(p.country_code)) {
900 seen_countries[p.country_code] = 1;
901 }
902 }
903
904 countries = seen_countries.size();
905 }
906
907 std::ostringstream oss;
908 oss << total_ips << " IP · " << total_hits << " conn. · "
909 << countries << " paesi";
910 m_summaryLabel.set_text(oss.str());
911}
912
913// ---------------------------------------------------------------------------
914// Nuovo (stile Little Snitch): marker PC locale + archi verso le connessioni
915// ---------------------------------------------------------------------------
916
918{
919 // Tenta subito dalla cache. Se cache miss, il worker di GeoResolver
920 // fara' il fetch e chiamera' on_resolved con ip="__self__"; il nostro
921 // onGeoResolved() lo riconoscera' e popolera' m_selfLat/Lon.
922 if (auto maybe = bsc::ui_util::GeoResolver::instance().resolve_self()) {
923 m_selfLat = maybe->lat;
924 m_selfLon = maybe->lon;
925 m_selfResolved = true;
927 }
928 // Altrimenti: il lookup e' in corso. L'aggiornamento arrivera' via
929 // onGeoResolved() al completamento.
930}
931
933{
934 // Marker del PC locale: cerchio blu con pulse esterno.
935 // Usa shumate_marker come contenitore e un Gtk::DrawingArea custom
936 // per disegnare un cerchio blu con alone che pulsa.
937 //
938 // Questo marker e' l'ANCORA visiva della mappa — deve restare sempre
939 // ben riconoscibile sotto qualunque sovrapposizione. Dimensioni e
940 // colori scelti per massima visibilita' anche a zoom alti dove i
941 // marker destinazione si addensano.
942 auto* marker = shumate_marker_new();
943 auto* area = gtk_drawing_area_new();
944
945 // Dimensione fissa — il "blob" occupa 32×32 px, piu' grande dei marker
946 // destinazione (tipicamente 16-20 px). Il pulse si espande fino a 14 px
947 // di raggio esterno.
948 gtk_widget_set_size_request(area, 32, 32);
949
950 gtk_drawing_area_set_draw_func(
951 GTK_DRAWING_AREA(area),
952 +[](GtkDrawingArea*, cairo_t* cr, int w, int h, gpointer) {
953 const double cx = w * 0.5;
954 const double cy = h * 0.5;
955
956 // Pulse sinusoidale 0..1 con periodo 2 secondi
957 const gint64 us = g_get_monotonic_time();
958 const double t = (us % 2000000) / 2000000.0;
959 const double pulse = 0.5 * (1.0 - std::cos(t * 2.0 * M_PI));
960
961 // Alone pulsante (blu molto trasparente, si espande)
962 const double ring_r = 10.0 + pulse * 6.0;
963 const double ring_a = 0.45 * (1.0 - pulse);
964 cairo_set_source_rgba(cr, 0.20, 0.60, 1.0, ring_a);
965 cairo_arc(cr, cx, cy, ring_r, 0.0, 2.0 * M_PI);
966 cairo_fill(cr);
967
968 // Drop shadow: cerchio nero sfumato sotto, per staccare dal
969 // fondo della mappa (mare/terra mischiati).
970 cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.35);
971 cairo_arc(cr, cx, cy + 1.0, 9.0, 0.0, 2.0 * M_PI);
972 cairo_fill(cr);
973
974 // Anello bianco esterno (bordo di contrasto)
975 cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
976 cairo_arc(cr, cx, cy, 9.0, 0.0, 2.0 * M_PI);
977 cairo_fill(cr);
978
979 // Cerchio pieno blu centrale (il vero marker)
980 cairo_set_source_rgba(cr, 0.15, 0.50, 1.0, 1.0);
981 cairo_arc(cr, cx, cy, 7.0, 0.0, 2.0 * M_PI);
982 cairo_fill(cr);
983
984 // Riflesso bianco in alto a sinistra — tocco 3D discreto
985 cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.55);
986 cairo_arc(cr, cx - 2.0, cy - 2.0, 2.2, 0.0, 2.0 * M_PI);
987 cairo_fill(cr);
988 },
989 nullptr, nullptr);
990
991 shumate_marker_set_child(SHUMATE_MARKER(marker), area);
992 shumate_location_set_location(SHUMATE_LOCATION(marker),
994 return GTK_WIDGET(marker);
995}
996
997void NetworkPage::drawConnectionArcs(cairo_t* cr, int width, int height)
998{
999 if (!m_viewport || !m_selfResolved) return;
1000 if (width <= 0 || height <= 0) return;
1001
1002 // Converte lat/lon del PC in pixel sul widget mappa.
1003 // NOTA: le coordinate sono relative a m_simpleMap (widget sotto);
1004 // l'overlay DrawingArea ha la stessa geometria — stesso sistema.
1005 double sx = 0.0, sy = 0.0;
1006 shumate_viewport_location_to_widget_coords(
1008 m_selfLat, m_selfLon, &sx, &sy);
1009
1010 // Origine fuori dalla mappa visibile: non abbiamo un punto di partenza
1011 // valido, skippiamo tutti gli archi (altrimenti partirebbero dal bordo
1012 // in modo misterioso — come nella screenshot dell'utente: freccia che
1013 // scende "dal nulla"). Stesso criterio dei target.
1014 const bool self_visible =
1015 sx >= 0 && sx <= width && sy >= 0 && sy <= height;
1016
1017 // Snapshot di TUTTI i pin sulla mappa (non solo gli ultimi 30 s).
1018 // L'utente ha chiesto di vedere tutto lo storico — al limite sparisce
1019 // solo oltre i 7 giorni via retention al loadLog().
1020 struct Arc { double tx, ty; bool visible; uint32_t hits; };
1021 std::vector<Arc> arcs;
1022 {
1023 std::lock_guard lk(m_mu);
1024 arcs.reserve(m_pins.size());
1025 for (const auto& [ip, p] : m_pins) {
1026 if (!p.geo_resolved) continue;
1027 // Salta pin con lat/lon degenere (geoip fallita o "Locale")
1028 if (p.lat == 0.0 && p.lon == 0.0) continue;
1029 double tx = 0.0, ty = 0.0;
1030 shumate_viewport_location_to_widget_coords(
1031 m_viewport, m_simpleMap, p.lat, p.lon, &tx, &ty);
1032 // Visibilita' STRETTA: il target deve essere COMPLETAMENTE dentro
1033 // il viewport. Nessun margin: un arco che "esce" dalla mappa
1034 // senza mostrare il puntino di arrivo e' confuso per l'utente.
1035 const bool visible =
1036 tx >= 0 && tx <= width &&
1037 ty >= 0 && ty <= height;
1038 arcs.push_back({tx, ty, visible, p.hits});
1039 }
1040 }
1041
1042 // Pulse per i puntini destinazione (stessa fase per tutti — piu'
1043 // pulito visivamente di un pulse sfasato).
1044 const gint64 us = g_get_monotonic_time();
1045 const double t = (us % 1400000) / 1400000.0; // periodo 1.4s
1046 const double pulse = 0.5 * (1.0 - std::cos(t * 2.0 * M_PI));
1047
1048 cairo_save(cr);
1049
1050 // Disegna ogni arco: linea curva dal PC al target, poi puntino lampeggiante.
1051 // Richiede entrambi i punti visibili, altrimenti l'arco "mozzato" al bordo
1052 // e' peggio di nessun arco.
1053 for (const auto& a : arcs) {
1054 if (!a.visible || !self_visible) continue;
1055
1056 // Se origine e destinazione sono praticamente coincidenti (stessa
1057 // citta' / stesso data center) niente arco — sarebbe un cerchietto
1058 // illeggibile. Il puntino destinazione si vede comunque grazie al
1059 // marker arancione di Shumate.
1060 const double dx_ = a.tx - sx;
1061 const double dy_ = a.ty - sy;
1062 const double dist = std::sqrt(dx_ * dx_ + dy_ * dy_);
1063 if (dist < 8.0) continue;
1064
1065 // Linea dritta con lieve curvatura ascendente (effetto "arco").
1066 // Il control point e' a meta' strada, spostato perpendicolarmente
1067 // al segmento per dare profondita'.
1068 const double mx = (sx + a.tx) * 0.5;
1069 const double my = (sy + a.ty) * 0.5;
1070 // Normale perpendicolare al segmento, normalizzata
1071 const double nx = -dy_ / dist;
1072 const double ny = dx_ / dist;
1073 const double curve = std::min(60.0, dist * 0.18);
1074 const double cx = mx + nx * curve;
1075 const double cy = my + ny * curve;
1076
1077 // Colore linea: arancione morbido con opacita' che cresce con hit count
1078 const double alpha = std::clamp(0.35 + 0.05 * std::log1p(a.hits), 0.35, 0.80);
1079 cairo_set_source_rgba(cr, 1.0, 0.60, 0.22, alpha);
1080 cairo_set_line_width(cr, 1.5);
1081 cairo_move_to(cr, sx, sy);
1082 cairo_curve_to(cr, cx, cy, cx, cy, a.tx, a.ty);
1083 cairo_stroke(cr);
1084
1085 // Puntino destinazione lampeggiante (alone + cerchio pieno)
1086 const double ring_r = 5.0 + pulse * 5.0;
1087 const double ring_a = 0.55 * (1.0 - pulse);
1088 cairo_set_source_rgba(cr, 1.0, 0.60, 0.22, ring_a);
1089 cairo_arc(cr, a.tx, a.ty, ring_r, 0.0, 2.0 * M_PI);
1090 cairo_fill(cr);
1091
1092 cairo_set_source_rgba(cr, 1.0, 0.70, 0.30, 1.0);
1093 cairo_arc(cr, a.tx, a.ty, 3.5, 0.0, 2.0 * M_PI);
1094 cairo_fill(cr);
1095 }
1096
1097 cairo_restore(cr);
1098}
1099
1101{
1102 if (m_mapFxArea) gtk_widget_queue_draw(m_mapFxArea);
1103}
1104
1106{
1107 {
1108 std::lock_guard lk(m_mu);
1109 m_pins.clear();
1110 m_rows.clear();
1111 }
1112
1113 m_store->clear();
1115 updateSummary();
1116
1117 // Cancella anche il log persistente: "Pulisci" e' un reset completo.
1118 std::error_code ec;
1119 std::filesystem::remove(logPath(), ec);
1120}
1121
1122// ---------------------------------------------------------------------------
1123// Persistenza pin (retention 7 giorni)
1124// ---------------------------------------------------------------------------
1125// Formato: JSON Lines. Una riga per pin con tutti i campi necessari a
1126// ricostruire marker e tabella. Il geo_resolved si deriva dalla presenza
1127// di lat/lon non nulle.
1128
1130{
1131 return "/tmp/bastionguard-sc-network-" +
1132 std::to_string(::getuid()) + ".jsonl";
1133}
1134
1135namespace {
1136 // Escape minimo per stringhe JSON (quote, backslash, control chars).
1137 // Duplicato da ConnectionsPage — sarebbe piu' pulito in un util
1138 // comune ma non vale la pena per 20 righe.
1139 std::string net_json_escape(const std::string& s) {
1140 std::string out;
1141 out.reserve(s.size());
1142 for (unsigned char c : s) {
1143 switch (c) {
1144 case '"': out += "\\\""; break;
1145 case '\\': out += "\\\\"; break;
1146 case '\n': out += "\\n"; break;
1147 case '\r': out += "\\r"; break;
1148 case '\t': out += "\\t"; break;
1149 default:
1150 if (c < 0x20) {
1151 char b[8];
1152 std::snprintf(b, sizeof(b), "\\u%04x", c);
1153 out += b;
1154 } else {
1155 out += static_cast<char>(c);
1156 }
1157 }
1158 }
1159 return out;
1160 }
1161
1162 // Parser ultra-semplice per i nostri log: chiave stringa o numero.
1163 // Non gestisce tutti i casi generali (array nested, etc.) — ma i nostri
1164 // record sono flat e controllati da noi, quindi basta.
1165 std::string net_json_field_str(const std::string& line,
1166 const std::string& key) {
1167 const std::string needle = "\"" + key + "\":\"";
1168 auto pos = line.find(needle);
1169 if (pos == std::string::npos) return {};
1170 pos += needle.size();
1171 auto end = line.find('"', pos);
1172 return end == std::string::npos ? std::string{}
1173 : line.substr(pos, end - pos);
1174 }
1175
1176 double net_json_field_double(const std::string& line,
1177 const std::string& key) {
1178 const std::string needle = "\"" + key + "\":";
1179 auto pos = line.find(needle);
1180 if (pos == std::string::npos) return 0.0;
1181 pos += needle.size();
1182 return std::strtod(line.c_str() + pos, nullptr);
1183 }
1184
1185 int64_t net_json_field_i64(const std::string& line,
1186 const std::string& key) {
1187 const std::string needle = "\"" + key + "\":";
1188 auto pos = line.find(needle);
1189 if (pos == std::string::npos) return 0;
1190 pos += needle.size();
1191 return std::strtoll(line.c_str() + pos, nullptr, 10);
1192 }
1193}
1194
1196{
1197 std::ofstream f(logPath(), std::ios::trunc);
1198 if (!f.is_open()) return;
1199
1200 // Snapshot sotto lock per non bloccare la UI durante I/O. m_mu
1201 // protegge m_pins.
1202 std::vector<Pin> snapshot;
1203 {
1204 std::lock_guard lk(m_mu);
1205 snapshot.reserve(m_pins.size());
1206 for (const auto& [ip, p] : m_pins) snapshot.push_back(p);
1207 }
1208
1209 for (const auto& p : snapshot) {
1210 // Skippiamo pin non geo-risolti: ricostruirli al prossimo avvio
1211 // significherebbe rifare le chiamate a ip-api e potrebbe esaurire
1212 // il rate limit. Se proprio serve verranno riaggiunti alla prima
1213 // connessione dopo restart.
1214 if (!p.geo_resolved) continue;
1215
1216 f << "{\"ip\":\"" << net_json_escape(p.ip) << "\""
1217 << ",\"host\":\"" << net_json_escape(p.host) << "\""
1218 << ",\"app\":\"" << net_json_escape(p.app_name) << "\""
1219 << ",\"cc\":\"" << net_json_escape(p.country_code) << "\""
1220 << ",\"country\":\"" << net_json_escape(p.country_name) << "\""
1221 << ",\"city\":\"" << net_json_escape(p.city) << "\""
1222 << ",\"isp\":\"" << net_json_escape(p.isp) << "\""
1223 << ",\"lat\":" << p.lat
1224 << ",\"lon\":" << p.lon
1225 << ",\"hits\":" << p.hits
1226 << ",\"last_ts\":" << p.last_seen_epoch
1227 << "}\n";
1228 }
1229}
1230
1232{
1233 std::ifstream f(logPath());
1234 if (!f.is_open()) return;
1235
1236 const int64_t now_epoch = static_cast<int64_t>(std::time(nullptr));
1237 const int64_t cutoff = now_epoch - kRetentionSeconds;
1238 int loaded = 0, dropped = 0;
1239
1240 std::string line;
1241 while (std::getline(f, line)) {
1242 if (line.empty() || line[0] != '{') continue;
1243
1244 const int64_t ts = net_json_field_i64(line, "last_ts");
1245 // Retention 7 giorni: record piu' vecchi vengono scartati.
1246 if (ts > 0 && ts < cutoff) { ++dropped; continue; }
1247
1248 Pin p;
1249 p.ip = net_json_field_str(line, "ip");
1250 if (p.ip.empty()) continue;
1251 p.host = net_json_field_str(line, "host");
1252 p.app_name = net_json_field_str(line, "app");
1253 p.country_code = net_json_field_str(line, "cc");
1254 p.country_name = net_json_field_str(line, "country");
1255 p.city = net_json_field_str(line, "city");
1256 p.isp = net_json_field_str(line, "isp");
1257 p.lat = net_json_field_double(line, "lat");
1258 p.lon = net_json_field_double(line, "lon");
1259 p.hits = static_cast<uint32_t>(net_json_field_i64(line, "hits"));
1260 p.last_seen_epoch = ts;
1261 // last_seen (steady_clock) non e' ricostruibile esattamente —
1262 // lo impostiamo a "adesso meno delta" cosi' le animazioni che
1263 // usano last_seen non impazziscono. Non piu' usato per culling.
1264 p.last_seen = std::chrono::steady_clock::now();
1265 p.geo_resolved = !(p.lat == 0.0 && p.lon == 0.0);
1266
1267 std::lock_guard lk(m_mu);
1268 m_pins[p.ip] = p;
1269 updateRow(m_pins[p.ip]);
1270 ++loaded;
1271 }
1272
1274 updateSummary();
1275
1276 g_message("NetworkPage: caricati %d pin, scartati %d per retention 7gg",
1277 loaded, dropped);
1278}
#define _(s)
GtkWidget * m_simpleMap
void noteConnection(const ConnectionInfo &info)
Gtk::Label m_headerLabel
bool shouldHoldUserViewport() const
guint m_hoverHideSource
~NetworkPage() override
static constexpr int64_t kRetentionSeconds
Glib::RefPtr< Gtk::ListStore > m_store
std::unordered_map< std::string, Gtk::TreeModel::iterator > m_rows
GtkWidget * m_hoverCardTitle
std::vector< std::pair< std::string, bsc::ui_util::GeoEntry > > m_pendingGeo
Gtk::TreeView m_treeView
GtkWidget * createSelfMarkerWidget()
guint m_pulseTimerId
void drawConnectionArcs(cairo_t *cr, int width, int height)
std::chrono::steady_clock::time_point m_lastUserViewportInteraction
void rebuildMapMarkers()
Gtk::Label m_summaryLabel
Glib::Dispatcher m_geoDispatcher
void updateRow(const Pin &pin)
Gtk::Box m_mapHost
Columns m_columns
void showHoverCard(const std::string &country_code, const std::string &country_name, const std::string &city, const std::string &ip, const std::string &host, const std::string &app_name, const std::string &isp, uint32_t hits)
GtkWidget * createHoverCard()
GtkWidget * m_mapFxArea
void requestSelfLocation()
void onGeoResolved()
void noteUserViewportInteraction()
std::mutex m_mu
GtkWidget * m_hoverCard
void hideHoverCard()
GtkWidget * m_mapOverlay
void buildMapWidget()
void saveLog() const
ShumateViewport * m_viewport
ShumateMarkerLayer * m_markerLayer
GtkWidget * createMarkerWidget(const Pin &pin)
std::mutex m_pendingMu
Gtk::ScrolledWindow m_scroll
Gtk::Button m_clearBtn
void maybeRecenterMap(const std::vector< Pin > &pins)
void triggerFxRedraw()
std::unordered_map< std::string, Pin > m_pins
static std::string logPath()
void updateSummary()
GtkWidget * m_hoverCardBody
static GeoResolver & instance()
void on_resolved(ResolvedCallback cb)
static void animated_marker_data_destroy(gpointer data)
static void map_fx_draw(GtkDrawingArea *, cairo_t *cr, int width, int height, gpointer user_data)
int64_t net_json_field_i64(const std::string &line, const std::string &key)
static gboolean animated_marker_tick(GtkWidget *widget, GdkFrameClock *, gpointer)
std::string net_json_field_str(const std::string &line, const std::string &key)
std::string net_json_escape(const std::string &s)
static void animated_marker_draw(GtkDrawingArea *, cairo_t *cr, int width, int height, gpointer user_data)
static void hover_payload_destroy(gpointer data, GClosure *)
double net_json_field_double(const std::string &line, const std::string &key)
static GtkWidget * create_animated_dot_widget(int hits)
std::string best_connection_display_name(const std::string &app_comm, const std::string &app_path, const std::vector< std::string > &app_args, const std::string &dst_ip, const std::string &src_ip)
bool is_safe_local_connection(const std::string &dst_ip, const std::string &src_ip)
std::string src_ip
Definition UIServer.hpp:35
std::string dest_host
Definition UIServer.hpp:37
std::string app_path
Definition UIServer.hpp:27
std::vector< std::string > app_args
Definition UIServer.hpp:30
std::string dest_ip
Definition UIServer.hpp:38
std::string app_comm
Definition UIServer.hpp:28
Gtk::TreeModelColumn< Glib::ustring > col_country
Gtk::TreeModelColumn< Glib::ustring > col_app
Gtk::TreeModelColumn< uint32_t > col_hits
Gtk::TreeModelColumn< Glib::ustring > col_isp
Gtk::TreeModelColumn< Glib::ustring > col_host
Gtk::TreeModelColumn< Glib::ustring > col_city
Gtk::TreeModelColumn< Glib::ustring > col_ip
int64_t last_seen_epoch
std::string city
std::string country_name
std::chrono::steady_clock::time_point last_seen
std::string isp
std::string app_name
std::string country_code
std::string host