PageSourceSearch

https://meteopomarico.it/assets/js/app.js?v=1789375811

js meteopomarico.it collected 2026-09-24 14:23:23 UTC 126,168 bytes, 2,761 lines download raw bytes

1/**
2 * MeteoPomarico - Frontend Application
3 */
4
5(function () {
6    'use strict';
7
8    const API_URL = 'api/weather.php';
9    const FORECAST_URL = 'api/forecast.php';
10    const ALERTS_URL = 'api/alerts.php';
11    const RECORDS_URL = 'api/records.php';
12    const STATS_URL = 'api/stats.php';
13    const MARINE_URL = 'api/marine.php';
14    const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minuti
15    let chart = null;
16    let currentChartType = 'temperature';
17    let currentChartRange = '24h';
18    let useFahrenheit = false;
19
20    // === INIZIALIZZAZIONE ===
21    document.addEventListener('DOMContentLoaded', function () {
22        I18N.init();
23        initLangToggle();
24        initThemeToggle();
25        initUnitToggle();
26        loadWeatherData();
27        loadForecastData();
28        loadAlerts();
29        loadRecords();
30        loadStats();
31        updateMoonPhase();
32        initShareBtn();
33        initMobileMapGestures();
34        loadMarineData();
35        initTempestWebSocket(); // Vento real-time ogni 3 secondi
36        setInterval(loadWeatherData, REFRESH_INTERVAL);
37        setInterval(loadForecastData, 30 * 60 * 1000); // 30 min
38        setInterval(loadMarineData, 30 * 60 * 1000); // 30 min
39
40        // Chart type tabs (temperatura / umidita / pressione)
41        document.querySelectorAll('.chart-tab').forEach(function (tab) {
42            tab.addEventListener('click', function () {
43                document.querySelectorAll('.chart-tab').forEach(function (t) { t.classList.remove('active'); });
44                tab.classList.add('active');
45                currentChartType = tab.dataset.type;
46                if (currentChartRange === '24h') {
47                    loadWeatherData();
48                } else {
49                    loadChartHistory(currentChartRange);
50                }
51            });
52        });
53
54        // Chart range tabs (24h / 7d / 30d)
55        document.querySelectorAll('.chart-range-tab').forEach(function (btn) {
56            btn.addEventListener('click', function () {
57                document.querySelectorAll('.chart-range-tab').forEach(function (b) { b.classList.remove('active'); });
58                btn.classList.add('active');
59                currentChartRange = btn.dataset.range;
60                updateChartTitle(currentChartRange);
61                if (currentChartRange === '24h') {
62                    loadWeatherData();
63                } else {
64                    loadChartHistory(currentChartRange);
65                }
66            });
67        });
68    });
69
70    // === FETCH DATI STAZIONE ===
71    function loadWeatherData() {
72        fetch(API_URL)
73            .then(function (response) {
74                if (!response.ok) throw new Error('HTTP ' + response.status);
75                return response.json();
76            })
77            .then(function (data) {
78                updateUI(data);
79                updateChart(data.history, '24h');
80                renderWindRose(data.history);
81                renderRainChart(data.history);
82                updateLastRefresh();
83                // Aggiorna SOLO la bussola della stazione meteo Pomarico.
84                // (Non tocca la bussola/advice marine: quelli prendono il
85                // vento dall'aggregato delle spiagge in api/marine.php.)
86                if (data.current && data.current.wind) {
87                    updateMarineWindFromStation(data.current.wind);
88                }
89            })
90            .catch(function (err) {
91                console.error('Errore caricamento dati stazione:', err);
92            });
93    }
94
95    // === FETCH PREVISIONI + AQ + UV ===
96    function loadForecastData() {
97        fetch(FORECAST_URL)
98            .then(function (response) {
99                if (!response.ok) throw new Error('HTTP ' + response.status);
100                return response.json();
101            })
102            .then(function (data) {
103                if (data.forecast) {
104                    renderForecast(data.forecast.daily);
105                    renderHourly(data.forecast.hourly, data.forecast.daily);
106                }
107                if (data.air_quality) {
108                    updateAirQuality(data.air_quality);
109                }
110                if (data.pollen) {
111                    updatePollen(data.pollen);
112                }
113                // Percepita: usa solo i dati della stazione Tempest (piu' precisi)
114                // Open-Meteo apparent_temperature non serve piu'
115                // Pioggia: la stazione Tempest ha il pluviometro, nessun fallback necessario
116                if (data.sun) {
117                    updateSunData(data.sun);
118                }
119                if (data.sea) {
120                    updateSeaTemp(data.sea);
121                }
122            })
123            .catch(function (err) {
124                console.error('Errore caricamento previsioni:', err);
125            });
126    }
127
128    // === AGGIORNA UI STAZIONE (tutti i dati Tempest) ===
129    function updateUI(data) {
130        var current = data.current;
131        if (!current) return;
132        var mm = data.daily_minmax || {};
133
134        // --- TEMPERATURA ---
135        setTextIfExists('hero-temp-value', formatTemp(current.outdoor?.temperature));
136        setTextIfExists('hero-temp-min', formatTemp(current.outdoor?.temp_min ?? mm.temp_min));
137        setTextIfExists('hero-temp-max', formatTemp(current.outdoor?.temp_max ?? mm.temp_max));
138
139        // Aggiorna titolo tab con temperatura corrente (rispetta unità C/F)
140        if (current.outdoor?.temperature != null) {
141            document.title = 'MeteoPomarico — ' + formatTemp(current.outdoor.temperature);
142        }
143
144        var trendEl = document.getElementById('hero-temp-trend');
145        if (trendEl && current.outdoor?.temp_trend) {
146            trendEl.className = 'trend-icon ' + getTrendClass(current.outdoor.temp_trend);
147            trendEl.innerHTML = getTrendIcon(current.outdoor.temp_trend);
148            trendEl.title = I18N.t({ up: 'trend_up_short', down: 'trend_down_short', stable: 'trend_stable_short' }[current.outdoor.temp_trend] || 'trend_stable_short');
149        }
150
151        // --- UMIDITA ---
152        setTextIfExists('humidity-value', current.outdoor?.humidity != null ? current.outdoor.humidity + '%' : '--');
153        var humMinMax = document.getElementById('humidity-minmax');
154        if (humMinMax && mm.hum_min != null) {
155            humMinMax.innerHTML =
156                '<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewB
156ox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m18 15-6-6-6 6"/></svg> <span style="opacity:.65;font-size:.7rem;">Max</span> ' + (mm.hum_max ?? '--') + '%' +
157                '&nbsp;&nbsp;&nbsp;' +
158                '<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg> <span style="opacity:.65;font-size:.7rem;">Min</span> ' + (mm.hum_min ?? '--') + '%';
159        }
160
161        // --- PRESSIONE ---
162        setTextIfExists('pressure-value', current.pressure != null ? current.pressure.toFixed(1) : '--');
163        var pressBar = document.getElementById('pressure-bar-fill');
164        if (pressBar && current.pressure) {
165            animateBar(pressBar, Math.max(0, Math.min(100, ((current.pressure - 980) / 60) * 100)));
166        }
167        if (current.pressure_trend) {
168            var pressTrend = document.getElementById('pressure-trend');
169            if (pressTrend) {
170                pressTrend.className = 'trend-icon ' + getTrendClass(current.pressure_trend);
171                pressTrend.innerHTML = getTrendIcon(current.pressure_trend);
172            }
173            var pressTrendLabel = document.getElementById('pressure-trend-label');
174            if (pressTrendLabel) {
175                pressTrendLabel.textContent = I18N.t({ up: 'trend_up', down: 'trend_down', stable: 'trend_stable' }[current.pressure_trend] || 'trend_stable');
176            }
177        }
178
179        // --- MIN/MAX GIORNALIERO ---
180        setTextIfExists('daily-temp-min', formatTemp(mm.temp_min));
181        setTextIfExists('daily-temp-max', formatTemp(mm.temp_max));
182
183        // --- UV (dalla stazione Tempest) ---
184        if (current.uv && current.uv.index != null) {
185            var uvEl = document.getElementById('uv-value');
186            var uvLabel = document.getElementById('uv-label');
187            if (uvEl) {
188                uvEl.textContent = current.uv.index.toFixed(1);
189                uvEl._stationUV = true;
190                var uvInfo = getUVInfo(current.uv.index);
191                uvEl.closest('.card-value').className = 'card-value ' + uvInfo.cls;
192                if (uvLabel) uvLabel.textContent = uvInfo.label;
193            }
194        }
195
196        // --- INDICE COMFORT OUTDOOR ---
197        var heroComfortEl = document.getElementById('hero-comfort');
198        if (heroComfortEl && current.outdoor?.temperature != null) {
199            var cTemp = current.outdoor.temperature;
200            var cHum = current.outdoor?.humidity != null ? current.outdoor.humidity : 50;
201            var cWind = current.wind?.speed != null ? current.wind.speed : 0;
202            var cUV = current.uv?.index != null ? current.uv.index : 0;
203            // Pioggia per comfort: cumulato 60 min (stabile). Fallback al rate istantaneo
204            // (mm/min * 60 = mm/h) se l'orario non è disponibile. Allineato al backend PHP.
205            var cRainRate = current.rain?.hour != null
206                ? current.rain.hour
207                : (current.rain?.current != null ? current.rain.current * 60 : 0);
208
209            // Temperature score: peak at 22, 0-10
210            var tempScore = Math.max(0, 10 - Math.abs(cTemp - 22) * 0.8);
211            // Humidity score: peak at 50, 0-10
212            var humScore = Math.max(0, 10 - Math.abs(cHum - 50) * 0.2);
213            // Wind score: 0=7, 10=10, 30=5, 60=0
214            var windScore;
215            if (cWind <= 10) { windScore = 7 + (cWind / 10) * 3; }
216            else if (cWind <= 30) { windScore = 10 - ((cWind - 10) / 20) * 5; }
217            else { windScore = Math.max(0, 5 - ((cWind - 30) / 30) * 5); }
218
219            // Weighted average
220            var comfortRaw = (tempScore * 0.4) + (humScore * 0.25) + (windScore * 0.35);
221
222            // UV penalty
223            if (cUV > 8) comfortRaw -= 2;
224            // Rain penalty
225            if (cRainRate > 0.05) comfortRaw -= 3;
226
227            // Clamp 1-10
228            var comfortVal = Math.max(1, Math.min(10, comfortRaw));
229
230            // Label and emoji
231            var comfortLabel, comfortEmoji, comfortColor;
232            if (comfortVal >= 8) { comfortLabel = I18N.t('comfort_ideal'); comfortEmoji = '\u2600\uFE0F'; }
233            else if (comfortVal >= 6) { comfortLabel = I18N.t('comfort_pleasant'); comfortEmoji = '\uD83D\uDE0A'; }
234            else if (comfortVal >= 4) { comfortLabel = I18N.t('comfort_fair'); comfortEmoji = '\uD83C\uDF25\uFE0F'; }
235            else if (comfortVal >= 2) { comfortLabel = I18N.t('comfort_unpleasant'); comfortEmoji = '\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F'; }
236            else { comfortLabel = I18N.t('comfort_extreme'); comfortEmoji = '\uD83E\uDD76'; }
237
238            // Color
239            if (comfortVal >= 7) comfortColor = '#22c55e';
240            else if (comfortVal >= 5) comfortColor = '#eab308';
241            else if (comfortVal >= 3) comfortColor = '#f97316';
242            else comfortColor = '#ef4444';
243
244            heroComfortEl.style.display = '';
245            heroComfortEl.innerHTML = '<span>' + comfortEmoji + ' Comfort: <strong style="color:' + comfortColor + ';">' + comfortVal.toFixed(1) + '/10</strong> \u2014 ' + comfortLabel + '</span>';
246        }
247
248        // --- INDICATORE PIOGGIA NELLA HERO ---
249        var heroRainEl = document.getElementById('hero-rain-indicator');
250        if (heroRainEl && current.rain) {
251            var heroIntensity = getRainIntensity(current.rain);
252            if (heroIntensity.rate > 0) {
253                heroRainEl.style.display = '';
254                heroRainEl.innerHTML =
255                    '<span class="hero-rain-dot" style="background:' + heroIntensity.color + ';"></span>' +
256                    '<span style="color:' + heroIntensity.color + ';font-weight:600;">' + heroIntensity.label + '</span>' +
257                    '<span style="opacity:0.7;">(' + heroIntensity.rate.toFixed(1) + ' mm/h)</span>';
258            } else {
259                heroRainEl.style.display = 'none';
260            }
261        }
262
263        // --- PIOGGIA ---
264        var rainSection = document.getElementById('rain-section');
265        if (rainSection && current.rain) {
266            rainSection.innerHTML = buildRainCard(current.rain);
267            rainSection._stationLoaded = true;
268            // Salva dati per il WebSocket (che non ha last_1hr/yesterday)
269            rainSection._lastStationRain = current.rain;
270        }
271
272        // --- PUNTO DI RUGIADA (dalla stazione) ---
273        var temp = current.outdoor?.temperature;
274        var dewPoint = current.outdoor?.dew_point;
275        if (dewPoint != null) {
276            setTextIfExists('dew-point-value', dewPoint.toFixed(1));
277            var dpLabel = document.getElementById('dew-point-label');
278            if (dpLabel) dpLabel.textContent = getDewPointLabel(dewPoint);
279        } else if (temp != null && current.outdoor?.humidity != null) {
280            dewPoint = calcDewPoint(temp, current.outdoor.humidity);
281            setTextIfExists('dew-point-value', dewPoint.toFixed(1));
282            var dpLabel2 = document.getElementById('dew-point-label');
283            if (dpLabel2) dpLabel2.textContent = getDewPointLabel(dewPoint);
284        }
285
286        // --- TEMPERATURA PERCEPITA (dalla stazione) ---
287        var feelsLike = current.outdoor?.feels_like;
288        if (feelsLike != null) {
289            setTextIfExists('feels-like-value', feelsLike.toFixed(1));
290            var flLabel = document.getElementById('feels-like-label');
291            if (flLabel && temp != null) flLabel.textContent = getFeelsLikeLabel(temp, feelsLike);
292        }
293
294        // --- FULMINI (come Tempest: ultimo rilevato, distanza, ultimi 3h) ---
295        if (current.lightning) {
296            var dailyLightning = mm.lightning_total || 0;
297            var lCountEl = document.getElementById('lightning-count');
298            var lDetailEl = document.getElementById('lightning-distance');
299
300            if (current.lightning.last_epoch && current.lightning.last_epoch > 0) {
301                // C'e' stato almeno un fulmine: mostra "ultimo X fa"
302                var lastTime = new Date(current.lightning.last_epoch * 1000);
303                var agoMs = Date.now() - lastTime.getTime();
304                var agoMin = Math.floor(agoMs / 60000);
305
306                // Giorni: conta per calendario (come Tempest), non ore esatte
307                var today = new Date(); today.setHours(0,0,0,0);
308                var lastDay = new Date(lastTime); lastDay.setHours(0,0,0,0);
309                var calendarDays = Math.round((today - lastDay) / 86400000);
310
311                var agoStr = '';
312                if (agoMin < 1) agoStr = I18N.t('lightning_just_now');
313                else if (agoMin < 60) agoStr = agoMin + ' min ' + I18N.t('lightning_ago');
314                else if (calendarDays === 0) {
315                    var hrs = Math.floor(agoMin / 60);
316                    agoStr = hrs + ' ' + (hrs === 1 ? I18N.t('lightning_hour_ago') : I18N.t('lightning_hours_ago'));
317                } else {
318                    agoStr = calendarDays + ' ' + (calendarDays === 1 ? I18N.t('lightning_day_ago') : I18N.t('lightning_days_ago'));
319                }
320
321                if (lCountEl) lCountEl.textContent = agoStr;
322
323                // Dettaglio: distanza + ultimi 3h + oggi
324                var details = [];
325                if (current.lightning.distance) details.push(I18N.t('lightning_distance') + ': ' + current.lightning.distance.toFixed(0) + ' km');
326                details.push(I18N.t('lightning_last_3hr') + ': ' + (current.lightning.count_3hr || 0));
327                if (dailyLightning > 0) details.push(I18N.t('rain_today_label') + ': ' + dailyLightning);
328                if (lDetailEl) lDetailEl.textContent = details.join(' | ');
329            } else {
330                // Nessun fulmine mai rilevato
331                if (lCountEl) lCountEl.textContent = '0';
332                if (lDetailEl) lDetailEl.textContent = I18N.t('lightning_none');
333            }
334        }
335
336        // --- RADIAZIONE SOLARE + ILLUMINAMENTO ---
337        if (current.uv) {
338            setTextIfExists('solar-rad-value', current.uv.solar_radiation != null ? current.uv.solar_radiation : '--');
339            var illumEl = document.getElementById('illuminance-value');
340            if (illumEl && current.illuminance != null) {
341                illumEl.textContent = I18N.t('illuminance_label') + ': ' + current.illuminance.toLocaleStr
341ing() + ' lux';
342            }
343        }
344
345        // --- DATI AVANZATI ---
346        setTextIfExists('adv-station-pressure', current.station_pressure != null ? current.station_pressure.toFixed(1) : '--');
347        setTextIfExists('adv-wet-bulb', current.outdoor?.wet_bulb != null ? current.outdoor.wet_bulb.toFixed(1) : '--');
348        setTextIfExists('adv-delta-t', current.delta_t != null ? current.delta_t.toFixed(1) : '--');
349        setTextIfExists('adv-air-density', current.air_density != null ? current.air_density.toFixed(4) : '--');
350        setTextIfExists('adv-wind-chill', current.outdoor?.wind_chill != null ? current.outdoor.wind_chill.toFixed(1) : '--');
351
352        // Delta T label (per irrorazione agricola)
353        var dtLabel = document.getElementById('adv-delta-t-label');
354        if (dtLabel && current.delta_t != null) {
355            var dt = current.delta_t;
356            if (dt < 2) dtLabel.textContent = I18N.t('delta_t_too_low');
357            else if (dt < 5) dtLabel.textContent = I18N.t('delta_t_marginal');
358            else if (dt <= 8) dtLabel.textContent = I18N.t('delta_t_ideal');
359            else if (dt <= 10) dtLabel.textContent = I18N.t('delta_t_marginal');
360            else dtLabel.textContent = I18N.t('delta_t_too_high');
361        }
362
363        // Batteria
364        if (current.battery != null) {
365            setTextIfExists('adv-battery', current.battery.toFixed(2));
366            var batStatus = document.getElementById('adv-battery-status');
367            if (batStatus) {
368                var v = current.battery;
369                if (v >= 2.455) { batStatus.textContent = I18N.t('battery_good'); batStatus.style.color = '#22c55e'; }
370                else if (v >= 2.39) { batStatus.textContent = I18N.t('battery_ok'); batStatus.style.color = '#eab308'; }
371                else { batStatus.textContent = I18N.t('battery_low'); batStatus.style.color = '#ef4444'; }
372            }
373        }
374
375        // WBGT (Wet Bulb Globe Temperature)
376        // Approssimazione: WBGT ≈ 0.7*Twb + 0.2*Tg + 0.1*Ta
377        // Dove Tg (globe) ≈ Ta + 0.005*Rs (radiazione solare)
378        var wbgt = null;
379        var wb = current.outdoor?.wet_bulb;
380        var ta = current.outdoor?.temperature;
381        var rs = current.uv ? current.uv.solar_radiation : 0;
382        if (wb != null && ta != null) {
383            var tg = ta + 0.005 * (rs || 0); // Globe temperature approssimata
384            wbgt = 0.7 * wb + 0.2 * tg + 0.1 * ta;
385        }
386        if (wbgt != null) {
387            setTextIfExists('adv-wbgt', wbgt.toFixed(1));
388            var wbgtStatus = document.getElementById('adv-wbgt-status');
389            if (wbgtStatus) {
390                if (wbgt < 25)      { wbgtStatus.textContent = I18N.t('wbgt_low'); wbgtStatus.style.color = '#22c55e'; }
391                else if (wbgt < 28) { wbgtStatus.textContent = I18N.t('wbgt_moderate'); wbgtStatus.style.color = '#eab308'; }
392                else if (wbgt < 30) { wbgtStatus.textContent = I18N.t('wbgt_high'); wbgtStatus.style.color = '#f97316'; }
393                else if (wbgt < 32) { wbgtStatus.textContent = I18N.t('wbgt_very_high'); wbgtStatus.style.color = '#ef4444'; }
394                else                { wbgtStatus.textContent = I18N.t('wbgt_extreme'); wbgtStatus.style.color = '#dc2626'; }
395            }
396        }
397
398        // Evapotraspirazione ET0 (Penman-Monteith semplificata)
399        // ET0 = (0.408 * Rn + gamma * (900/(T+273)) * u2 * (es - ea)) / (delta + gamma * (1 + 0.34*u2))
400        // Semplificazione Hargreaves: ET0 ≈ 0.0023 * (Tmean + 17.8) * (Tmax - Tmin)^0.5 * Ra
401        // Ma con i dati Tempest possiamo usare una approssimazione migliore
402        var et0El = document.getElementById('adv-et0');
403        if (et0El && current.outdoor?.temperature != null && current.uv && current.uv.solar_radiation != null && current.wind) {
404            var T = current.outdoor.temperature;
405            var RH = current.outdoor?.humidity || 50;
406            var Rs = current.uv.solar_radiation; // W/m2
407            var u2 = (current.wind.speed || 0) / 3.6; // km/h to m/s
408
409            // Penman-Monteith FAO-56 semplificata (stima oraria scalata a giornaliera)
410            var es = 0.6108 * Math.exp((17.27 * T) / (T + 237.3)); // kPa
411            var ea = es * RH / 100;
412            var delta = (4098 * es) / Math.pow(T + 237.3, 2);
413            var gamma = 0.0665; // kPa/°C (a ~950 hPa)
414            var Rn = Rs * 0.0036 * 0.75; // conversione W/m2 a MJ/m2/h approssimata * albedo
415            var RnDay = Rn * 12; // approssimazione 12 ore di luce
416
417            var et0 = (0.408 * RnDay * delta + gamma * (900 / (T + 273)) * u2 * (es 
417- ea)) /
418                      (delta + gamma * (1 + 0.34 * u2));
419            et0 = Math.max(0, et0);
420
421            et0El.textContent = et0.toFixed(1);
422            var et0Note = document.getElementById('adv-et0-note');
423            if (et0Note) {
424                if (et0 < 2)      et0Note.textContent = I18N.t('et0_low');
425                else if (et0 < 4) et0Note.textContent = I18N.t('et0_moderate');
426                else if (et0 < 6) et0Note.textContent = I18N.t('et0_high');
427                else              et0Note.textContent = I18N.t('et0_very_high');
428            }
429        }
430
431        // Durata pioggia
432        if (current.rain && current.rain.minutes_today != null && current.rain.minutes_today > 0) {
433            var rh = Math.floor(current.rain.minutes_today / 60);
434            var rm = current.rain.minutes_today % 60;
435            setTextIfExists('adv-rain-duration', (rh > 0 ? rh + 'h ' : '') + rm + 'min');
436        }
437
438        // --- TIMESTAMP ---
439        if (current.timestamp) {
440            var d = new Date(current.timestamp.replace(' ', 'T'));
441            setTextIfExists('data-timestamp', formatDateTime(d));
442        }
443    }
444
445    // === PREVISIONI 7 GIORNI ===
446    function renderForecast(days) {
447        var container = document.getElementById('forecast-container');
448        if (!container || !days || days.length === 0) return;
449
450        var dayKeys = ['day_sun', 'day_mon', 'day_tue', 'day_wed', 'day_thu', 'day_fri', 'day_sat'];
451        var today = new Date().toISOString().split('T')[0];
452        var html = '';
453
454        days.forEach(function (day) {
455            var d = new Date(day.date + 'T00:00:00');
456            var isToday = day.date === today;
457            var dayName = isToday ? I18N.t('today') : I18N.t(dayKeys[d.getDay()]);
458
459            html += '<div class="forecast-day' + (isToday ? ' today' : '') + '">' +
460                '<div class="forecast-day-name">' + dayName + '</div>' +
461                '<div class="forecast-icon">' + getWeatherIcon(day.weather_code) + '</div>' +
462                '<div class="forecast-temps">' +
463                    '<span class="max">' + convertTemp(day.temp_max) + '</span>' +
464                    '<span class="min">' + convertTemp(day.temp_min) + '</span>' +
465                '</div>' +
466                (day.precip_prob > 0 ? '<div class="forecast-precip">' + day.precip_prob + '% ' + I18N.t('precip_rain') + '</div>' : '') +
467                '<div class="forecast-wind">' + Math.round(day.wind_max) + ' km/h</div>' +
468                '</div>';
469        });
470
471        container.innerHTML = html;
472    }
473
474    // === PREVISIONI ORARIE ===
475    // Riceve daily (con sunrise/sunset per ogni giorno) per determinare
476    // correttamente giorno/notte nelle icone orarie.
477    function renderHourly(hours, daily) {
478        var container = document.getElementById('hourly-container');
479        if (!container || !hours || hours.length === 0) return;
480
481        var dayNames = I18N.currentLang === 'it'
482            ? ['DOM', 'LUN', 'MAR', 'MER', 'GIO', 'VEN', 'SAB']
483            : ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
484
485        var sunByDate = {};
486        if (daily && Array.isArray(daily)) {
487            daily.forEach(function (d) {
488                if (d.date) {
489                    sunByDate[d.date] = { sunrise: d.sunrise, sunset: d.sunset };
490                }
491            });
492        }
493
494        var html = '';
495        var lastDay = -1;
496        hours.forEach(function (h) {
497            var d = new Date(h.time);
498            var hour = String(d.getHours()).padStart(2, '0') + ':00';
499            var dayOfMonth = d.getDate();
500            var showDayLabel = dayOfMonth !== lastDay;
501            lastDay = dayOfMonth;
502
503            var dateKey = d.getFullYear() + '-'
504                        + String(d.getMonth() + 1).padStart(2, '0') + '-'
505                        + String(d.getDate()).padStart(2, '0');
506            var sun = sunByDate[dateKey] || null;
507
508            var dayName = dayNames[d.getDay()];
509            var night = isHourNight(d, sun);
510            html += '<div class="hourly-item' + (showDayLabel ? ' new-day' : '') + '">';
511            if (showDayLabel) {
512                html += '<div class="hourly-day">' + dayName + '</div>';
513            }
514            html += '<div class="hourly-time">' + hour + '</div>' +
515                '<div class="hourly-icon">' + getWeatherIcon(h.weather_code, night) + '</div>' +
516                '<div class="hourly-temp">' + convertTemp(h.temp) + '</div>' +
517                '</div>';
518        });
519
520        container.innerHTML = html;
521    }
522
523    // === QUALITA ARIA ===
524    function updateAirQuality(aq) {
525        // AQI principale
526        var aqiEl = document.getElementById('aqi-value');
527        var labelEl = document.getElementById('aqi-label');
528        if (aqiEl && aq.aqi !== null) {
529            aqiEl.textContent = aq.aqi;
530            var aqiInfo = getAQIInfo(aq.aqi);
531            aqiEl.className = 'aqi-score ' + aqiInfo.cls;
532            if (labelEl) {
533                labelEl.textContent = aqiInfo.label;
534                labelEl.className = 'aqi-label ' + aqiInfo.cls;
535            }
536            // Posiziona indicatore sulla scala (0-100+)
537            var indicator = document.getElementById('aqi-indicator');
538            if (indicator) {
539                var pct = Math.min(100, Math.max(0, aq.aqi));
540                animateBar(indicator, pct, 'left');
541            }
542        }
543
544        // Inquinanti singoli
545        var pollutants = [
546            { id: 'pm25', val: aq.pm25, max: 25 },
547            { id: 'pm10', val: aq.pm10, max: 50 },
548            { id: 'no2',  val: aq.no2,  max: 200 },
549            { id: 'o3',   val: aq.o3,   max: 180 },
550            { id: 'so2',  val: aq.so2,  max: 350 },
551            { id: 'co',   val: aq.co,   max: 10000 },
552        ];
553
554        pollutants.forEach(function (p) {
555            var valEl = document.getElementById('pol-' + p.id);
556            var barEl = document.getElementById('bar-' + p.id);
557            if (valEl) {
558                if (p.val !== null && p.val !== undefined) {
559                    valEl.textContent = p.id === 'co' ? Math.round(p.val) : p.val.toFixed(1);
560                    var ratio = Math.min(1, p.val / p.max);
561                    if (barEl) {
562                        barEl.style.backgroundColor = getPollutantColor(ratio);
563                        animateBar(barEl, Math.max(2, ratio * 100));
564                    }
565                } else {
566                    valEl.textContent = 'N/D';
567                }
568            }
569        });
570
571        // UV (fallback da forecast se la stazione non ha UV)
572        var uvEl = document.getElementById('uv-value');
573        var uvLabel = document.getElementById('uv-label');
574        if (uvEl && aq.uv_index !== null && !uvEl._stationUV) {
575            uvEl.textContent = aq.uv_index.toFixed(1);
576            var uvInfo = getUVInfo(aq.uv_index);
577            uvEl.closest('.card-value').className = 'card-value ' + uvInfo.cls;
578            if (uvLabel) uvLabel.textContent = uvInfo.label;
579        }
580    }
581
582    function getPollutantColor(ratio) {
583        if (ratio < 0.25) return '#22c55e';
584        if (ratio < 0.5)  return '#eab308';
585        if (ratio < 0.75) return '#f97316';
586        return '#ef4444';
587    }
588
589    // === POLLINI (Google Pollen API - UPI 0-5) ===
590    function updatePollen(pollen) {
591        if (!pollen) return;
592        var types = ['tree', 'grass', 'weed'];
593        types.forEach(function (type) {
594            var valEl = document.getElementById('pollen-' + type);
595            var levelEl = document.getElementById('pollen-' + type + '-level');
596            var gaugeEl = document.getElementById('pollen-' + type + '-gauge');
597            var data = pollen[type];
598            if (!data) {
599                if (valEl) valEl.textContent = '--';
600                return;
601            }
602            var index = data.index || 0;
603            var info = getPollenInfo(index);
604            if (valEl) {
605                valEl.textContent = index + '/5';
606                valEl.className = 'pollen-value ' + info.cls;
607            }
608            if (levelEl) {
609                levelEl.textContent = info.label;
610                levelEl.className = 'pollen-level ' + info.cls;
611            }
612            if (gaugeEl) {
613                var pct = (index / 5) * 100;
614                gaugeEl.style.width = pct + '%';
615                gaugeEl.style.backgroundColor = info.color;
616            }
617        });
618    }
619
620    function getPollenInfo(index) {
621        // UPI: 0=None, 1=Very Low, 2=Low, 3=Moderate, 4=High, 5=Very High
622        if (index <= 0) return { label: I18N.t('pollen_none'),      cls: 'pollen-none',      color: '#6b7280' };
623        if (index <= 1) return { label: I18N.t('pollen_very_low'),   cls: 'pollen-low',       color: '#22c55e' };
624        if (index <= 2) return { label: I18N.t('pollen_low'),        cls: 'pollen-low',       color: '#84cc16' };
625        if (index <= 3) return { label: I18N.t('pollen_moderate'),   cls: 'pollen-moderate',  color: '#eab308' };
626        if (index <= 4) return { label: I18N.t('pollen_high'),       cls: 'pollen-high',      color: '#f97316' };
627        return              { label: I18N.t('pollen_very_high'),     cls: 'pollen-very-high', color: '#ef4444' };
628    }
629
630    // === ALBA / TRAMONTO ===
631    function updateSunData(sun) {
632        var sr, ss;
633        if (sun.sunrise) {
634            sr = new Date(sun.sunrise);
635            setTextIfExists('sunrise-time', sr.getHours() + ':' + String(sr.getMinutes()).padStart(2, '0'));
636        }
637        if (sun.sunset) {
638            ss = new Date(sun.sunset);
639            setTextIfExists('sunset-time', ss.getHours() + ':' + String(ss.getMinutes()).padStart(2, '0'));
640        }
641        if (sun.daylight_hours) {
642            var h = Math.floor(sun.daylight_hours);
643            var m = Math.round((sun.daylight_hours - h) * 60);
644            setTextIfExists('daylight-hours', h + 'h ' + m + 'min ' + I18N.t('daylight_suffix'));
645        }
646
647        // Indicatore lineare posizione sole
648        if (sr && ss) {
649            var now = Date.now();
650            var sunriseMs = sr.getTime();
651            var sunsetMs = ss.getTime();
652            var totalDay = sunsetMs - sunriseMs;
653            var elapsed = now - sunriseMs;
654            var pct = Math.max(0, Math.min(100, (elapsed / totalDay) * 100));
655
656            var fill = document.getElementById('sun-progress-fill');
657            var dot = document.getElementById('sun-progress-dot');
658
659            if (fill) fill.style.width = pct + '%';
660            if (dot) {
661                dot.style.left = pct + '%';
662                // Di notte: pallino grigio
663                if (now < sunriseMs || now > sunsetMs) {
664                    dot.classList.add('night');
665                } else {
666                    dot.classList.remove('night');
667                }
668            }
669        }
670    }
671
672    // === GRAFICO 24H ===
673    function updateChart(history, range) {
674        if (!history || history.length === 0) return;
675        if (range === undefined) range = currentChartRange;
676
677        var canvas = document.getElementById('weather-chart');
678        if (!canvas) return;
679
680        var ctx = canvas.getContext('2d');
681        var labels = [];
682        var dataset1 = [];
683
684        var config = getChartConfig(currentChartType);
685
686        history.forEach(function (point) {
687            labels.push(formatChartLabel(point.time, range));
688            dataset1.push(point[config.field1]);
689        });
690
691        var datasets = [{
692            label: config.label1,
693            data: dataset1,
694            borderColor: config.color1,
695            backgroundColor: config.color1 + '15',
696            borderWidth: 2,
697            fill: true,
698            tension: 0.4,
699            pointRadius: 0,
700            pointHitRadius: 10,
701        }];
702
703        if (chart) {
704            chart.data.labels = labels;
705            chart.data.datasets = datasets;
706            chart.options.scales.y.title.text = config.yLabel;
707            chart.update('none');
708        } else {
709            var tc = getChartThemeColors();
710            chart = new Chart(ctx, {
711                type: 'line',
712                data: { labels: labels, datasets: datasets },
713                options: {
714                    responsive: true,
715                    maintainAspectRatio: false,
716                    interaction: { mode: 'index', intersect: false },
717                    plugins: {
718                        legend: { display: false },
719                        tooltip: {
720                            backgroundColor: tc.tooltipBg,
721                            titleColor: tc.tooltipTitle,
722                            bodyColor: tc.tooltipBody,
723                            borderColor: tc.tooltipBorder,
724                            borderWidth: 1,
725                            padding: 12,
726                            cornerRadius: 10,
727                        }
728                    },
729                    scales: {
730                        x: {
731                            grid: { color: tc.gridColor, drawBorder: false },
732                            ticks: { color: tc.tickColor, maxTicksLimit: 12, font: { size: 11 } }
733                        },
734                        y: {
735                            grid: { color: tc.gridColor, drawBorder: false },
736                            ticks: { color: tc.tickColor, font: { size: 11 } },
737                            title: { display: true, text: config.yLabel, color: tc.tickColor }
738                        }
739                    }
740                }
741            });
742        }
743    }
744
745    function getChartConfig(type) {
746        var unit = useFahrenheit ? '\u00b0F' : '\u00b0C';
747        var configs = {
748            temperature: {
749                field1: 'temp_out', label1: I18N.t('chart_temp_label'),
750                color1: '#3b82f6', yLabel: I18N.t('chart_temp_label') + ' (' + unit + ')'
751            },
752            humidity: {
753                field1: 'humidity_out', label1: I18N.t('chart_hum_label'),
754                color1: '#06b6d4', yLabel: I18N.t('chart_hum_label') + ' (%)'
755            },
756            pressure: {
757                field1: 'pressure', label1: I18N.t('chart_press_label'),
758                color1: '#a855f7', yLabel: I18N.t('chart_press_label') + ' (hPa)'
759            }
760        };
761        return configs[type] || configs.temperature;
762    }
763
764    // === WEATHER CODE -> METEOCONS SVG (stesso stile app) ===
765    function getWeatherIconFile(code, isNight) {
766        if (code === null || code === undefined) return 'overcast-day';
767        switch (code) {
768            case 0: return isNight ? 'clear-night' : 'clear-day';
769            case 1: return isNight ? 'clear-night' : 'clear-day';
770            case 2: return isNight ? 'partly-cloudy-night' : 'partly-cloudy-day';
771            case 3: return isNight ? 'overcast-night' : 'overcast-day';
772            case 45: case 48: return isNight ? 'fog-night' : 'fog-day';
773            case 51: case 53: case 55: return 'drizzle';
774            case 56: case 57: case 66: case 67: return 'sleet';
775            case 61: case 63: case 80: case 81: return 'rain';
776            case 65: case 82: return 'extreme-rain';
777            case 71: case 73: case 85: return 'snow';
778            case 75: case 77: case 86: return 'extreme-snow';
779            case 95: return isNight ? 'thunderstorms-night' : 'thunderstorms-day';
780            case 96: case 99: return 'thunderstorms-extreme-rain';
781            default: return isNight ? 'overcast-night' : 'overcast-day';
782        }
783    }
784    function getWeatherIcon(code, isNight) {
785        var name = getWeatherIconFile(code, isNight);
786        return '<img class="wx-icon" src="assets/icons/weather/' + name + '.svg" alt="" loading="lazy">';
787    }
788
789    // === WEATHER CODE -> EMOJI (fallback testuale) ===
790    function getWeatherEmoji(code, isNight) {
791        if (code === null || code === undefined) return '\u2753';
792        var dayMap = {
793            // code 1 "prev sereno" -> stessa icona di "sereno" (allineato con app)
794            0: '\u2600\ufe0f', 1: '\u2600\ufe0f', 2: '\u26c5', 3: '\u2601\ufe0f',
795            45: '\ud83c\udf2b\ufe0f', 48: '\ud83c\udf2b\ufe0f',
796            51: '\ud83c\udf26\ufe0f', 53: '\ud83c\udf26\ufe0f', 55: '\ud83c\udf26\ufe0f',
797            56: '\ud83c\udf28\ufe0f', 57: '\ud83c\udf28\ufe0f',
798            61: '\ud83c\udf27\ufe0f', 63: '\ud83c\udf27\ufe0f', 65: '\ud83c\udf27\ufe0f',
799            66: '\ud83c\udf28\ufe0f', 67: '\ud83c\udf28\ufe0f',
800            71: '\ud83c\udf28\ufe0f', 73: '\u2744\ufe0f', 75: '\u2744\ufe0f', 77: '\u2744\ufe0f',
801            80: '\ud83c\udf26\ufe0f', 81: '\ud83c\udf27\ufe0f', 82: '\ud83c\udf27\ufe0f',
802            85: '\ud83c\udf28\ufe0f', 86: '\ud83c\udf28\ufe0f',
803            95: '\u26c8\ufe0f', 96: '\u26c8\ufe0f', 99: '\u26c8\ufe0f',
804        };
805        if (isNight) {
806            if (code === 0) return '\ud83c\udf19';              // 🌙
807            if (code === 1) return '\ud83c\udf19';              // 🌙
808            if (code === 2) return '\u2601\ufe0f\ud83c\udf19';  // ☁️🌙
809        }
810        return dayMap[code] || '\u2601\ufe0f';
811    }
812
813    function isHourNight(hourDate, sunData) {
814        var hr = hourDate.getHours();
815        if (sunData && sunData.sunrise && sunData.sunset) {
816            var sr = parseHHMM(sunData.sunrise);
817            var ss = parseHHMM(sunData.sunset);
818            var min = hourDate.getHours() * 60 + hourDate.getMinutes();
819            if (sr !== null && ss !== null) {
820                return min < sr || min >= ss;
821            }
822        }
823        return hr < 6 || hr >= 20;
824    }
825    function parseHHMM(s) {
826        if (!s || typeof s !== 'string') return null;
827        var m = s.match(/(\d{1,2}):(\d{2})/);
828        if (!m) return null;
829        return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
830    }
831
832    // === TEMPERATURA MARE ===
833    function updateSeaTemp(sea) {
834        var el = document.getElementById('sea-temp-value');
835        if (el && sea.temperature !== null && sea.temperature !== undefined) {
836            el.textContent = parseFloat(sea.temperature).toFixed(1);
837        }
838    }
839
840    // === FASE LUNARE (canvas raytraced identico a leccemeteo) ===
841    var LUNAR_CYCLE = 29.530588853;
842
843    function updateMoonPhase() {
844        var now = new Date();
845        var age = getMoonAge(now);
846        var illumination = getMoonIllumination(age);
847        var label = getMoonLabel(age);
848
849        var labelEl = document.getElementById('moon-label');
850        var illumEl = document.getElementById('moon-illumination');
851        var ageEl = document.getElementById('moon-age');
852
853        if (labelEl) labelEl.textContent = label;
854        if (illumEl) illumEl.textContent = illumination.toFixed(1) + '%';
855        if (ageEl) ageEl.textContent = age.toFixed(1) + ' ' + I18N.t('moon_days');
856
857        renderMoonCanvas(age);
858    }
859
860    function getMoonAge(date) {
861        var refNewMoon = new Date(Date.UTC(2000, 0, 6, 18, 14, 0));
862        var diff = (date - refNewMoon) / (1000 * 60 * 60 * 24);
863        return ((diff % LUNAR_CYCLE) + LUNAR_CYCLE) % LUNAR_CYCLE;
864    }
865
866    function getMoonIllumination(age) {
867        return (1 - Math.cos(2 * Math.PI * age / LUNAR_CYCLE)) / 2 * 100;
868    }
869
870    function getMoonLabel(age) {
871        if (age < 1.85)  return I18N.t('moon_new');
872        if (age < 7.38)  return I18N.t('moon_waxing_crescent');
873        if (age < 9.22)  return I18N.t('moon_first_quarter');
874        if (age < 14.77) return I18N.t('moon_waxing_gibbous');
875        if (age < 16.61) return I18N.t('moon_full');
876        if (age < 22.15) return I18N.t('moon_waning_gibbous');
877        if (age < 23.99) return I18N.t('moon_last_quarter');
878        return I18N.t('moon_waning_crescent');
879    }
880
881    function renderMoonCanvas(age) {
882        var canvas = document.getElementById('moon-canvas');
883        if (!canvas) return;
884        var ctx = canvas.getContext('2d');
885        var W = canvas.width, H = canvas.height;
886        var cx = W / 2, cy = H / 2, R = W / 2 - 4;
887
888        var isLight = document.documentElement.getAttribute('data-theme') === 'light';
889
890        var phaseAngle = 2 * Math.PI * age / LUNAR_CYCLE;
891        var lightX = Math.sin(phaseAngle);
892        var lightZ = -Math.cos(phaseAngle);
893
894        function hash(x, y) {
895            var h = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
896            return h - Math.floor(h);
897        }
898        function noise(x, y) {
899            var ix = Math.floor(x), iy = Math.floor(y);
900            var fx = x - ix, fy = y - iy;
901            var a = hash(ix, iy), b = hash(ix + 1, iy);
902            var c = hash(ix, iy + 1), d = hash(ix + 1, iy + 1);
903            var ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy);
904            return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy;
905        }
906        function fbm(x, y) {
907            var v = 0, amp = 0.5, freq = 1;
908            for (var i = 0; i < 5; i++) {
909                v += amp * noise(x * freq, y * freq);
910                amp *= 0.5; freq *= 2;
911            }
912            return v;
913        }
914
915        var imgData = ctx.createImageData(W, H);
916        var data = imgData.data;
917
918        for (var py = 0; py < H; py++) {
919            for (var px = 0; px < W; px++) {
920                var dx = (px - cx) / R, dy = (py - cy) / R;
921                var dist2 = dx * dx + dy * dy;
922                var idx = (py * W + px) * 4;
923
924                if (dist2 > 1) {
925                    data[idx] = data[idx + 1] = data[idx + 2] = 0;
926                    data[idx + 3] = 0;
927                    continue;
928                }
929
930                var dz = Math.sqrt(1 - dist2);
931                var light = dx * lightX + dz * lightZ;
932                light = Math.max(0, light);
933
934                var u = Math.atan2(dx, dz) / Math.PI * 4 + 2;
935                var v = Math.asin(Math.max(-1, Math.min(1, dy))) / Math.PI * 4 + 2;
936
937                var surface = fbm(u * 3, v * 3);
938                var baseColor = isLight ? 180 + surface * 50 : 155 + surface * 70;
939                var maria = fbm(u * 1.5 + 10, v * 1.5 + 10);
940                if (maria > 0.55) baseColor *= (isLight ? 0.8 : 0.7);
941                var craters = fbm(u * 8, v * 8);
942                if (craters > 0.7) baseColor *= 0.85;
943
944                var sunLight = Math.max(0, light);
945                var ambient = isLight ? 0.38 : 0.25;
946                var limb = 1 - Math.pow(1 - dz, 2) * 0.2;
947                var term = sunLight > 0.01 ? Math.pow(sunLight, 0.5) : 0;
948                var brightness = (term * 0.65 + ambient) * limb;
949
950                var r  = baseColor * brightness;
951                var g  = (baseColor - 2) * brightness;
952                var bl = baseColor * brightness * (sunLight < 0.15 ? 1.1 : 1);
953
954                data[idx]     = Math.min(255, Math.max(0, r));
955                data[idx + 1] = Math.min(255, Math.max(0, g));
956                data[idx + 2] = Math.min(255, Math.max(0, bl));
957                data[idx + 3] = 255;
958            }
959        }
960
961        ctx.putImageData(imgData, 0, 0);
962
963        ctx.save();
964        ctx.strokeStyle = isLight ? 'rgba(80,80,100,0.3)' : 'rgba(180,190,210,0.15)';
965        ctx.lineWidth = isLight ? 1.5 : 1;
966        ctx.beginPath();
967        ctx.arc(cx, cy, R, 0, Math.PI * 2);
968        ctx.stroke();
969        ctx.restore();
970    }
971
972    // === RECORD STORICI ===
973    function loadRecords() {
974        fetch(RECORDS_URL)
975            .then(function (r) { return r.json(); })
976            .then(function (data) { renderRecords(data); })
977            .catch(function (err) { console.error('Errore record:', err); });
978    }
979
980    function renderRecords(data) {
981        function setRecord(idVal, idDate, record) {
982            var elVal = document.getElementById(idVal);
983            var elDate = document.getElementById(idDate);
984            if (!record) return;
985            if (elVal) elVal.textContent = record.value;
986            if (elDate && record.date) {
987                var d = new Date(record.date.replace(' ', 'T'));
988                elDate.textContent = formatDateTime(d);
989            }
990        }
991        if (data.temperature) {
992            setRecord('rec-temp-max', 'rec-temp-max-date', data.temperature.max);
993            setRecord('rec-temp-min', 'rec-temp-min-date', data.temperature.min);
994        }
995        if (data.humidity) {
996            setRecord('rec-hum-max', 'rec-hum-max-date', data.humidity.max);
997            setRecord('rec-hum-min', 'rec-hum-min-date', data.humidity.min);
998        }
999        if (data.pressure) {
1000            setRecord('rec-press-max', 'rec-press-max-date', data.pressure.max);
1001            setRecord('rec-press-min', 'rec-press-min-date', data.pressure.min);
1002        }
1003        if (data.wind_gust && data.wind_gust.max) {
1004            setRecord('rec-gust-max', 'rec-gust-max-date', data.wind_gust.max);
1005        }
1006        if (data.uv_index && data.uv_index.max) {
1007            setRecord('rec-uv-max', 'rec-uv-max-date', data.uv_index.max);
1008        }
1009        var meta = document.getElementById('records-meta');
1010        if (meta && data.total_measurements && data.first_record) {
1011            var since = new Date(data.first_record.replace(' ', 'T'));
1012            meta.textContent = I18N.t('records_since') + ' ' + formatDateTime(since)
1013                + ' \u2014 ' + data.total_measurements.toLocaleStr
1013ing() + ' ' + I18N.t('records_measurements');
1014        }
1015    }
1016
1017    // === STATISTICHE (oggi l'anno scorso + estremi mensili) ===
1018    function loadStats() {
1019        fetch(STATS_URL)
1020            .then(function (r) { return r.json(); })
1021            .then(function (data) {
1022                if (data.last_year) renderLastYear(data.last_year);
1023                if (data.monthly_extremes && data.monthly_extremes.length > 0) renderMonthlyExtremes(data.monthly_extremes);
1024            })
1025            .catch(function (err) { console.error('Errore stats:', err); });
1026    }
1027
1028    function renderLastYear(ly) {
1029        var section = document.getElementById('last-year-section');
1030        var container = document.getElementById('last-year-container');
1031        if (!section || !container) return;
1032
1033        section.style.display = '';
1034        container.style.display = '';
1035
1036        setTextIfExists('ly-avg-temp', ly.avg_temp.toFixed(1));
1037        setTextIfExists('ly-avg-hum', ly.avg_hum);
1038        setTextIfExists('ly-avg-press', ly.avg_press.toFixed(1));
1039
1040        var rangeEl = document.getElementById('ly-temp-range');
1041        if (rangeEl) {
1042            rangeEl.textContent = 'Min ' + ly.min_temp + '\u00b0 / Max ' + ly.max_temp + '\u00b0';
1043        }
1044
1045        var dateEl = document.getElementById('last-year-date');
1046        if (dateEl) {
1047            var d = new Date(ly.date + 'T00:00:00');
1048            dateEl.textContent = I18N.t('ly_date_label') + ' ' + formatDateTime(d).split(' ').slice(0, 2).join(' ');
1049        }
1050    }
1051
1052    function renderMonthlyExtremes(months) {
1053        var section = document.getElementById('monthly-section');
1054        var container = document.getElementById('monthly-container');
1055        var tbody = document.getElementById('monthly-tbody');
1056        if (!section || !container || !tbody) return;
1057
1058        section.style.display = '';
1059        container.style.display = '';
1060
1061        var html = '';
1062        months.forEach(function (m) {
1063            var monthName = I18N.t('month_' + m.month);
1064            var label = m.year ? monthName + ' ' + m.year : monthName;
1065            html += '<tr>' +
1066                '<td>' + label + '</td>' +
1067                '<td class="temp-max">' + m.max_temp + '</td>' +
1068                '<td class="temp-min">' + m.min_temp + '</td>' +
1069                '<td>' + m.avg_temp + '</td>' +
1070                '<td>' + m.avg_hum + '</td>' +
1071                '</tr>';
1072        });
1073        tbody.innerHTML = html;
1074    }
1075
1076    // === PUNTO DI RUGIADA ===
1077    // Formula di Magnus
1078    function calcDewPoint(T, RH) {
1079        var a = 17.625, b = 243.04;
1080        var alpha = (a * T) / (b + T) + Math.log(RH / 100);
1081        return (b * alpha) / (a - alpha);
1082    }
1083
1084    function getDewPointLabel(dp) {
1085        if (dp < 0)  return I18N.t('dp_dry');
1086        if (dp < 10) return I18N.t('dp_comfortable');
1087        if (dp < 16) return I18N.t('dp_pleasant');
1088        if (dp < 21) return I18N.t('dp_humid');
1089        if (dp < 25) return I18N.t('dp_very_humid');
1090        return I18N.t('dp_oppressive');
1091    }
1092
1093    // === TEMPERATURA PERCEPITA ===
1094    // Usa apparent_temperature da Open-Meteo (tiene conto di vento, umidita, radiazione)
1095    var _omApparentTemp = null;
1096
1097    function updateFeelsLikeFromOM(currentOM) {
1098        _omApparentTemp = currentOM.apparent_temperature;
1099        // Aggiorna subito se i dati della stazione sono gia caricati
1100        var heroTemp = document.getElementById('hero-temp-value');
1101        if (heroTemp && heroTemp.textContent !== '--') {
1102            var temp = parseFloat(heroTemp.textContent.replace(',', '.'));
1103            if (!isNaN(temp)) {
1104                var fl = _omApparentTemp;
1105                setTextIfExists('feels-like-value', fl.toFixed(1));
1106                var flLabel = document.getElementById('feels-like-label');
1107                if (flLabel) flLabel.textContent = getFeelsLikeLabel(temp, fl);
1108            }
1109        }
1110    }
1111
1112    function calcFeelsLike(T, RH) {
1113        // Se abbiamo la percepita da Open-Meteo, usala (piu precisa)
1114        if (_omApparentTemp !== null) return _omApparentTemp;
1115        // Fallback: Heat Index per temperature alte
1116        if (T >= 27 && RH >= 40) {
1117            return -8.78469475556
1118                + 1.61139411   * T
1119                + 2.33854883889 * RH
1120                - 0.14611605   * T * RH
1121                - 0.012308094  * T * T
1122                - 0.0164248277778 * RH * RH
1123                + 0.002211732  * T * T * RH
1124                + 0.00072546   * T * RH * RH
1125                - 0.000003582  * T * T * RH * RH;
1126        }
1127        return T;
1128    }
1129
1130    function getFeelsLikeLabel(T, feelsLike) {
1131        var diff = feelsLike - T;
1132        if (Math.abs(diff) < 0.5) return I18N.t('fl_same');
1133        if (diff > 0) return I18N.t('fl_warmer');
1134        return I18N.t('fl_cooler');
1135    }
1136
1137    // === AQI INFO ===
1138    function getAQIInfo(aqi) {
1139        // European AQI: 0-20 Good, 21-40 Fair, 41-60 Moderate, 61-80 Poor, 81-100 Very Poor
1140        if (aqi <= 20) return { label: I18N.t('aqi_good'),      cls: 'aqi-good' };
1141        if (aqi <= 40) return { label: I18N.t('aqi_fair'),      cls: 'aqi-fair' };
1142        if (aqi <= 60) return { label: I18N.t('aqi_moderate'),  cls: 'aqi-moderate' };
1143        if (aqi <= 80) return { label: I18N.t('aqi_poor'),      cls: 'aqi-poor' };
1144        return      { label: I18N.t('aqi_very_poor'),           cls: 'aqi-very-poor' };
1145    }
1146
1147    // === UV INFO ===
1148    function getUVInfo(uv) {
1149        if (uv < 3) return { label: I18N.t('uv_low'), cls: 'uv-low' };
1150        if (uv < 6) return { label: I18N.t('uv_moderate'), cls: 'uv-moderate' };
1151        if (uv < 8) return { label: I18N.t('uv_high'), cls: 'uv-high' };
1152        if (uv < 11) return { label: I18N.t('uv_very_high'), cls: 'uv-very-high' };
1153        return { label: I18N.t('uv_extreme'), cls: 'uv-extreme' };
1154    }
1155
1156    // === HELPERS ===
1157    function setTextIfExists(id, value) {
1158        var el = document.getElementById(id);
1159        if (el) el.textContent = value ?? '--';
1160    }
1161
1162    function animateBar(el, targetPct, prop) {
1163        prop = prop || 'width';
1164        if (!el.dataset.animated) {
1165            el.style[prop] = '0%';
1166            el.dataset.animated = '1';
1167            el.offsetWidth; // Forza reflow
1168            el.style.transition = prop + ' 1s cubic-bezier(0.4, 0, 0.2, 1)';
1169        }
1170        el.style[prop] = targetPct + '%';
1171    }
1172
1173    function convertTemp(val) {
1174        if (val === null || val === undefined) return '--';
1175        var v = parseFloat(val);
1176        if (useFahrenheit) v = v * 9 / 5 + 32;
1177        return Math.round(v) + '\u00b0';
1178    }
1179
1180    function formatTemp(val) {
1181        if (val === null || val === undefined) return '--';
1182        var v = parseFloat(val);
1183        if (useFahrenheit) {
1184            v = v * 9 / 5 + 32;
1185            return v.toFixed(1) + '\u00b0F';
1186        }
1187        return v.toFixed(1) + '\u00b0C';
1188    }
1189
1190    function formatDateTime(d) {
1191        var dayFullKeys = ['day_sun_full', 'day_mon_full', 'day_tue_full', 'day_wed_full', 'day_thu_full', 'day_fri_full', 'day_sat_full'];
1192        var day = String(d.getDate()).padStart(2, '0');
1193        var month = String(d.getMonth() + 1).padStart(2, '0');
1194        var year = String(d.getFullYear()).slice(-2);
1195        return I18N.t(dayFullKeys[d.getDay()]) + ' ' +
1196            day + '/' + month + '/' + year + ' ' +
1197            d.getHours() + ':' + String(d.getMinutes()).padStart(2, '0');
1198    }
1199
1200    function getTrendClass(trend) {
1201        if (trend === 'up') return 'trend-up';
1202        if (trend === 'down') return 'trend-down';
1203        return 'trend-stable';
1204    }
1205
1206    function getTrendIcon(trend) {
1207        if (trend === 'up') return '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m7 17 9.2-9.2M17 17V7H7"/></svg>';
1208        if (trend === 'down') return '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7l9.2 9.2M17 7v10H7"/></svg>';
1209        return '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></svg>';
1210    }
1211
1212    function getWindDirection(angle) {
1213        var dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSO', 'SO', 'OSO', 'O', 'ONO', 'NO', 'NNO'];
1214        return dirs[Math.round(angle / 22.5) % 16];
1215    }
1216
1217    function buildWindCard(wind) {
1218        var html = '<div class="card-label">' + I18N.t('wind') + '</div>' +
1219            '<div class="card-value">' + wind.speed.toFixed(1) + ' <span class="unit">km/h</span></div>' +
1220            '<div class="card-detail">' + I18N.t('wind_direction') + ': ' + getWindDirection(wind.angle);
1221        if (wind.gust) {
1222            html += ' | ' + I18N.t('wind_gust_label') + ': ' + wind.gust.toFixed(1) + ' km/h';
1223        }
1224        if (wind.lull !== null && wind.lull !== undefined) {
1225            html += ' | ' + I18N.t('wind_lull_label') + ': ' + wind.lull.toFixed(1) + ' km/h';
1226        }
1227        html += '</div>';
1228        return html;
1229    }
1230
1231    function getRainIntensity(rain) {
1232        // Usa precip istantaneo (mm/min → mm/h) come principale.
1233        // Fallback: mm degli ultimi ~15 min (campo 'recent', delta cumulato) → mm/h.
1234        // Reattivo in entrambe le direzioni: rileva la pioggia intermittente ma non
1235        // resta "pioggia leggera" fino a un'ora dopo che ha smesso (difetto di
1236        // last_1hr). Se 'recent' manca (payload vecchio), fallback su last_1hr.
1237        var instantMmh = (rain.current || 0) * 60;
1238        var recentMmh = rain.recent != null
1239            ? rain.recent * 4
1240            : (rain.last_1hr != null ? rain.last_1hr : 0);
1241
1242        var mmh = instantMmh > 0.05 ? instantMmh : recentMmh;
1243        var isActive = instantMmh > 0.05;
1244
1245        // Soglie allineate alla stazione Tempest WeatherFlow:
1246        // None: 0, Light: < 1, Moderate: 1-4, Heavy: 4-16, Very Heavy: > 16
1247        if (mmh <= 0)   return { label: I18N.t('rain_no_rain'),     color: '', rate: 0, active: false };
1248        if (mmh < 1)    return { label: I18N.t('rain_light'),       color: '#22c55e', rate: mmh, active: isActive };
1249        if (mmh < 4)    return { label: I18N.t('rain_moderate'),    color: '#eab308', rate: mmh, active: isActive };
1250        if (mmh < 16)   return { label: I18N.t('rain_heavy'),       color: '#f97316', rate: mmh, active: isActive };
1251        return            { label: I18N.t('rain_very_heavy'),       color: '#ef4444', rate: mmh, active: isActive };
1252    }
1253
1254    function buildRainCard(rain) {
1255        var dayVal = rain.day != null ? rain.day.toFixed(1) : '0.0';
1256        var hourVal = rain.last_1hr != null ? rain.last_1hr : (rain.hour != null ? rain.hour : null);
1257
1258        // Intensita' pioggia
1259        var intensity = getRainIntensity(rain);
1260        var intensityHtml = '';
1261        if (intensity.rate > 0) {
1262            var dotStyle = intensity.active
1263                ? 'display:inline-block;width:8px;height:8px;border-radius:50%;background:' + intensity.color + ';margin-right:0.4rem;animation:pulse-rain 1.5s ease-in-out infinite;'
1264                : 'display:inline-block;width:8px;height:8px;border-radius:50%;background:' + intensity.color + ';margin-right:0.4rem;opacity:0.6;';
1265            intensityHtml = '<div class="rain-intensity" style="margin-top:0.3rem;display:flex;align-items:center;">' +
1266                '<span style="' + dotStyle + '"></span>' +
1267                '<span style="color:' + intensity.color + ';font-weight:600;font-size:0.85rem;">' + intensity.label + '</span>' +
1268                '<span style="color:var(--text-muted);font-size:0.75rem;margin-left:0.4rem;">(' + intensity.rate.toFixed(1) + ' mm/h)</span>' +
1269                '</div>';
1270        }
1271
1272        // Dettagli: 1h | Oggi | Ieri | Durata
1273        var details = [];
1274        if (hourVal != null && hourVal > 0) details.push(I18N.t('rain_1h_label') + ': ' + parseFloat(hourVal).toFixed(1) + ' mm');
1275        details.push(I18N.t('rain_today_label') + ': ' + dayVal + ' mm');
1276        if (rain.yesterday != null && rain.yesterday > 0) {
1277            details.push(I18N.t('rain_yesterday_label') + ': ' + rain.yesterday.toFixed(1) + ' mm');
1278        }
1279        if (rain.minutes_today != null && rain.minutes_today > 0) {
1280            var h = Math.floor(rain.minutes_today / 60);
1281            var m = rain.minutes_today % 60;
1282            details.push((h > 0 ? h + 'h ' : '') + m + 'min ' + I18N.t('rain_duration'));
1283        }
1284
1285        return '<div class="card-label"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
1285ox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="M16 14v6"/><path d="M8 14v6"/><path d="M12 16v6"/></svg> ' + I18N.t('rain') + '</div>' +
1286            '<div class="card-value">' + dayVal + ' <span class="unit">mm</span></div>' +
1287            intensityHtml +
1288            '<div class="card-detail">' + details.join(' | ') + '</div>';
1289    }
1290
1291    // === MOBILE MAP GESTURES ===
1292    function initMobileMapGestures() {
1293        if (!('ontouchstart' in window)) return;
1294
1295        // Overlay iframe: tap per attivare, 5s timeout per disattivare
1296        document.querySelectorAll('.iframe-overlay').forEach(function (overlay) {
1297            overlay.addEventListener('click', function () {
1298                overlay.classList.add('active');
1299                clearTimeout(overlay._timer);
1300                overlay._timer = setTimeout(function () {
1301                    overlay.classList.remove('active');
1302                }, 5000);
1303            });
1304        });
1305    }
1306
1307    function updateLastRefresh() {
1308        var el = document.getElementById('refresh-time');
1309        if (el) {
1310            var now = new Date();
1311            el.textContent = I18N.t('updated_at') + ' ' + now.getHours() + ':' + String(now.getMinutes()).padStart(2, '0');
1312        }
1313    }
1314
1315    // === SHARE BUTTON ===
1316    // Flusso:
1317    //   1) Fetch PNG generato da api/share_image.php (stesso renderer IG/FB)
1318    //   2) Se browser supporta navigator.share con file -> share sheet nativo
1319    //   3) Se supporta solo navigator.share testo -> share testo + download immagine
1320    //   4) Altrimenti -> modal con anteprima immagine, download, copy testo
1321    function initShareBtn() {
1322        var btn = document.getElementById('share-btn');
1323        if (!btn) return;
1324        btn.addEventListener('click', function () {
1325            shareWithImage(btn);
1326        });
1327    }
1328
1329    function setShareButtonState(btn, state) {
1330        var label = document.getElementById('share-btn-label');
1331        if (state === 'loading') {
1332            btn.classList.add('loading');
1333            if (label) label.textContent = I18N.t('share_loading') || '...';
1334        } else if (state === 'copied') {
1335            btn.classList.remove('loading');
1336            btn.classList.add('copied');
1337            if (label) label.textContent = I18N.t('share_copied');
1338            setTimeout(function () { setShareButtonState(btn, 'idle'); }, 2500);
1339        } else {
1340            btn.classList.remove('loading');
1341            btn.classList.remove('copied');
1342            if (label) label.textContent = I18N.t('share');
1343        }
1344    }
1345
1346    // Testo BREVE usato quando si condivide con immagine (i dati sono gia'
1347    // visibili nell'immagine, quindi ridondante ripeterli).
1348    function buildShareShortText() {
1349        return 'meteopomarico.it';
1350    }
1351
1352    function shareWithImage(btn) {
1353        var fullText  = buildShareText();
1354        var shortText = buildShareShortText();
1355        setShareButtonState(btn, 'loading');
1356
1357        fetch('api/share_image.php', { credentials: 'same-origin' })
1358            .then(function (res) {
1359                if (!res.ok) throw new Error('HTTP ' + res.status);
1360                return res.blob();
1361            })
1362            .then(function (blob) {
1363                var fileName = 'meteopomarico-' + Date.now() + '.png';
1364                var file = new File([blob], fileName, { type: 'image/png' });
1365
1366                if (!navigator.share) {
1367                    openShareModal(blob, fullText, shortText, btn);
1368                    return;
1369                }
1370
1371                // Con immagine: usa testo breve (dettagli gia' nell'immagine).
1372                navigator.share({ text: shortText, files: [file] })
1373                    .then(function () { setShareButtonState(btn, 'idle'); })
1374                    .catch(function (err) {
1375                        if (err && err.name === 'AbortError') {
1376                            setShareButtonState(btn, 'idle');
1377                            return;
1378                        }
1379                        // Browser non accetta file -> share testo completo
1380                        navigator.share({ text: fullText })
1381                            .then(function () { setShareButtonState(btn, 'idle'); })
1382                            .catch(function (err2) {
1383                                if (err2 && err2.name === 'AbortError') {
1384                                    setShareButtonState(btn, 'idle');
1385                                    return;
1386                                }
1387                                openShareModal(blob, fullText, shortText, btn);
1388                            });
1389                    });
1390            })
1391            .catch(function (err) {
1392                console.error('Share fetch error:', err);
1393                setShareButtonState(btn, 'idle');
1394                if (navigator.clipboard) {
1395                    navigator.clipboard.writeText(buildShareText())
1396                        .then(function () { setShareButtonState(btn, 'copied'); });
1397                }
1398            });
1399    }
1400
1401    function openShareModal(blob, fullText, shortText, btn) {
1402        setShareButtonState(btn, 'idle');
1403        var url = URL.createObjectURL(blob);
1404
1405        var canCopyImage = !!(navigator.clipboard && window.ClipboardItem);
1406
1407        var overlay = document.createElement('div');
1408        overlay.className = 'share-modal-overlay';
1409        var copyImgBtn = canCopyImage
1410            ? '<button class="share-modal-btn primary" data-action="copy-image">' +
1411                  (I18N.t('share_copy_image') || 'Copia immagine') +
1412              '</button>'
1413            : '';
1414        overlay.innerHTML =
1415            '<div class="share-modal">' +
1416                '<button class="share-modal-close" aria-label="Chiudi">&times;</button>' +
1417                '<img class="share-modal-img" src="' + url + '" alt="">' +
1418                '<div class="share-modal-text"></div>' +
1419                '<div class="share-modal-actions">' +
1420                    copyImgBtn +
1421                    '<a class="share-modal-btn" download="meteopomarico-' + Date.now() + '.png" href="' + url + '">' +
1422                        (I18N.t('share_download') || 'Scarica') +
1423                    '</a>' +
1424                    '<button class="share-modal-btn" data-action="copy-text">' +
1425                        (I18N.t('share_copy_text') || 'Copia testo') +
1426                    '</button>' +
1427                '</div>' +
1428            '</div>';
1429
1430        overlay.querySelector('.share-modal-text').textContent = fullText;
1431
1432        document.body.appendChild(overlay);
1433        document.body.style.overflow = 'hidden';
1434
1435        function close() {
1436            URL.revokeObjectURL(url);
1437            overlay.remove();
1438            document.body.style.overflow = '';
1439        }
1440
1441        overlay.addEventListener('click', function (e) {
1442            if (e.target === overlay) close();
1443        });
1444        overlay.querySelector('.share-modal-close').addEventListener('click', close);
1445        overlay.querySelector('[data-action="copy-text"]').addEventListener('click', function () {
1446            if (!navigator.clipboard) return;
1447            var self = this;
1448            navigator.clipboard.writeText(fullText).then(function () {
1449                self.textContent = I18N.t('share_copied') || 'Copiato!';
1450            });
1451        });
1452        var copyImgEl = overlay.querySelector('[data-action="copy-image"]');
1453        if (copyImgEl) {
1454            copyImgEl.addEventListener('click', function () {
1455                var self = this;
1456                var textBlob = new Blob([shortText], { type: 'text/plain' });
1457                navigator.clipboard.write([
1458                    new ClipboardItem({
1459                        'image/png': blob,
1460                        'text/plain': textBlob,
1461                    })
1462                ]).then(function () {
1463                    self.textContent = I18N.t('share_copied') || 'Copiato!';
1464                }).catch(function (err) {
1465                    console.warn('Copy image not supported, falling back to text:', err);
1466                    if (navigator.clipboard) {
1467                        navigator.clipboard.writeText(fullText).then(function () {
1468                            self.textContent = (I18N.t('share_copied') || 'Copiato!') + ' (testo)';
1469                        }).catch(function () {
1470                            self.textContent = I18N.t('share_unsupported') || 'Non supportato';
1471                        });
1472                    } else {
1473                        self.textContent = I18N.t('share_unsupported') || 'Non supportato';
1474                    }
1475                });
1476            });
1477        }
1478    }
1479
1480    function buildShareText() {
1481        var temp     = document.getElementById('hero-temp-value')?.textContent || '--';
1482        var humidity = document.getElementById('humidity-value')?.textContent || '--';
1483        var pressure = document.getElementById('pressure-value')?.textContent || '--';
1484        var aqi      = document.getElementById('aqi-value')?.textContent || '--';
1485        var aqiLabel = document.getElementById('aqi-label')?.textContent || '';
1486        var now      = new Date();
1487        var time     = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
1488        var date     = String(now.getDate()).padStart(2, '0') + '.'
1489                     + String(now.getMonth() + 1).padStart(2, '0') + '.'
1490                     + now.getFullYear();
1491
1492        var lines = [
1493            '\ud83d\udccd Pomarico - ' + date + ' - ' + time,
1494            '\ud83c\udf21\ufe0f ' + I18N.t('share_temp') + ' ' + temp,
1495            '\ud83d\udca7 ' + I18N.t('share_hum') + ' ' + humidity,
1496            '\ud83d\udd35 ' + I18N.t('share_press') + ' ' + pressure + ' hPa',
1497        ];
1498        if (aqi !== '--') lines.push('\ud83c\udf3f ' + I18N.t('share_aqi') + ' ' + aqi + ' 
1498(' + aqiLabel + ')');
1499
1500        var alertBar   = document.getElementById('alert-bar');
1501        var alertTitle = document.getElementById('alert-bar-title');
1502        if (alertBar && alertTitle) {
1503            var isActive = !alertBar.classList.contains('alert-verde');
1504            lines.push(isActive ? '\u26a0\ufe0f ' + alertTitle.textContent : '\u2705 ' + I18N.t('no_alert'));
1505        }
1506
1507        lines.push('\ud83d\udcca meteopomarico.it');
1508        return lines.join('\n');
1509    }
1510
1511    // === LANG TOGGLE ===
1512    function initLangToggle() {
1513        var btn = document.getElementById('lang-toggle');
1514        if (!btn) return;
1515        btn.textContent = I18N.currentLang === 'it' ? 'EN' : 'IT';
1516        btn.addEventListener('click', function () {
1517            var next = I18N.currentLang === 'it' ? 'en' : 'it';
1518            I18N.setLang(next);
1519            btn.textContent = next === 'it' ? 'EN' : 'IT';
1520            // Ricarica dati per aggiornare testi dinamici
1521            loadWeatherData();
1522            loadForecastData();
1523            loadAlerts();
1524            loadRecords();
1525            loadStats();
1526            loadMarineData();
1527            updateMoonPhase();
1528            updateChartTitle(currentChartRange);
1529        });
1530    }
1531
1532    // === UNIT TOGGLE C / F ===
1533    function initUnitToggle() {
1534        var saved = localStorage.getItem('tempUnit');
1535        if (saved === 'F') useFahrenheit = true;
1536
1537        var btn = document.getElementById('unit-toggle');
1538        if (!btn) return;
1539
1540        btn.textContent = useFahrenheit ? '\u00b0F' : '\u00b0C';
1541
1542        btn.addEventListener('click', function () {
1543            useFahrenheit = !useFahrenheit;
1544            btn.textContent = useFahrenheit ? '\u00b0F' : '\u00b0C';
1545            localStorage.setItem('tempUnit', useFahrenheit ? 'F' : 'C');
1546            // Ricarica i dati per aggiornare tutte le temperature
1547            loadWeatherData();
1548            loadForecastData();
1549        });
1550    }
1551
1552    // === GRAFICO STORICO ESTESO ===
1553    function loadChartHistory(range) {
1554        fetch(API_URL + '?range=' + range)
1555            .then(function (r) { return r.json(); })
1556            .then(function (data) { updateChart(data.history, range); })
1557            .catch(function (err) { console.error('Errore storico chart:', err); });
1558    }
1559
1560    function updateChartTitle(range) {
1561        var el = document.getElementById('chart-title');
1562        if (!el) return;
1563        var keys = { '24h': 'chart_24h', '7d': 'chart_7d', '30d': 'chart_30d' };
1564        el.textContent = I18N.t(keys[range] || 'chart_24h');
1565    }
1566
1567    function formatChartLabel(timeStr, range) {
1568        var d = new Date(timeStr.replace(' ', 'T'));
1569        if (range === '24h') {
1570            return d.getHours() + ':' + String(d.getMinutes()).padStart(2, '0');
1571        }
1572        if (range === '7d') {
1573            var dayKeys = ['day_sun', 'day_mon', 'day_tue', 'day_wed', 'day_thu', 'day_fri', 'day_sat'];
1574            return I18N.t(dayKeys[d.getDay()]) + ' ' + String(d.getHours()).padStart(2, '0') + ':00';
1575        }
1576        // 30d
1577        return String(d.getDate()).padStart(2, '0') + '/' + String(d.getMonth() + 1).padStart(2, '0');
1578    }
1579
1580    // === ALLERTE METEO ===
1581    function loadAlerts() {
1582        fetch(ALERTS_URL)
1583            .then(function (r) {
1584                if (!r.ok) throw new Error('HTTP ' + r.status);
1585                return r.json();
1586            })
1587            .then(function (data) {
1588                var bar = document.getElementById('alert-bar');
1589                if (!bar) return;
1590
1591                // Mostra sempre la barra
1592                bar.style.display = 'block';
1593                bar.className = 'alert-bar alert-' + data.level;
1594
1595                // Icona
1596                var icon = document.getElementById('alert-bar-icon');
1597                if (icon) {
1598                    if (data.active) {
1599                        icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>';
1600                    } else {
1601                        icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="m9 12 2 2 4-4"/></svg>';
1602                    }
1603                }
1604
1605                // Titolo compatto
1606                var title = document.getElementById('alert-bar-title');
1607                if (title) {
1608                    if (data.active) {
1609                        var colorKey = 'alert_' + data.level;
1610                        var alertLabel = I18N.t(colorKey);
1611                        // Se solo domani, aggiungi "(da domani)" nel titolo
1612                        var hasOggi = data.alerts && data.alerts.some(function(a) { return a.period === 'oggi'; });
1613                        var hasDomani = data.alerts && data.alerts.some(function(a) { return a.period && a.period !== 'oggi'; });
1614                        var periodHint = '';
1615                        if (!hasOggi && hasDomani) {
1616                            periodHint = ' (' + I18N.t('from_tomorrow') + ')';
1617                        }
1618                        title.textContent = alertLabel + ' \u2014 ' + I18N.t('alert_source').split(' \u2014 ')[1] + periodHint;
1619                    } else {
1620                        title.textContent = I18N.t('no_alert');
1621                    }
1622                }
1623
1624                // Dettagli espandibili con pill colorate
1625                var details = document.getElementById('alert-bar-details');
1626                if (details) {
1627                    details.innerHTML = '';
1628                    if (data.active && data.alerts && data.alerts.length > 0) {
1629                        var riskMap = {
1630                            'Rischio Idraulico': 'risk_idraulico',
1631                            'Temporali': 'risk_temporali',
1632                            'Rischio Idrogeologico': 'risk_idrogeologico'
1633                        };
1634                        var colorCssMap = {
1635                            'giallo': 'giallo', 'gialla': 'giallo',
1636                            'arancione': 'arancione',
1637                            'rosso': 'rosso', 'rossa': 'rosso'
1638                        };
1639
1640                        // Raggruppa per periodo
1641                        var byPeriod = {};
1642                        data.alerts.forEach(function (a) {
1643                            var p = a.period || 'oggi';
1644                            if (!byPeriod[p]) byPeriod[p] = [];
1645                            byPeriod[p].push(a);
1646                        });
1647
1648                        var periods = Object.keys(byPeriod);
1649                        periods.forEach(function (period) {
1650                            var section = document.createElement('div');
1651                            section.className = 'alert-bar-period';
1652
1653                            // Label periodo con data
1654                            var label = document.createElement('div');
1655                            label.className = 'alert-bar-period-label';
1656                            var now = new Date();
1657                            if (period === 'oggi') {
1658                                var dd = String(now.getDate()).padStart(2, '0');
1659                                var mm = String(now.getMonth() + 1).padStart(2, '0');
1660                                label.textContent = I18N.t('alert_period_today') + ' ' + dd + '/' + mm;
1661                            } else {
1662                                // "da domani DD/MM/YYYY" -> estrai la data
1663                                var dateMatch = period.match(/(\d{2}\/\d{2})/);
1664                                if (dateMatch) {
1665                                    label.textContent = I18N.t('alert_period_tomorrow') + ' ' + dateMatch[1];
1666                                } else {
1667                                    var tom = new Date(now.getTime() + 86400000);
1668                                    var tdd = String(tom.getDate()).padStart(2, '0');
1669                                    var tmm = String(tom.getMonth() + 1).padStart(2, '0');
1670                                    label.textContent = I18N.t('alert_period_tomorrow') + ' ' + tdd + '/' + tmm;
1671                                }
1672                            }
1673                            section.appendChild(label);
1674
1675                            // Pill per ogni rischio con colore tra parentesi
1676                            byPeriod[period].forEach(function (a) {
1677                                var pill = document.createElement('span');
1678                                var cssColor = colorCssMap[(a.color || '').toLowerCase()] || 'arancione';
1679                                pill.className = 'alert-bar-risk risk-' + cssColor;
1680
1681                                var dot = document.createElement('span');
1682                                dot.className = 'alert-bar-risk-dot';
1683                                pill.appendChild(dot);
1684
1685                                var translatedType = riskMap[a.type] ? I18N.t(riskMap[a.type]) : a.type;
1686                                if (a.label) translatedType += ' (' + a.label + ')';
1687                                pill.appendChild(document.createTextNode(translatedType));
1688
1689                                section.appendChild(pill);
1690                            });
1691
1692                            details.appendChild(section);
1693                        });
1694
1695                        // Allerta attiva: parti sempre espanso (utente
1696                        // clicca toggle per chiudere). Indipendente dal
1697                        // numero di periodi.
1698                        bar.classList.add('expanded');
1699
1700                        // Link bollettino PDF
1701                        if (data.bulletin && data.bulletin.pdf) {
1702                            var bulletinDiv = document.createElement('div');
1703                            bulletinDiv.className = 'alert-bar-bulletin';
1704                            var link = document.createElement('a');
1705                            link.className = 'alert-bar-bulletin-link';
1706                            link.href = data.bulletin.pdf;
1707                            link.target = '_blank';
1708                            link.rel = 'noopener';
1709                            link.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg> ' + I18N.t('alert_bulletin_link');
1710                            bulletinDiv.appendChild(link);
1711                            details.appendChild(bulletinDiv);
1712                        }
1713                    }
1714                }
1715
1716                // Fonte
1717                var source = document.getElementById('alert-bar-source');
1718                if (source && data.source) {
1719                    source.textContent = I18N.t('source_label') + ': ' + data.source;
1720                }
1721
1722                // Toggle espandi/comprimi (solo se allerta attiva)
1723                var toggle = document.getElementById('alert-bar-toggle');
1724                if (toggle && !toggle._hasListener) {
1725                    toggle._hasListener = true;
1726                    toggle.addEventListener('click', function () {
1727                        if (!bar.classList.contains('alert-verde')) {
1728                            bar.classList.toggle('expanded');
1729                        }
1730                    });
1731                }
1732            })
1733            .catch(function (err) {
1734                console.error('Errore caricamento allerte:', err);
1735            });
1736    }
1737
1738    // === THEME TOGGLE ===
1739    function initThemeToggle() {
1740        var toggle = document.getElementById('theme-toggle');
1741        if (!toggle) return;
1742
1743        toggle.addEventListener('click', function () {
1744            var current = document.documentElement.getAttribute('data-theme');
1745            var next = current === 'light' ? 'dark' : 'light';
1746            applyTheme(next);
1747            localStorage.setItem('theme', next);
1748        });
1749
1750        window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function (e) {
1751            if (!localStorage.getItem('theme')) {
1752                applyTheme(e.matches ? 'light' : 'dark');
1753            }
1754        });
1755    }
1756
1757    function applyTheme(theme) {
1758        if (theme === 'light') {
1759            document.documentElement.setAttribute('data-theme', 'light');
1760        } else {
1761            document.documentElement.removeAttribute('data-theme');
1762        }
1763        var meta = document.querySelector('meta[name="theme-color"]');
1764        if (meta) meta.setAttribute('content', theme === 'light' ? '#f0f4f8' : '#0a0e17');
1765        updateChartTheme();
1766        updateMarineMapTheme(theme);
1767    }
1768
1769    function getChartThemeColors() {
1770        var isLight = document.documentElement.getAttribute('data-theme') === 'light';
1771        return {
1772            gridColor:    isLight ? 'rgba(203, 213, 225, 0.5)' : 'rgba(42, 58, 80, 0.4)',
1773            tickColor:    isLight ? '#475569' : '#64748b',
1774            tooltipBg:    isLight ? '#ffffff' : '#1a2332',
1775            tooltipTitle: isLight ? '#0f172a' : '#f0f4f8',
1776            tooltipBody:  isLight ? '#475569' : '#94a3b8',
1777            tooltipBorder:isLight ? '#cbd5e1' : '#2a3a50',
1778        };
1779    }
1780
1781    function updateChartTheme() {
1782        if (!chart) return;
1783        var c = getChartThemeColors();
1784        chart.options.scales.x.grid.color = c.gridColor;
1785        chart.options.scales.x.ticks.color = c.tickColor;
1786        chart.options.scales.y.grid.color = c.gridColor;
1787        chart.options.scales.y.ticks.color = c.tickColor;
1788        chart.options.scales.y.title.color = c.tickColor;
1789        chart.options.plugins.tooltip.backgroundColor = c.tooltipBg;
1790        chart.options.plugins.tooltip.titleColor = c.tooltipTitle;
1791        chart.options.plugins.tooltip.bodyColor = c.tooltipBody;
1792        chart.options.plugins.tooltip.borderColor = c.tooltipBorder;
1793        chart.update('none');
1794    }
1795
1796    // === TEMPEST WEBSOCKET — VENTO REAL-TIME ===
1797    var _wsRetryCount = 0;
1798    var _wsMaxRetries = 5;
1799
1800    function initTempestWebSocket() {
1801        var cfg = window.MP_TEMPEST || {};
1802        var token = cfg.token || '6017ce2c-5c3f-41ef-9de8-083453b23afe';
1803        var deviceId = cfg.deviceId || 120500;
1804        var wsUrl = 'wss://ws.weatherflow.com/swd/data?token=' + token;
1805
1806        try {
1807            var ws = new WebSocket(wsUrl);
1808
1809            ws.onopen = function () {
1810                _wsRetryCount = 0;
1811                // Sottoscrivi a rapid_wind (ogni 3 sec) e obs_st (ogni minuto)
1812                ws.send(JSON.stringify({
1813                    type: 'listen_rapid_start',
1814                    device_id: deviceId,
1815                    id: 'mp-rapid-' + Date.now()
1816                }));
1817                ws.send(JSON.stringify({
1818                    type: 'listen_start',
1819                    device_id: deviceId,
1820                    id: 'mp-obs-' + Date.now()
1821                }));
1822                console.log('Tempest WebSocket connesso');
1823                // Indicatore live
1824                var dot = document.getElementById('live-dot');
1825                if (dot) dot.classList.add('ws-live');
1826            };
1827
1828            ws.onmessage = function (event) {
1829                try {
1830                    var msg = JSON.parse(event.data);
1831
1832                    // rapid_wind: [timestamp, wind_mps, wind_direction]
1833                    if (msg.type === 'rapid_wind' && msg.ob) {
1834                        var windMps = msg.ob[1];
1835                        var windDir = msg.ob[2];
1836                        var windKmh = Math.round(windMps * 3.6 * 10) / 10;
1837                        updateWindRealtime(windKmh, windDir);
1838                    }
1839
1840                    // obs_st: osservazione COMPLETA ogni ~1 minuto
1841                    // Indici: 0=ts,1=lull,2=avg,3=gust,4=dir,5=interval,
1842                    //   6=pressure,7=temp,8=humidity,9=lux,10=uv,11=solar,
1843                    //   12=rain,13=precipType,14=strikeDist,15=strikeCount,
1844                    //   16=battery,17=interval,18=dailyRain,19=rainNC,20=dailyRainNC
1845                    if (msg.type === 'obs_st' && msg.obs && msg.obs[0]) {
1846                        var o = msg.obs[0];
1847                        var toKmh = function(mps) { return mps != null ? Math.round(mps * 3.6 * 10) / 10 : null; };
1848
1849                        // Temperatura
1850                        if (o[7] != null) {
1851                            setTextIfExists('hero-temp-value', formatTemp(o[7]));
1852                            // Aggiorna anche il titolo della tab (live via WebSocket)
1853                            document.title = 'MeteoPomarico — ' + formatTemp(o[7]);
1854                        }
1855
1856                        // Umidita'
1857                        if (o[8] != null) setTextIfExists('humidity-value', Math.round(o[8]) + '%');
1858
1859                        // Pressione (SLP approssimato: station + ~45 hPa per 380m altitudine)
1860                        if (o[6] != null) {
1861                            var slp = o[6] + 45.5; // approssimazione per 379.5m
1862                            setTextIfExists('pressure-value', slp.toFixed(1));
1863                        }
1864
1865                        // Vento raffiche + lull
1866                        setTextIfExists('wind-stat-gusts', toKmh(o[3]) || '--');
1867                        var rangeEl = document.getElementById('wind-range');
1868                        if (rangeEl) rangeEl.textContent = (toKmh(o[1]) || 0) + ' - ' + (toKmh(o[3]) || 0);
1869
1870                        // UV
1871                        if (o[10] != null) {
1872                            var uvEl = document.getElementById('uv-value');
1873                            if (uvEl) {
1874                                uvEl.textContent = o[10].toFixed(1);
1875                                var uvInfo = getUVInfo(o[10]);
1876                                uvEl.closest('.card-value').className = 'card-value ' + uvInfo.cls;
1877                                var uvLabel = document.getElementById('uv-label');
1878                                if (uvLabel) uvLabel.textContent = uvInfo.label;
1879                            }
1880                        }
1881
1882                        // Radiazione solare + illuminamento
1883                        if (o[11] != null) setTextIfExists('solar-rad-value', Math.round(o[11]));
1884                        var illumEl = document.getElementById('illuminance-value');
1885                        if (illumEl && o[9] != null) {
1886                            illumEl.textContent = I18N.t('illuminance_label') + ': ' + Math.round(o[9]).toLocaleStr
1886ing() + ' lux';
1887                        }
1888
1889                        // Pioggia — aggiorna card completa
1890                        var rainSection = document.getElementById('rain-section');
1891                        if (rainSection) {
1892                            var wsRain = {
1893                                current: o[12] || 0,
1894                                last_1hr: null, // non disponibile in obs_st
1895                                day: o[20] || o[18] || 0,
1896                                yesterday: null,
1897                                minutes_today: null
1898                            };
1899                            // Mantieni last_1hr/yesterday dalla ultima chiamata API
1900                            if (rainSection._lastStationRain) {
1901                                wsRain.last_1hr = rainSection._lastStationRain.last_1hr;
1902                                wsRain.yesterday = rainSection._lastStationRain.yesterday;
1903                                wsRain.minutes_today = rainSection._lastStationRain.minutes_today;
1904                            }
1905                            rainSection.innerHTML = buildRainCard(wsRain);
1906                        }
1907
1908                        // Punto di rugiada (calcola da temp e umidita')
1909                        if (o[7] != null && o[8] != null) {
1910                            var dp = calcDewPoint(o[7], o[8]);
1911                            setTextIfExists('dew-point-value', dp.toFixed(1));
1912                            var dpLabel = document.getElementById('dew-point-label');
1913                            if (dpLabel) dpLabel.textContent = getDewPointLabel(dp);
1914                        }
1915
1916                        // Fulmini
1917                        if (o[15] != null) {
1918                            var lCountEl = document.getElementById('lightning-count');
1919                            if (lCountEl && o[15] > 0) lCountEl.textContent = o[15];
1920                        }
1921
1922                        // Timestamp aggiornamento
1923                        var now = new Date();
1924                        setTextIfExists('data-timestamp', formatDateTime(now));
1925                    }
1926                } catch (e) {
1927                    // Ignora errori di parsing
1928                }
1929            };
1930
1931            ws.onclose = function () {
1932                console.log('Tempest WebSocket disconnesso');
1933                var dot = document.getElementById('live-dot');
1934                if (dot) dot.classList.remove('ws-live');
1935                // Riconnetti con backoff
1936                if (_wsRetryCount < _wsMaxRetries) {
1937                    _wsRetryCount++;
1938                    var delay = Math.min(5000 * _wsRetryCount, 30000);
1939                    setTimeout(initTempestWebSocket, delay);
1940                }
1941            };
1942
1943            ws.onerror = function () {
1944                ws.close();
1945            };
1946        } catch (e) {
1947            console.error('WebSocket non supportato:', e);
1948        }
1949    }
1950
1951    function updateWindRealtime(speedKmh, direction) {
1952        var names = ['Tramontana', 'Grecale', 'Levante', 'Scirocco', 'Ostro', 'Libeccio', 'Ponente', 'Maestrale'];
1953        var idx = Math.round(direction / 45) % 8;
1954        var windName = names[idx];
1955
1956        // Beaufort
1957        var bft = 0;
1958        var bftThresholds = [1, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118];
1959        for (var i = 0; i < bftThresholds.length; i++) {
1960            if (speedKmh >= bftThresholds[i]) bft = i + 1;
1961        }
1962
1963        _prevWindDir = smoothRotation(_prevWindDir, direction);
1964
1965        // Aggiorna SEZIONE STAZIONE (principale)
1966        setTextIfExists('station-wind-speed', speedKmh);
1967        setTextIfExists('station-wind-degrees', direction + '\u00b0');
1968        setTextIfExists('station-wind-name', I18N.t('wind_' + windName));
1969        var arrow = document.getElementById('station-wind-arrow');
1970        if (arrow) arrow.style.transform = 'rotate(' + _prevWindDir + 'deg)';
1971        var bftEl = document.getElementById('station-wind-bft');
1972        if (bftEl) { bftEl.textContent = bft; bftEl.className = 'wind-stat-value wind-stat-bft bft-' + bft; }
1973        var bftLbl = document.getElementById('station-wind-bft-label');
1974        if (bftLbl) bftLbl.textContent = I18N.t('bft_' + bft);
1975
1976        // Aggiorna anche SEZIONE MARINE (se visibile)
1977        setTextIfExists('wind-stat-speed', speedKmh);
1978        setTextIfExists('wind-gauge-degrees', direction + '\u00b0');
1979        setTextIfExists('wind-gauge-name', I18N.t('wind_' + windName));
1980        var arrow2 = document.getElementById('wind-arrow-current');
1981        if (arrow2) arrow2.style.transform = 'rotate(' + _prevWindDir + 'deg)';
1982        var bftEl2 = document.getElementById('wind-stat-bft');
1983        if (bftEl2) { bftEl2.textContent = bft; bftEl2.className = 'wind-stat-value wind-stat-bft bft-' + bft; }
1984        var bftLabel = document.getElementById('wind-bft-label');
1985        if (bftLabel) bftLabel.textContent = I18N.t('bft_' + bft);
1986    }
1987
1988    // === VENTI E MARE DELLO IONIO LUCANO ===
1989    function loadMarineData() {
1990        fetch(MARINE_URL)
1991            .then(function (r) { return r.json(); })
1992            .then(function (data) {
1993                renderMarineSection(data);
1994            })
1995            .catch(function (err) { console.error('Errore marine:', err); });
1996    }
1997
1998    function renderMarineSection(data) {
1999        var section = document.getElementById('marine-section');
2000        if (!section) return;
2001        section.style.display = '';
2002
2003        // Consiglio automatico
2004        renderMarineAdvice(data.advice, data.wind, data.swell_note);
2005
2006        // Vento
2007        renderMarineWind(data.wind);
2008
2009        // Card riepilogo costa (singola costa: Ionio Lucano)
2010        renderCoastCards(data.locations);
2011
2012        // Mappa visuale
2013        renderMarineMap(data.locations);
2014
2015        // Previsione 3 giorni vento + mari
2016        renderMarineForecast(data.wind_forecast, data.locations);
2017    }
2018
2019    function renderMarineAdvice(advice, wind, swellNote) {
2020        var el = document.getElementById('marine-advice');
2021        if (!el || !advice) return;
2022        el.style.display = '';
2023
2024        // Classe per colore card
2025        el.className = 'marine-advice-card card advice-' + (advice.level || 'good');
2026
2027        var iconEl = document.getElementById('marine-advice-icon');
2028        var mainEl = document.getElementById('marine-advice-main');
2029        var reasonEl = document.getElementById('marine-advice-reason');
2030
2031        // Icona — triangolo per allerta, sole per buono, onda per mosso
2032        if (iconEl) {
2033            if (advice.key === 'advice_alert_active' || advice.key === 'advice_strong_wind') {
2034                iconEl.textContent = '\u26A0\uFE0F';
2035            } else if (advice.key === 'advice_thunderstorm') {
2036                iconEl.textContent = '\u26C8\uFE0F';
2037            } else if (advice.key === 'advice_rough_sea' || advice.key === 'advice_rough_everywhere' || advice.key === 'advice_ionian' || advice.key === 'advice_slight') {
2038                iconEl.textContent = '\uD83C\uDF0A';
2039            } else if (advice.key === 'advice_calm_sea' || advice.key === 'advice_good_conditions' || advice.key === 'advice_calm' || advice.key === 'advice_ondina' || advice.key === 'advice_rain') {
2040                if (advice.weather_code !== null && advice.weather_code !== undefined) {
2041                    iconEl.innerHTML = getWeatherIcon(advice.weather_code);
2042                } else {
2043                    iconEl.textContent = '\u2600\uFE0F';
2044                }
2045            } else {
2046                iconEl.textContent = '\uD83C\uDF0A';
2047            }
2048        }
2049
2050        // Classe card: colore in base al tipo di consiglio
2051        if (advice.key === 'advice_alert_active') {
2052            el.className = 'marine-advice-card card advice-none';
2053        } else if (advice.key === 'advice_strong_wind' || advice.key === 'advice_rough_sea' || advice.key === 'advice_rough_everywhere' || advice.key === 'advice_ionian' || advice.key === 'advice_thunderstorm') {
2054            el.className = 'marine-advice-card card advice-none';
2055        } else {
2056            el.className = 'marine-advice-card card advice-entrambe';
2057        }
2058
2059        // Titolo
2060        if (mainEl) mainEl.textContent = I18N.t(advice.key);
2061
2062        // Dettagli
2063        if (reasonEl) {
2064            var windNameStr = advice.wind_name ? I18N.t('wind_' + advice.wind_name) : '';
2065            var lines = [];
2066
2067            // Riga 1: vento
2068            if (wind && windNameStr) {
2069                lines.push(windNameStr + ' ' + wind.speed + ' km/h');
2070            }
2071
2072            // Riga 2: altezza onde Ionio
2073            var waveAvg = advice.avg_wave || advice.wave_avg;
2074            if (waveAvg !== undefined && waveAvg !== null) {
2075                lines.push(I18N.t('marine_taranto') + ': ' + waveAvg + ' m');
2076            }
2077
2078            // Durante allerta: mostra messaggio Protezione Civile, no swell note
2079            if (advice.key === 'advice_alert_active') {
2080                lines.push(I18N.t(advice.reason_key));
2081            } else if (swellNote) {
2082                // Pomarico è single-coast (Ionio): mostra sempre la nota swell
2083                // se presente. Il messaggio descrive l'evento passato che ha
2084                // generato le onde lunghe ancora in dissipazione.
2085                var lang = (typeof I18N !== 'undefined' && I18N.currentLang === 'en') ? 'en' : 'it';
2086                var swellMsg = lang === 'en' ? swellNote.message_en : swellNote.message;
2087                if (swellMsg) {
2088                    lines.push('<span class="swell-note">' + swellMsg + '</span>');
2089                }
2090            }
2091
2092            reasonEl.innerHTML = lines.join('<br>');
2093        }
2094    }
2095
2096    // Rotazione continua senza salti (350->10 va via +20, non -340)
2097    var _prevWindDir = null;
2098
2099    function smoothRotation(prev, target) {
2100        if (prev === null) return target;
2101        var diff = target - prev;
2102        if (diff > 180) diff -= 360;
2103        if (diff < -180) diff += 360;
2104        return prev + diff;
2105    }
2106
2107    // Aggiorna SOLO la bussola della stazione meteo Pomarico (entroterra,
2108    // stazione Tempest). Storicamente questa funzione sovrascriveva anche
2109    // la bussola marine + advice "VENTI E MARE DELLO IONIO" coi dati della
2110    // stazione, motivando "piu' precisi" — in realta' Pomarico e' ~30-40 km
2111    // dall'Ionio: a basse velocita' la direzione e' rumore. Il marine ora
2112    // prende il vento dall'aggregato vettoriale delle spiagge in api/marine.php.
2113    // Il nome della funzione e' rimasto per backward-compat (chiamato in 1 solo
2114    // punto, vedi loadWeatherData).
2115    function updateMarineWindFromStation(stationWind) {
2116        if (!stationWind || stationWind.speed == null) return;
2117
2118        var dir = stationWind.angle || 0;
2119        var speed = stationWind.speed;
2120        var gust = stationWind.gust;
2121        var lull = stationWind.lull;
2122
2123        var names = ['Tramontana', 'Grecale', 'Levante', 'Scirocco', 'Ostro', 'Libeccio', 'Ponente', 'Maestrale'];
2124        var idx = Math.round(dir / 45) % 8;
2125        var windName = names[idx];
2126
2127        // === SEZIONE STAZIONE (bussola principale) ===
2128        setTextIfExists('station-wind-degrees', dir + '\u00b0');
2129        setTextIfExists('station-wind-name', I18N.t('wind_' + windName));
2130        setTextIfExists('station-wind-speed', speed);
2131        if (gust != null) setTextIfExists('station-wind-gusts', gust);
2132
2133        var rangeEl = document.getElementById('station-wind-range');
2134        if (rangeEl && gust != null) {
2135            rangeEl.textContent = (lull || 0) + ' - ' + gust;
2136        }
2137
2138        // Beaufort
2139        var bft = 0;
2140        var bftThresholds = [1, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118];
2141        for (var i = 0; i < bftThresholds.length; i++) {
2142            if (speed >= bftThresholds[i]) bft = i + 1;
2143        }
2144        var bftEl = document.getElementById('station-wind-bft');
2145        if (bftEl) {
2146            bftEl.textContent = bft;
2147            bftEl.className = 'wind-stat-value wind-stat-bft bft-' + bft;
2148        }
2149        var bftLabel = document.getElementById('station-wind-bft-label');
2150        if (bftLabel) bftLabel.textContent = I18N.t('bft_' + bft);
2151
2152        // Freccia bussola stazione
2153        _prevWindDir = smoothRotation(_prevWindDir, dir);
2154        var arrow = document.getElementById('station-wind-arrow');
2155        if (arrow) {
2156            arrow.style.transform = 'rotate(' + _prevWindDir + 'deg)';
2157        }
2158
2159        // [RIMOSSO] Override bussola marine con dati stazione inland.
2160        // La sezione marine prende il vento dall'aggregato vettoriale
2161        // delle spiagge (api/marine.php), fonte corretta per dati marittimi.
2162
2163        // [RIMOSSO] Override testo advice marine con dati stazione inland.
2164        // L'advice viene popolato da renderMarineAdvice() col vento aggregato
2165        // delle spiagge — non lo tocchiamo qui.
2166    }
2167
2168    function renderMarineWind(wind) {
2169        if (!wind) return;
2170
2171        // Testo centrale
2172        setTextIfExists('wind-gauge-degrees', wind.direction + '\u00b0');
2173        setTextIfExists('wind-gauge-name', I18N.t('wind_' + wind.name));
2174
2175        // Stats
2176        setTextIfExists('wind-stat-speed', wind.speed);
2177        setTextIfExists('wind-stat-gusts', wind.gusts);
2178
2179        // Range lull-gust (come Tempest: "1 - 9")
2180        var rangeEl = document.getElementById('wind-range');
2181        if (rangeEl && wind.gusts != null) {
2182            var lull = wind.lull != null ? wind.lull : 0;
2183            rangeEl.textContent = lull + ' - ' + wind.gusts;
2184        }
2185
2186        // Beaufort
2187        if (wind.beaufort) {
2188            var bftEl = document.getElementById('wind-stat-bft');
2189            var bftLabelEl = document.getElementById('wind-bft-label');
2190            if (bftEl) {
2191                bftEl.textContent = wind.beaufort.level;
2192                bftEl.className = 'wind-stat-value wind-stat-bft bft-' + wind.beaufort.level;
2193            }
2194            if (bftLabelEl) {
2195                bftLabelEl.textContent = I18N.t('bft_' + wind.beaufort.level) || wind.beaufort.label;
2196            }
2197        }
2198
2199        // Ruota freccia (CSS transform, 3s transition)
2200        _prevWindDir = smoothRotation(_prevWindDir, wind.direction);
2201        var arrow = document.getElementById('wind-arrow-current');
2202        if (arrow) {
2203            arrow.style.transform = 'rotate(' + _prevWindDir + 'deg)';
2204        }
2205    }
2206
2207    var MARINE_WAVES_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 6c.6.5 1.2 1 2.5 1C7 7 7 5 9.5 5c2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"/><path d="M2 12c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"/><path d="M2 18c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"/></svg>';
2208
2209    function seaWaveColor(loc) {
2210        if (loc && loc.sea_state && SEA_STATE_COLORS[loc.sea_state.color]) {
2211            return SEA_STATE_COLORS[loc.sea_state.color];
2212        }
2213        return '#64748b';
2214    }
2215
2216    // Card singola spiaggia (stile app): onda grande colorata + stato + score 
2216+ acqua
2217    function renderBeachCard(loc) {
2218        var color = seaWaveColor(loc);
2219        var wave = (loc.wave_height !== null && loc.wave_height !== undefined) ? loc.wave_height.toFixed(2) : '--';
2220        var state = loc.sea_state ? I18N.t(loc.sea_state.key) : '--';
2221        var score = loc.score ? '<div class="bc-score">' + loc.score + '/10</div>' : '';
2222        var temp = (loc.sea_temp !== null && loc.sea_temp !== undefined) ? '<div class="bc-temp">' + formatTemp(loc.sea_temp) + '</div>' : '';
2223        return '<div class="beach-card">' +
2224            '<div class="bc-name">' + loc.name + '</div>' +
2225            '<div class="bc-wave" style="color:' + color + '">' + wave + '<small>m</small></div>' +
2226            '<div class="bc-state">' + state + '</div>' +
2227            score + temp +
2228            '</div>';
2229    }
2230
2231    function renderCoastBlock(titleKey, icon, locs) {
2232        if (!locs || locs.length === 0) return '';
2233        var out = '<div class="section-title marine-coast-title">' + icon +
2234            '<span data-i18n="' + titleKey + '">' + I18N.t(titleKey) + '</span></div>';
2235        out += '<div class="beach-scroll">';
2236        locs.forEach(function (loc) { out += renderBeachCard(loc); });
2237        out += '</div>';
2238        return out;
2239    }
2240
2241    // Card per singola spiaggia a scorrimento (come su LecceMeteo). Qui la costa
2242    // e' una sola (Ionica Lucana), quindi un unico blocco con tutte le localita
2243    // nell'ordine di api/marine.php (da nord-est a sud-ovest).
2244    function renderBeachScroll(locations) {
2245        var container = document.getElementById('marine-coasts');
2246        if (!container || !locations) return;
2247        container.innerHTML = renderCoastBlock('coast_ionian_lucana', MARINE_WAVES_ICON, locations);
2248    }
2249
2250    // Card per singola spiaggia a scorrimento (blocco "Costa Ionica Lucana").
2251    // Prima qui c'era anche la card aggregata "Mar Ionio" con la lista delle
2252    // localita': mostrava le stesse dieci spiagge delle card qui sotto, quindi
2253    // e' stata rimossa dal DOM insieme al codice che la riempiva.
2254    function renderCoastCards(locations) {
2255        if (!locations || locations.length === 0) return;
2256        renderBeachScroll(locations);
2257    }
2258
2259    function getScoreLabel(score) {
2260        if (score >= 7) return I18N.t('score_ottimo');
2261        if (score >= 5) return I18N.t('score_buono');
2262        if (score >= 3) return I18N.t('score_discreto');
2263        return I18N.t('score_sconsigliato');
2264    }
2265
2266    var SEA_STATE_COLORS = {
2267        'sea-calm':       '#22c55e',
2268        'sea-ripple':     '#06b6d4',
2269        'sea-slight':     '#eab308',
2270        'sea-moderate':   '#f97316',
2271        'sea-rough':      '#ef4444',
2272        'sea-very-rough': '#a855f7',
2273    };
2274
2275    var marineMap = null;
2276    var marineMarkers = [];
2277    var lastMarineBounds = null;
2278
2279    // Chiamato dallo switch dei tab all'apertura di "Mappa": la mappa MapLibre
2280    // viene creata anche mentre il tab e' nascosto (container 0x0), quindi al
2281    // primo show serve un resize + refit sui bounds calcolati.
2282    window.meteopomaricoShowMarineTab = function () {
2283        if (!marineMap) return;
2284        marineMap.resize();
2285        if (lastMarineBounds && !lastMarineBounds.isEmpty()) {
2286            marineMap.fitBounds(lastMarineBounds, { padding: { top: 40, bottom: 40, left: 20, right: 180 }, maxZoom: 10.5 });
2287        }
2288    };
2289
2290    function renderMarineMap(locations) {
2291        if (!locations || typeof maplibregl === 'undefined') return;
2292
2293        var mapEl = document.getElementById('marine-leaflet-map');
2294        if (!mapEl) return;
2295
2296        // Stile base: CARTO vector tiles (gratuiti, no API key)
2297        var isLight = document.documentElement.getAttribute('data-theme') === 'light';
2298        var styleUrl = isLight
2299            ? 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'
2300            : 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';
2301
2302        // Inizializza mappa solo la prima volta
2303        if (!marineMap) {
2304            marineMap = new maplibregl.Map({
2305                container: 'marine-leaflet-map',
2306                style: styleUrl,
2307                center: [16.78, 40.28],
2308                zoom: 9,
2309                attributionControl: false,
2310                scrollZoom: false,
2311                cooperativeGestures: true,
2312            });
2313
2314            marineMap.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-left');
2315            marineMap.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-right');
2316
2317            marineMap.on('load', function () {
2318
2319                // Marker spiagge sopra i layer
2320                addMarineMarkers(locations);
2321            });
2322        } else {
2323            marineMarkers.forEach(function (m) { m.remove(); });
2324            marineMarkers = [];
2325            addMarineMarkers(locations);
2326        }
2327    }
2328
2329    function addMarineMarkers(locations) {
2330        marineMarkers.forEach(function (m) { m.remove(); });
2331        marineMarkers = [];
2332
2333        var bounds = new maplibregl.LngLatBounds();
2334
2335        locations.forEach(function (loc) {
2336            if (loc.wave_height === null) return;
2337
2338            var color = loc.sea_state ? (SEA_STATE_COLORS[loc.sea_state.color] || '#64748b') : '#64748b';
2339            var stateText = loc.sea_state ? I18N.t(loc.sea_state.key) : '--';
2340            var waveText = loc.wave_height + ' m';
2341            var tempText = loc.sea_temp !== null ? loc.sea_temp + '\u00b0' : '';
2342            var scoreText = loc.score ? loc.score + '/10' : '';
2343
2344            // Marker con label integrato (dot + nome)
2345            var el = document.createElement('div');
2346            el.className = 'marine-marker-label';
2347            el.innerHTML =
2348                '<span class="mml-dot" style="background:' + color + ';"></span>' +
2349                '<span class="mml-text">' +
2350                    '<strong>' + loc.name + '</strong>' +
2351                    '<span class="mml-info">' + stateText + ' \u2022 ' + waveText +
2352                    (tempText ? ' \u2022 <span class="mml-temp">' + tempText + '</span>' : '') +
2353                    (scoreText ? ' \u2022 <span class="mml-score">' + scoreText + '</span>' : '') +
2354                    '</span>' +
2355                '</span>';
2356
2357            // Popup dettagliato al click
2358            var popupHtml = '<div style="font-family:Inter,sans-serif;font-size:13px;line-height:1.6;padding:4px;">' +
2359                '<strong style="font-size:14px;">' + loc.name + '</strong><br>' +
2360                '<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + color + ';margin-right:5px;vertical-align:middle;"></span>' +
2361                '<strong>' + stateText + '</strong> \u2022 ' + waveText + '<br>' +
2362                (loc.sea_temp !== null ? '\uD83C\uDF21 ' + I18N.t('marine_sea_temp') + ': <strong style="color:#06b6d4;">' + loc.sea_temp + '\u00b0C</strong><br>' : '') +
2363                (loc.score ? '\u2B50 Score: <strong style="color:#7B9CFF;">' + loc.score + '/10</strong> — ' + getScoreLabel(loc.score) : '') +
2364                '</div>';
2365
2366            var popup = new maplibregl.Popup({
2367                offset: [0, -8],
2368                closeButton: false,
2369                maxWidth: '250px'
2370            }).setHTML(popupHtml);
2371
2372            var marker = new maplibregl.Marker({ element: el, anchor: 'left' })
2373                .setLngLat([loc.lon, loc.lat])
2374                .setPopup(popup)
2375                .addTo(marineMap);
2376
2377            marineMarkers.push(marker);
2378            bounds.extend([loc.lon, loc.lat]);
2379        });
2380
2381        lastMarineBounds = bounds;
2382        if (!bounds.isEmpty()) {
2383            marineMap.fitBounds(bounds, { padding: { top: 40, bottom: 40, left: 20, right: 180 }, maxZoom: 10.5 });
2384        }
2385    }
2386
2387    function renderMarineForecast(forecast, locations) {
2388        var container = document.getElementById('marine-forecast-container');
2389        if (!container || !forecast || forecast.length === 0) return;
2390
2391        // Pre-calcola medie onde per ogni timestep forecast
2392        var coastWaves = {};
2393        if (locations) {
2394            locations.forEach(function (loc) {
2395                if (!loc.forecast) return;
2396                loc.forecast.forEach(function (ff) {
2397                    if (!coastWaves[ff.time]) coastWaves[ff.time] = [];
2398                    coastWaves[ff.time].push(ff.wave_height);
2399                });
2400            });
2401        }
2402
2403        var html = '';
2404        var prevDay = '';
2405        forecast.forEach(function (f) {
2406            var d = new Date(f.time);
2407            var dayNames = [
2408                I18N.t('day_sun'), I18N.t('day_mon'), I18N.t('day_tue'),
2409                I18N.t('day_wed'), I18N.t('day_thu'), I18N.t('day_fri'), I18N.t('day_sat')
2410            ];
2411            var dayName = dayNames[d.getDay()];
2412            var hourStr = String(d.getHours()).padStart(2, '0') + ':00';
2413            var dayKey = d.toDateString();
2414            var showDayLabel = dayKey !== prevDay;
2415            prevDay = dayKey;
2416
2417            // Stato mare medio a questo timestep
2418            var waveArr = coastWaves[f.time];
2419            var waveStr = '';
2420            if (waveArr && waveArr.length > 0) {
2421                var avg = waveArr.reduce(function(a, b) { return a + b; }, 0) / waveArr.length;
2422                waveStr = avg.toFixed(1) + 'm';
2423            }
2424
2425            html += '<div class="marine-forecast-item' + (showDayLabel ? ' new-day' : '') + '">';
2426            if (showDayLabel) {
2427                html += '<div class="marine-forecast-day">' + dayName + '</div>';
2428            }
2429            html += '<div class="marine-forecast-time">' + hourStr + '</div>' +
2430                '<div class="marine-forecast-arrow" style="transform:rotate(' + f.direction + 'deg)">\u2193</div>' +
2431                '<div class="marine-forecast-wind-name">' + I18N.t('wind_' + f.name) + '</div>' +
2432                '<div class="marine-forecast-wind-speed">' + f.speed + ' km/h</div>';
2433
2434            if (waveStr) {
2435                html += '<div class="marine-forecast-seas">';
2436                html += '<span class="fc-sea" title="Mar Ionio">' + waveStr + '</span>';
2437                html += '</div>';
2438            }
2439
2440            html += '</div>';
2441        });
2442
2443        container.innerHTML = html;
2444    }
2445
2446    // === ROSA DEI VENTI (polarArea) ===
2447    function renderWindRose(history) {
2448        if (!history || history.length === 0) return;
2449        var canvas = document.getElementById('wind-rose-chart');
2450        if (!canvas || !canvas.getContext) return;
2451
2452        var ctx = canvas.getContext('2d');
2453        var W = canvas.width;
2454        var H = canvas.height;
2455        var cx = W / 2;
2456        var cy = H / 2;
2457        var maxR = Math.min(cx, cy) - 30;
2458
2459        var sectorNames = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
2460        var windNames = ['Tramontana', 'Grecale', 'Levante', 'Scirocco', 'Ostro', 'Libeccio', 'Ponente', 'Maestrale'];
2461        // Angoli: N=270deg canvas (su), NE=315, E=0, SE=45, S=90, SW=135, W=180, NW=225
2462        var angles = [-90, -45, 0, 45, 90, 135, 180, -135]; // gradi canvas
2463
2464        var totalPoints = 0;
2465        var counts = [0, 0, 0, 0, 0, 0, 0, 0];
2466        var speedSums = [0, 0, 0, 0, 0, 0, 0, 0];
2467        var maxSpeeds = [0, 0, 0, 0, 0, 0, 0, 0];
2468
2469        history.forEach(function (point) {
2470            if (point.wind_angle == null) return;
2471            var idx = Math.round(point.wind_angle / 45) % 8;
2472            counts[idx]++;
2473            totalPoints++;
2474            var spd = point.wind_speed || 0;
2475            speedSums[idx] += spd;
2476            if (spd > maxSpeeds[idx]) maxSpeeds[idx] = spd;
2477        });
2478
2479        if (totalPoints === 0) return;
2480
2481        var percentages = counts.map(function (c) { return c / totalPoints; });
2482        var avgSpeeds = counts.map(function (c, i) { return c > 0 ? speedSums[i] / c : 0; });
2483        var maxPct = Math.max.apply(null, percentages);
2484        if (maxPct === 0) maxPct = 1;
2485
2486        var maxIdx = 0;
2487        for (var i = 1; i < counts.length; i++) {
2488            if (counts[i] > counts[maxIdx]) maxIdx = i;
2489        }
2490
2491        var isLight = document.documentElement.getAttribute('data-theme') === 'light';
2492
2493        // Colore petalo basato su velocita' media
2494        function petalColor(spd, alpha) {
2495            if (spd < 5)  return 'rgba(96, 165, 250, ' + alpha + ')';
2496            if (spd < 15) return 'rgba(59, 130, 246, ' + alpha + ')';
2497            if (spd < 30) return 'rgba(37, 99, 235, ' + alpha + ')';
2498            return 'rgba(124, 58, 237, ' + alpha + ')';
2499        }
2500
2501        // Pulisci canvas
2502        ctx.clearRect(0, 0, W, H);
2503
2504        // Cerchi concentrici di riferimento
2505        var gridColor = isLight ? 'rgba(203,213,225,0.5)' : 'rgba(42,58,80,0.4)';
2506        var textColor = isLight ? '#475569' : '#64748b';
2507        for (var g = 1; g <= 4; g++) {
2508            var gr = maxR * g / 4;
2509            ctx.beginPath();
2510            ctx.arc(cx, cy, gr, 0, Math.PI * 2);
2511            ctx.strokeStyle = gridColor;
2512            ctx.lineWidth = 1;
2513            ctx.stroke();
2514        }
2515
2516        // Linee cardinali sottili
2517        for (var l = 0; l < 8; l++) {
2518            var la = angles[l] * Math.PI / 180;
2519            ctx.beginPath();
2520            ctx.moveTo(cx, cy);
2521            ctx.lineTo(cx + Math.cos(la) * maxR, cy + Math.sin(la) * maxR);
2522            ctx.strokeStyle = gridColor;
2523            ctx.lineWidth = 0.5;
2524            ctx.stroke();
2525        }
2526
2527        // Petali
2528        for (var p = 0; p < 8; p++) {
2529            var pct = percentages[p];
2530            var r = maxR * (pct / maxPct) * 0.9 + maxR * 0.1; // Minimo 10%
2531            if (counts[p] === 0) r = maxR * 0.05;
2532            var a = angles[p] * Math.PI / 180;
2533            var halfAngle = (22.5) * Math.PI / 180; // Mezza ampiezza settore
2534
2535            // Disegna petalo come arco
2536            ctx.beginPath();
2537            ctx.moveTo(cx, cy);
2538            ctx.arc(cx, cy, r, a - halfAngle, a + halfAngle);
2539            ctx.closePath();
2540
2541            // Gradiente radiale per il petalo
2542            var grd = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
2543            grd.addColorStop(0, petalColor(avgSpeeds[p], 0.2));
2544            grd.addColorStop(0.5, petalColor(avgSpeeds[p], 0.5));
2545            grd.addColorStop(1, petalColor(avgSpeeds[p], 0.8));
2546            ctx.fillStyle = grd;
2547            ctx.fill();
2548
2549            // Bordo petalo
2550            ctx.strokeStyle = petalColor(avgSpeeds[p], 1);
2551            ctx.lineWidth = p === maxIdx ? 2.5 : 1;
2552            ctx.stroke();
2553        }
2554
2555        // Cerchio centrale
2556        ctx.beginPath();
2557        ctx.arc(cx, cy, 6, 0, Math.PI * 2);
2558        ctx.fillStyle = isLight ? '#475569' : '#94a3b8';
2559        ctx.fill();
2560
2561        // Freccia direzione attuale (dal weather data piu' recente)
2562        var lastPoint = history[history.length - 1];
2563        if (lastPoint && lastPoint.wind_angle != null) {
2564            var currentAngle = (lastPoint.wind_angle - 90) * Math.PI / 180; // Converti a canvas
2565            var arrowLen = maxR * 0.35;
2566            var ax = cx + Math.cos(currentAngle) * arrowLen;
2567            var ay = cy + Math.sin(currentAngle) * arrowLen;
2568
2569            ctx.beginPath();
2570            ctx.moveTo(cx, cy);
2571            ctx.lineTo(ax, ay);
2572            ctx.strokeStyle = '#ef4444';
2573            ctx.lineWidth = 2.5;
2574            ctx.stroke();
2575
2576            // Punta freccia
2577            var headLen = 8;
2578            var headAngle = 0.5;
2579            ctx.beginPath();
2580            ctx.moveTo(ax, ay);
2581            ctx.lineTo(ax - headLen * Math.cos(currentAngle - headAngle), ay - headLen * Math.sin(currentAngle - headAngle));
2582            ctx.lineTo(ax - headLen * Math.cos(currentAngle + headAngle), ay - headLen * Math.sin(currentAngle + headAngle));
2583            ctx.closePath();
2584            ctx.fillStyle = '#ef4444';
2585            ctx.fill();
2586        }
2587
2588        // Labels direzione
2589        ctx.textAlign = 'center';
2590        ctx.textBaseline = 'middle';
2591        for (var t = 0; t < 8; t++) {
2592            var ta = angles[t] * Math.PI / 180;
2593            var labelR = maxR + 18;
2594            var lx = cx + Math.cos(ta) * labelR;
2595            var ly = cy + Math.sin(ta) * labelR;
2596            ctx.font = (t === maxIdx ? 'bold 14px' : '12px') + ' Inter, sans-serif';
2597            ctx.fillStyle = t === maxIdx ? '#3b82f6' : textColor;
2598            ctx.fillText(sectorNames[t], lx, ly);
2599        }
2600
2601        // Legenda laterale
2602        var legendEl = document.getElementById('wind-rose-legend');
2603        if (legendEl) {
2604            var pctDisplay = Math.round(percentages[maxIdx] * 100);
2605            var lhtml = '<div style="font-size:1.1rem;font-weight:800;color:var(--text-primary);">'
2606                + I18N.t('wind_' + windNames[maxIdx]) + '</div>';
2607            lhtml += '<div style="font-size:1.8rem;font-weight:800;color:var(--accent);">' + pctDisplay + '%</div>';
2608            lhtml += '<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:1rem;">'
2609                + I18N.t('wind_rose_dominant') + '</div>';
2610
2611            // Stats per settore dominante
2612            lhtml += '<div style="font-size:0.8rem;margin-bottom:0.75rem;">';
2613            lhtml += '<div>' + I18N.t('wind_rose_avg') + ': <strong>' + avgSpeeds[maxIdx].toFixed(1) + ' km/h</strong></div>';
2614            lhtml += '<div>' + I18N.t('wind_rose_max') + ': <strong>' + maxSpeeds[maxIdx].toFixed(1) + ' km/h</strong></div>';
2615            lhtml += '</div>';
2616
2617            // Scala colori
2618            lhtml += '<div style="font-size:0.7rem;color:var(--text-muted);margin-bottom:4px;">' + I18N.t('marine_speed') + '</div>';
2619            var speedRanges = [
2620                { label: '< 5', color: 'rgba(96,165,250,0.7)' },
2621                { label: '5-15', color: 'rgba(59,130,246,0.8)' },
2622                { label: '15-30', color: 'rgba(37,99,235,0.9)' },
2623                { label: '> 30', color: 'rgba(124,58,237,0.9)' },
2624            ];
2625            speedRanges.forEach(function (r) {
2626                lhtml += '<div style="display:flex;align-items:center;gap:5px;margin:2px 0;">' +
2627                    '<span style="width:10px;height:10px;border-radius:2px;background:' + r.color + ';"></span>' +
2628                    '<span style="font-size:0.7rem;">' + r.label + ' km/h</span></div>';
2629            });
2630
2631            // Indicatore freccia rossa
2632            lhtml += '<div style="display:flex;align-items:center;gap:5px;margin-top:8px;">' +
2633                '<span style="width:16px;height:2px;background:#ef4444;"></span>' +
2634                '<span style="font-size:0.7rem;color:var(--text-muted);">' + I18N.t('wind_rose_current') + '</span></div>';
2635
2636            legendEl.innerHTML = lhtml;
2637        }
2638    }
2639
2640    // === GRAFICO PIOGGIA A BARRE ===
2641    var rainChart = null;
2642
2643    function renderRainChart(history) {
2644        if (!history || history.length === 0) return;
2645        var canvas = document.getElementById('rain-chart');
2646        if (!canvas) return;
2647
2648        var labels = [];
2649        var rateData = [];
2650        var cumulData = [];
2651        var barColors = [];
2652
2653        history.forEach(function (point) {
2654            var d = new Date(point.time.replace(' ', 'T'));
2655            labels.push(String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'));
2656
2657            var rate = point.rain_rate != null ? point.rain_rate : 0;
2658            rateData.push(rate);
2659
2660            var cumul = point.rain_day != null ? point.rain_day : 0;
2661            cumulData.push(cumul);
2662
2663            // Color by intensity
2664            if (rate < 1) barColors.push('rgba(34, 197, 94, 0.7)');
2665            else if (rate < 4) barColors.push('rgba(234, 179, 8, 0.7)');
2666            else if (rate < 16) barColors.push('rgba(249, 115, 22, 0.7)');
2667            else barColors.push('rgba(239, 68, 68, 0.7)');
2668        });
2669
2670        var tc = getChartThemeColors();
2671
2672        var datasets = [
2673            {
2674                label: 'mm/h',
2675                type: 'bar',
2676                data: rateData,
2677                backgroundColor: barColors,
2678                borderColor: barColors.map(function (c) { return c.replace('0.7', '1'); }),
2679                borderWidth: 1,
2680                yAxisID: 'y',
2681                order: 2
2682            },
2683            {
2684                label: 'mm (cum.)',
2685                type: 'line',
2686                data: cumulData,
2687                borderColor: '#3b82f6',
2688                backgroundColor: 'rgba(59, 130, 246, 0.1)',
2689                borderWidth: 2,
2690                fill: true,
2691                tension: 0.4,
2692                pointRadius: 0,
2693                pointHitRadius: 10,
2694                yAxisID: 'y1',
2695                order: 1
2696            }
2697        ];
2698
2699        if (rainChart) {
2700            rainChart.data.labels = labels;
2701            rainChart.data.datasets = datasets;
2702            rainChart.update('none');
2703        } else {
2704            rainChart = new Chart(canvas.getContext('2d'), {
2705                type: 'bar',
2706                data: { labels: labels, datasets: datasets },
2707                options: {
2708                    responsive: true,
2709                    maintainAspectRatio: false,
2710                    interaction: { mode: 'index', intersect: false },
2711                    plugins: {
2712                        legend: { display: true, labels: { color: tc.tickColor, font: { size: 11 } } },
2713                        tooltip: {
2714                            backgroundColor: tc.tooltipBg,
2715                            titleColor: tc.tooltipTitle,
2716                            bodyColor: tc.tooltipBody,
2717                            borderColor: tc.tooltipBorder,
2718                            borderWidth: 1,
2719                            padding: 12,
2720                            cornerRadius: 10
2721                        }
2722                    },
2723                    scales: {
2724                        x: {
2725                            grid: { color: tc.gridColor, drawBorder: false },
2726                            ticks: { color: tc.tickColor, maxTicksLimit: 12, font: { size: 11 } }
2727                        },
2728                        y: {
2729                            type: 'linear',
2730                            position: 'left',
2731                            grid: { color: tc.gridColor, drawBorder: false },
2732                            ticks: { color: tc.tickColor, font: { size: 11 } },
2733                            title: { display: true, text: 'mm/h', color: tc.tickColor },
2734                            beginAtZero: true
2735                        },
2736                        y1: {
2737                            type: 'linear',
2738                            position: 'right',
2739                            grid: { drawOnChartArea: false },
2740                            ticks: { color: '#3b82f6', font: { size: 11 } },
2741                            title: { display: true, text: 'mm', color: '#3b82f6' },
2742                            beginAtZero: true
2743                        }
2744                    }
2745                }
2746            });
2747        }
2748    }
2749
2750    function updateMarineMapTheme(theme) {
2751        if (!marineMap) return;
2752        var styleUrl = theme === 'light'
2753            ? 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'
2754            : 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';
2755        marineMap.setStyle(styleUrl);
2756        marineMap.once('styledata', function() {
2757            // Stile cambiato
2758        });
2759    }
2760
2761})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.