PageSourceSearch

https://dreamy-truffle-77db6e.netlify.app/inat_admin.js?v=3

js dreamy-truffle-77db6e.netlify.app collected 2026-10-03 10:40:08 UTC 17,644 bytes, 421 lines download raw bytes

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.