1(() => { 2 const ua = navigator.userAgent || ""; 3 const isMobileUA = /Android|iPhone|iPad|iPod/i.test(ua); 4 const smallScreen = Number(window.innerWidth || 0) < 720; 5 const force = (() => { 6 try { 7 const qp = new URLSearchParams(window.location.search); 8 return qp.get("inat") === "1"; 9 } catch (e) { 10 return false; 11 } 12 })(); 13 if (!force && (isMobileUA || smallScreen)) return; 14 15 const allowedEmails = new Set(["[email protected]", "[email protected]"]); 16 const startDate = "2026-01-01"; 17 18 const fmtIt = (iso) => { 19 const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso || "").trim()); 20 return m ? `${m[3]}/${m[2]}/${m[1]}` : String(iso || ""); 21 }; 22 23 const pad2 = (n) => String(n).padStart(2, "0"); 24 const localTodayIso = () => { 25 const d = new Date(); 26 return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; 27 }; 28 const addDaysLocalIso = (iso, delta) => { 29 const s = String(iso || "").trim(); 30 const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s); 31 if (!m) return ""; 32 const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), 12, 0, 0, 0); 33 d.setDate(d.getDate() + Number(delta || 0)); 34 return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; 35 }; 36 const capIso = () => addDaysLocalIso(localTodayIso(), -1); 37 const isImportableDay = (dayIso) => { 38 const d = String(dayIso || "").trim(); 39 const cap = capIso(); 40 return /^\d{4}-\d{2}-\d{2}$/.test(d) && /^\d{4}-\d{2}-\d{2}$/.test(cap) && d <= cap; 41 }; 42 43 const sleep = (ms) => new Promise((r) => setTimeout(r, Math.max(0, Number(ms) || 0))); 44 45 async function waitForAuthUser(timeoutMs = 20000) { 46 const start = Date.now(); 47 while (Date.now() - start < timeoutMs) { 48 const auth = window.firebaseAuth || null; 49 const u = auth && auth.currentUser ? auth.currentUser : null; 50 if (u && u.email) return u; 51 await sleep(250); 52 } 53 return null; 54 } 55 56 async function authedFetch(url, init) { 57 const auth = window.firebaseAuth || null; 58 const user = auth && auth.currentUser ? auth.currentUser : null; 59 if (!user || typeof user.getIdToken !== "function") throw new Error("not_logged"); 60 const token = await user.getIdToken(); 61 if (!token) throw new Error("missing_token"); 62 const headers = Object.assign({}, (init && init.headers) || {}, { Authorization: `Bearer ${token}` }); 63 return fetch(url, Object.assign({}, init || {}, { headers })); 64 } 65 66 function ensurePanel() { 67 const existing = document.getElementById("owner-inat-status"); 68 if (existing) return existing; 69 70 const box = document.createElement("div"); 71 box.id = "owner-inat-status"; 72 box.style.position = "fixed"; 73 box.style.top = "86px"; 74 box.style.right = "12px"; 75 box.style.width = "360px"; 76 box.style.maxWidth = "calc(100vw - 24px)"; 77 box.style.zIndex = "999999"; 78 box.style.background = "#ffffff"; 79 box.style.border = "1px solid rgba(203,213,225,0.9)"; 80 box.style.borderRadius = "14px"; 81 box.style.boxShadow = "0 12px 30px rgba(15,23,42,0.12)"; 82 box.style.padding = "10px"; 83 box.style.fontFamily = "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial"; 84 85 const header = document.createElement("div"); 86 header.style.display = "flex"; 87 header.style.alignItems = "center"; 88 header.style.justifyContent = "space-between"; 89 header.style.gap = "10px"; 90 91 const title = document.createElement("div"); 92 title.textContent = "Stato iNat"; 93 title.style.fontWeight = "950"; 94 title.style.color = "#0f172a"; 95 96 const close = document.createElement("button"); 97 close.type = "button"; 98 close.textContent = "â"; 99 close.style.border = "1px solid rgba(203,213,225,0.9)"; 100 close.style.background = "#ffffff"; 101 close.style.borderRadius = "10px"; 102 close.style.padding = "4px 8px"; 103 close.style.cursor = "pointer"; 104 close.onclick = () => { try { box.remove(); } catch (e) {} }; 105 106 header.appendChild(title); 107 header.appendChild(close); 108 109 const controls = document.createElement("div"); 110 controls.style.display = "flex"; 111 controls.style.flexWrap = "wrap"; 112 controls.style.gap = "8px"; 113 controls.style.marginTop = "10px"; 114 115 const dateInput = document.createElement("input"); 116 dateInput.type = "date"; 117 dateInput.id = "owner-inat-verify-date"; 118 dateInput.value = capIso() || localTodayIso(); 119 dateInput.max = capIso() || ""; 120 dateInput.style.flex = "1"; 121 dateInput.style.minWidth = "140px"; 122 dateInput.style.border = "1px solid rgba(203,213,225,0.9)"; 123 dateInput.style.borderRadius = "10px"; 124 dateInput.style.padding = "6px 10px"; 125 dateInput.style.fontWeight = "900"; 126 127 const mkBtn = (label, bg, color) => { 128 const b = document.createElement("button"); 129 b.type = "button"; 130 b.textContent = label; 131 b.style.border = "1px solid rgba(203,213,225,0.9)"; 132 b.style.borderRadius = "10px"; 133 b.style.padding = "6px 10px"; 134 b.style.background = bg; 135 b.style.color = color; 136 b.style.fontWeight = "950"; 137 b.style.cursor = "pointer"; 138 return b; 139 }; 140 141 const btnVerify = mkBtn("Verifica", "#ffffff", "#0f172a"); 142 const btnFast = mkBtn("Check veloce", "#ffffff", "#0f172a"); 143 const btnFix = mkBtn("Scarica ed aggiorna", "#1e3a8a", "#ffffff"); 144 btnFix.disabled = true; 145 btnFix.style.opacity = "0.55"; 146 const btnAlign = mkBtn("Allinea stato", "#eff6ff", "#1e3a8a"); 147 btnAlign.style.display = "none"; 148 149 controls.appendChild(dateInput); 150 controls.appendChild(btnVerify); 151 controls.appendChild(btnFast); 152 controls.appendChild(btnFix); 153 controls.appendChild(btnAlign); 154 155 const body = document.createElement("div"); 156 body.id = "owner-inat-status-body"; 157 body.style.marginTop = "10px"; 158 body.style.fontSize = "13px"; 159 body.style.color = "#0f172a"; 160 body.style.fontWeight = "850"; 161 162 box.appendChild(header); 163 box.appendChild(controls); 164 box.appendChild(body); 165 166 document.body.appendChild(box); 167 return box; 168 } 169 170 function readVerifyDate() { 171 const el = document.getElementById("owner-inat-verify-date"); 172 const v = String(el && el.value || "").trim(); 173 return /^\d{4}-\d{2}-\d{2}$/.test(v) ? v : ""; 174 } 175 176 async function fetchStatus(dayIso) { 177 const day = String(dayIso || "").trim(); 178 const url = new URL("/.netlify/functions/inat-status", window.location.origin); 179 if (day) url.searchParams.set("date", day); 180 const res = await authedFetch(url.toString(), { method: "GET" }); 181 const j = await res.json().catch(() => null); 182 if (!j || j.ok !== true) throw new Error(j && j.error ? String(j.error) : "status_failed"); 183 return j; 184 } 185 186 async function auditMissing(d2) { 187 const to = String(d2 || "").trim(); 188 const url = new URL("/.netlify/functions/inat-audit", window.location.origin); 189 url.searchParams.set("d1", startDate); 190 url.searchParams.set("d2", to); 191 url.searchParams.set("recentDays", "21"); 192 const res = await authedFetch(url.toString(), { method: "GET" }); 193 const j = await res.json().catch(() => null); 194 if (!j || j.ok !== true) throw new Error(j && j.error ? String(j.error) : "audit_failed"); 195 return Array.isArray(j.missingDays) ? j.missingDays : []; 196 } 197 198 async function repairSince() { 199 const url = new URL("/.netlify/functions/inat-status", window.location.origin); 200 url.searchParams.set("action", "repair_since"); 201 const res = await authedFetch(url.toString(), { method: "POST" }); 202 const j = await res.json().catch(() => null); 203 if (!j || j.ok !== true) throw new Error(j && j.error ? String(j.error) : "repair_failed"); 204 return true; 205 } 206 207 async function syncDay(dayIso) { 208 const day = String(dayIso || "").trim(); 209 const url = new URL("/.netlify/functions/sync-inaturalist", window.location.origin); 210 url.searchParams.set("since", day); 211 url.searchParams.set("until", day); 212 url.searchParams.set("maxWrites", "2500"); 213 url.searchParams.set("perPage", "200"); 214 url.searchParams.set("maxTotal", "2500"); 215 const res = await authedFetch(url.toString(), { method: "GET" }); 216 const j = await res.json().catch(() => null); 217 if (!j || j.ok !== true) throw new Error(j && j.error ? String(j.error) : "sync_failed"); 218 return j; 219 } 220
221 function renderStatus(payload, verifyDate) { 222 const body = document.getElementById("owner-inat-status-body"); 223 if (!body) return; 224 225 const lastSaved = String(payload.lastSaved || "").trim(); 226 const lastSavedCount = typeof payload.lastSavedCount === "number" ? payload.lastSavedCount : null; 227 const lastRemote = typeof payload.lastSavedRemoteCount === "number" ? payload.lastSavedRemoteCount : null; 228 const lastSavedInFuture = payload.lastSavedInFuture === true; 229 const sinceDateInFuture = payload.sinceDateInFuture === true; 230 const todayLocal = String(payload.todayLocal || "").trim(); 231 232 const lastStatusOk = (!lastSavedInFuture && lastSavedCount !== null && lastRemote !== null) ? (lastSavedCount === lastRemote) : null; 233 const lastIcon = lastSavedInFuture ? "â" : (lastStatusOk === true ? "â " : (lastStatusOk === false ? "â" : "")); 234 235 const rows = []; 236 rows.push(`<div style="font-weight:950;">${lastIcon ? `${lastIcon} ` : ""}Ultimo giorno salvato: <span style="color:#1e293b;">${lastSaved || "â"}</span>${lastSavedInFuture ? ` <span style="color:#b91c1c;">(stato nel futuro)</span>` : ""}</div>`); 237 if (todayLocal) rows.push(`<div style="margin-top:4px; color:#64748b; font-weight:850;">Oggi (locale): ${todayLocal}</div>`); 238 rows.push(`<div style="margin-top:6px;"><span style="font-weight:900;">Osservazioni iNat (ultimo giorno):</span> ${lastRemote === null ? "â" : String(lastRemote)} · <span style="font-weight:900;">Salvate in GuardaMI:</span> ${lastSavedCount === null ? "â" : String(lastSavedCount)}</div>`); 239 240 const btnAlign = document.getElementById("btn-owner-inat-align"); 241 if (btnAlign) btnAlign.style.display = (sinceDateInFuture || lastSavedInFuture) ? "inline-block" : "none"; 242 243 const vDate = String(verifyDate || payload.verifyDate || "").trim(); 244 const vc = typeof payload.verifyCount === "number" ? payload.verifyCount : null; 245 const vr = typeof payload.verifyRemoteCount === "number" ? payload.verifyRemoteCount : null; 246 if (/^\d{4}-\d{2}-\d{2}$/.test(vDate)) { 247 const importable = isImportableDay(vDate); 248 let icon = ""; 249 let bg = "#eff6ff"; 250 let border = "#bfdbfe"; 251 let verdict = ""; 252 if (vc !== null && vr !== null) { 253 if (vc === vr) { 254 icon = "â "; 255 verdict = "OK: tutti i dati risultano salvati."; 256 bg = "#ecfdf5"; 257 border = "#bbf7d0"; 258 } else { 259 icon = "â"; 260 verdict = `PROBLEMA: su iNat ${vr}, in GuardaMI ${vc}`; 261 bg = "#fef2f2"; 262 border = "#fecaca"; 263 } 264 } else { 265 icon = "â"; 266 verdict = "Impossibile verificare."; 267 bg = "#fff7ed"; 268 border = "#fed7aa"; 269 } 270 if (!importable) { 271 verdict = `Oggi/non importabile. Seleziona un giorno fino a ${fmtIt(capIso())}.`; 272 bg = "#fff7ed"; 273 border = "#fed7aa"; 274 } 275 rows.push(`<div style="margin-top:10px; padding:10px; border-radius:12px; background:${bg}; border:1px solid ${border};">` + 276 `<div style="font-weight:950;">${icon} Verifica giorno ${fmtIt(vDate)}</div>` + 277 `<div style="margin-top:6px;">Osservazioni iNat: ${vr === null ? "â" : String(vr)}</div>` + 278 `<div style="margin-top:2px;">Salvate in GuardaMI: ${vc === null ? "â" : String(vc)}</div>` + 279 `<div style="margin-top:8px; font-weight:950;">${verdict}</div>` + 280 `${(importable && vc !== null && vr !== null && vc < vr) ? `<button id="owner-inat-reimport-day" type="button" data-day="${vDate}" style="margin-top:10px; width:100%; padding:9px 12px; border-radius:999px; border:1px solid #fecaca; background:#fff7ed; color:#7c2d12; font-weight:950; cursor:pointer; font-size:12px;">Re-importa questo giorno</button>` : ""}` + 281 `</div>`); 282 } 283 284 body.innerHTML = rows.join(""); 285 const reBtn = document.getElementById("owner-inat-reimport-day"); 286 if (reBtn && !reBtn.__bound) { 287 reBtn.__bound = true; 288 reBtn.addEventListener("click", async () => { 289 const d = String(reBtn.getAttribute("data-day") || "").trim(); 290 if (!/^\d{4}-\d{2}-\d{2}$/.test(d)) return; 291 reBtn.disabled = true; 292 reBtn.style.opacity = "0.6"; 293 try { 294 const bodyEl = document.getElementById("owner-inat-status-body"); 295 if (bodyEl) bodyEl.textContent = `Re-importazione ${fmtIt(d)}â¦`; 296 await syncDay(d); 297 try { await repairSince(); } catch (e) {} 298 const refreshed = await fetchStatus(d); 299 renderStatus(refreshed, d); 300 } catch (e) { 301 const bodyEl = document.getElementById("owner-inat-status-body"); 302 if (bodyEl) bodyEl.textContent = `Errore re-import (${e && e.message ? e.message :
302String(e)})`; 303 } finally { 304 reBtn.disabled = false; 305 reBtn.style.opacity = "1"; 306 } 307 }); 308 } 309 } 310 311 async function boot() { 312 const user = await waitForAuthUser(20000); 313 if (!user) return; 314 const email = String(user.email || "").trim().toLowerCase(); 315 if (!allowedEmails.has(email)) return; 316 317 ensurePanel(); 318 319 const dateEl = document.getElementById("owner-inat-verify-date"); 320 const btnAlign = document.getElementById("btn-owner-inat-align"); 321 const btnVerify = Array.from(document.querySelectorAll("#owner-inat-status button")).find((b) => b.textContent === "Verifica") || null; 322 const btnFast = Array.from(document.querySelectorAll("#owner-inat-status button")).find((b) => b.textContent === "Check veloce") || null; 323 const btnFix = Array.from(document.querySelectorAll("#owner-inat-status button")).find((b) => b.textContent === "Scarica ed aggiorna") || null; 324 325 let missingDays = []; 326 327 const load = async (day) => { 328 const payload = await fetchStatus(day); 329 renderStatus(payload, day); 330 }; 331 332 if (btnVerify) btnVerify.onclick = async () => { 333 try { 334 const d = readVerifyDate() || todayIso(); 335 await load(d); 336 } catch (e) {} 337 }; 338 if (dateEl) dateEl.onchange = async () => { 339 try { 340 const d = readVerifyDate() || todayIso(); 341 await load(d); 342 } catch (e) {} 343 }; 344 if (btnAlign) btnAlign.onclick = async () => { 345 const body = document.getElementById("owner-inat-status-body"); 346 if (body) body.textContent = "Allineamento statoâ¦"; 347 try { await repairSince(); } catch (e) {} 348 try { await load(readVerifyDate() || todayIso()); } catch (e) {} 349 }; 350 351 if (btnFast) btnFast.onclick = async () => { 352 const body = document.getElementById("owner-inat-status-body"); 353 const to = readVerifyDate() || capIso() || localTodayIso(); 354 const cappedTo = isImportableDay(to) ? to : (capIso() || to); 355 if (to < startDate) return; 356 if (body) body.textContent = "Check veloce in corsoâ¦"; 357 if (btnFix) { 358 btnFix.disabled = true; 359 btnFix.style.opacity = "0.55"; 360 } 361 missingDays = [];
362 try { 363 const list = await auditMissing(cappedTo); 364 missingDays = list.map((x) => String(x && x.date || "").trim()).filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d)); 365 if (!missingDays.length) { 366 if (body) body.innerHTML = `<div style=\"font-weight:950; color:#065f46;\">â OK: nessun dato mancante dal ${fmtIt(startDate)} al ${fmtIt(cappedTo)}.</div>`; 367 } else { 368 if (body) { 369 body.innerHTML = 370 `<div style=\"font-weight:950; color:#991b1b;\">â Mancano dati in ${missingDays.length} giorni.</div>` + 371 `<div style=\"margin-top:6px; color:#0f172a; font-weight:900;\">Date:</div>` + 372 `<div style=\"margin-top:4px; color:#334155; font-weight:800; line-height:1.4;\">${missingDays.map(fmtIt).join(" · ")}</div>` + 373 `<div style=\"margin-top:6px; color:#64748b; font-weight:800;\">Clicca âScarica ed aggiornaâ per recuperare automaticamente.</div>`; 374 } 375 if (btnFix) { 376 btnFix.disabled = false; 377 btnFix.style.opacity = "1"; 378 } 379 } 380 } catch (e) { 381 if (body) body.textContent = `Errore check veloce (${e && e.message ? e.message : String(e)})`; 382 } 383 }; 384 385 if (btnFix) btnFix.onclick = async () => { 386 if (!missingDays.length) return; 387 const body = document.getElementById("owner-inat-status-body"); 388 btnFix.disabled = true; 389 btnFix.style.opacity = "0.65"; 390 try { 391 for (let i = 0; i < missingDays.length; i++) { 392 const d = missingDays[i]; 393 if (body) body.textContent = `Scarico ${fmtIt(d)} (${i + 1}/${missingDays.length})â¦`; 394 await syncDay(d); 395 await sleep(900); 396 } 397 try { await repairSince(); } catch (e) {} 398 missingDays = []; 399 if (body) body.textContent = "Aggiornamento completato. Rifaccio checkâ¦"; 400 await sleep(400); 401 if (btnFast) await btnFast.onclick(); 402 } catch (e) { 403 if (body) body.textContent = `Errore scarica/aggiorna (${e && e.message ? e.message : String(e)})`; 404 } finally { 405 btnFix.disabled = true; 406 btnFix.style.opacity = "0.55"; 407 } 408 }; 409 410 try { 411 const d = readVerifyDate() || todayIso(); 412 await load(d); 413 } catch (e) {} 414 } 415 416 if (document.readyState === "loading") { 417 document.addEventListener("DOMContentLoaded", boot); 418 } else { 419 boot(); 420 } 421})();
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.