Piattaforma serverless per il tracciamento delle catture di pesca con arricchimento geo-meteorologico automatico da 7+ API pubbliche in tempo reale
Quando un pescatore cattura un pesce, le condizioni ambientali โ meteo, pressione barometrica, fase lunare, livello dell'acqua โ sono cruciali per capire perchรฉ il pesce ha abboccato. GeoData acquisisce automaticamente tutti questi dati dal GPS del telefono, senza alcun input manuale.
Il sistema aggrega dati da 7+ API pubbliche gratuite in parallelo tramite Promise.allSettled. Se un provider รจ down, gli altri compensano. Un indicatore data_quality (0-100) traccia la completezza dei dati per ogni cattura.
Temperatura, umiditร , pressione, trend barometrico, vento (velocitร + direzione + raffica), copertura nuvolosa, precipitazioni, UV index
Reverse geocoding completo via Nominatim: cittร , provincia, via, indirizzo formattato, geohash a 7 caratteri per query di prossimitร
Fase lunare (8 fasi), illuminazione %, periodi solunari major/minor, day rating 1-5, transiti Moon Overhead/Underfoot
Fiumi, laghi, canali, stagni nel raggio di 10km via Overpass API (OpenStreetMap). Dighe nel raggio di 50km con altezza e materiale
Indice Kp planetario 0-9 da NOAA SWPC. Campo magnetico terrestre calmo (Kpโค2) = ottimo per la pesca
Dati real-time dalla rete AEGIS Regione Lazio โ livello fiume in metri, trend, stazione piรน vicina con distanza
Orario alba/tramonto, flag giorno/notte, bonus twilight calcolato per la "golden hour" di pesca
Predizioni maree via Open Tide API (modello TPXO9-v5), altezze e tempi delle prossime 24 ore
Temperatura acqua stimata (con lag stagionale), delta temperatura 24h, fishing score composito 0-100 su 8 fattori
Hono v4 โ routing, middleware CORS, security headers built-insrc/worker.js โ esporta l'app Hono come export default appwrangler dev --local con D1/R2 simulati localmenteDB diretto nel worker contextrepository.js โ tutte le query passano dal DAL.bind() per parametri, mai concatenazione SQLPHOTOScatches.js via c.env.PHOTOS.put()catches/{catchId}/{timestamp}-{filename}web/ viene servita come CDN globale Cloudflare[assets] directory = "./web" in wrangler.tomlweb/ โ nessuna dipendenza esterna a runtimehono (runtime) e wrangler (dev/deploy). Zero bundler, zero framework frontend, zero ORM. Tutto vanilla JavaScript.
geo-data.js tramite Promise.allSettled per massima resilienza.| Provider | Modulo | Dati Forniti | Rate Limit | Timeout | Fallback |
|---|---|---|---|---|---|
| Open-Meteo | open-meteo.js | Temp, pressione, vento, umiditร , UV, precipitazioni, temp ieri per delta | 10K/giorno | 8s | โ Yr.no |
| Yr.no (MET Norway) | yr-no.js | Stessi dati meteo, formato MET Norway compact | User-Agent req. | 8s | Ultimo nella catena |
| Nominatim (OSM) | location.js | Reverse geocoding: cittร , provincia, via, indirizzo formattato | 1 req/sec! | 10s | โ |
| Overpass API (OSM) | location.js | Corpi d'acqua vicini (fiumi, laghi, canali) + dighe con altezza | ~10K/giorno | 15s | Mirror FR |
| NOAA SWPC | scientific.js | Indice geomagnetico planetario Kp (0-9) | Free | 5s | โ null |
| AEGIS Regione Lazio | water-level.js | Livello idrometrico real-time, trend, stazione, distanza | Free (OAuth2) | 10s | โ skip |
| Open Tide API | location.js | Predizioni maree (TPXO9-v5), altezze, tempi prossime 24h | Free | 8s | โ skip |
Il modulo weather-manager.js implementa un pattern waterfall iterativo: prova i provider nell'ordine configurato in geo.json. Se il primo fallisce, passa al successivo automaticamente.
API meteo gratuita con forecast corrente e dati storici dal 1940. Fornisce temperature_2m_mean del giorno precedente per il calcolo delta temperatura 24h. Include dati orari per il calcolo del trend barometrico su 6 ore.
Se Open-Meteo รจ down, timeout o rate-limited, il manager tenta Yr.no. Richiede User-Agent obbligatorio. Non fornisce delta temp 24h (campo โ null nel frontend) nรฉ dati storici.
Se tutti i provider meteo falliscono, il campo weather รจ null ma la cattura viene comunque salvata con tutti gli altri dati (lunar, location, water). Il campo data_quality riflette il degrado (es. 85% invece di 100%).
AegisPubblico / AegisPubblico (pubbliche, embedded nel JS del portale)POST /connect/token โ OAuth2 tokenGET /v3/locations โ Lista stazioni con coordinateGET /v3/elements โ Letture sensori livehwl: true (ha sensore idrometrico)elementName === 'Water Level' o contiene "idrometro"/"livello"La cache in-memory usa un lookup spaziale Haversine: se una richiesta arriva da coordinate entro 100m di una precedente, restituisce il dato dalla cache. TTL 30 minuti, cap a 100 entry con eviction FIFO (oldest key). Si resetta al restart del worker isolate โ accettabile perchรฉ Workers sono stateless by design.
fishing-score.js con base 50 e fattori additivi/sottrattivi.| Fattore | Peso | Logica | Fondamento Scientifico |
|---|---|---|---|
| Pressione Barometrica | ยฑ20 | Rising fast +20 ยท Rising +10 ยท Stable +5 ยท Falling -5 ยท Fast -15 | Variazione pressione altera la vescica natatoria |
| Fase Lunare | ยฑ15 | New/Full moon +15 ยท Near new/full +5 | Gravitร lunare influenza maree e attivitร |
| Periodo Solunar | ยฑ15 | Major (overhead/underfoot) +15 ยท Minor (rise/set) +8 | Teoria di J.A. Knight โ transit lunari = picchi |
| Condizioni Meteo | ยฑ10 | Nuvolosoโฅ60% +5 ยท Vento leggero +3 ยท Pioggia leggera +3 ยท Pioggia -10 | Luce ridotta attiva i predatori |
| Indice Kp | ยฑ8 | Calmo โค2 +8 ยท Moderato โค4 +3 ยท Tempesta โฅ6 -8 | Campo geomagnetico influenza sensori laterali |
| Crepuscolo | +10 | Golden hour alba/tramonto, bonus proporzionale alla vicinanza | Transizione luce = picco attivitร predatoria |
| Delta Temp 24h | ยฑ5 | Stabile <3ยฐC +5 ยท Rapido >8ยฐC -5 | Pesci ectotermici sensibili a shock termici |
| Temp Acqua Stimata | ยฑ5 | Ottimale 15-22ยฐC +5 ยท Buona 10-25ยฐC +2 ยท Estrema -5 | Range metabolico ideale delle specie |
Tutte le query DB passano da repository.js. Le route non fanno mai query dirette. Il DAL espone funzioni tipizzate con parametri bound:
storeCatch(db, geoData, catchInfo, sessionId) โ INSERT con 55 bind parametersgetCatch(db, id) โ SELECT + JOIN photos e tagsgetAllCatches(db, offset, limit) โ Paginated con LEFT JOIN first photoupdateCatch(db, id, updates) โ UPDATE con whitelist + regex /^[a-z_][a-z0-9_]*$/findNearbyCatches(db, lat, lon, radius) โ Bounding box query + Haversine post-filtergetSpeciesAnalytics(db, species) โ AVG di score, temp, pressione, umiditร , ventogetLocationStats(db, lat, lon, radius) โ Top 5 species + avg score per areacreateLocation(db, data) / updateLocation / deleteLocation โ CRUD tabella locationsfindMatchingLocation(db, lat, lon) โ โ
Geo-fencing: Haversine vs radius_m, ritorna il luogo piรน vicinoeraseAllData(db) โ DELETE FROM tutte le tabelle + reset sqlite_sequenceCREATE TABLE IF NOT EXISTS (per nuove installazioni). Le migration usano ALTER TABLE ADD COLUMN (per DB esistenti). Entrambi devono essere sincronizzati โ ogni nuova colonna va aggiunta in entrambi.
a4d808b). Remediation in 7 fasi completata a Maggio 2026 (tag v1.1.0-security) โ 135 test automatizzati coprono tutte le fix. Audit completo in SECURITY_AUDIT.md.X-API-Key su tutti gli endpoint /api/*GET /api/photos/* pubblico โ i tag <img> non possono inviare header customwrangler secret put API_KEY)/api/admin/* con timingSafeEqualCache-Control: private, no-store su tutte le risposte API autenticatedefault-src 'self' โ zero CDN esterni. Admin UI usa nonce per-request su scriptSrc.max-age=31536000; includeSubDomains| Dove | Cosa | Come |
|---|---|---|
catches.js POST/PUT | Coordinate range, string lengths | lat: -90..90, lon: -180..180, strings max 200/100/2000 |
catches.js photo upload | File size, MIME type, magic bytes | Max 10MB, whitelist jpeg/png/webp/heic, magic bytes via image-validation.js |
admin.js import | Limite righe, lat/lon per riga | Max 10.000 righe (413), range check per ogni record |
admin.js restore foto | R2 key, size, magic bytes | Regex ^catches/\d+/[A-Za-z0-9._-]+$, max 10MB, magic bytes |
admin.js backup/restore | Conferma esplicita | ?confirm=RESTORE obbligatorio; snapshot rollback in R2 prima dell'operazione |
tags.js POST | Name, color format | Name required max 100 chars, color regex #RRGGBB |
photos.js GET | Path traversal, chiavi riservate | Block .., prefix check catches/, blocco _rollback/ (403) |
repository.js update | Column name injection | Regex /^[a-z_][a-z0-9_]*$/ + allowedFields whitelist |
repository.js analytics | LIKE wildcard escape | % e _ rimossi dall'input |
zip.js restore | ZIP malformati, path traversal | Firma local header, bounds check, EOCD limitato a 64KB |
geo.js | Crash provider esterni | Promise.allSettled + try/catch โ sempre 200, partial: true |
app.js frontend | XSS output encoding | escapeHtml() con 5 entity su tutti gli output utente |
worker.js global | Error leak prevention | app.onError() + safeError() โ mai stack trace al client |
admin.js import | Timing oracle | Confronto admin password con timingSafeEqual โ no timing leak |
http://localhost:8787/api (dev) o https://geodata-fishing.*.workers.dev/api (prod).| Endpoint | Metodi | Auth | Descrizione |
|---|---|---|---|
/api/health | GET | ๐ | Health check con contatori (catches, sessions, photos, tags) |
/api/stats | GET | ๐ | Statistiche aggregate (species count, locations count) |
/api/catches | GET POST | ๐ | Lista paginate (offset/limit) ยท Crea cattura dal GPS |
/api/catches/:id | GET PUT DEL | ๐ | CRUD singola cattura con photos e tags |
/api/catches/:id/photos | GET POST | ๐ | Lista + Upload foto (multipart o base64) |
/api/catches/:id/tags/:tagId | POST DEL | ๐ | Associa/rimuovi tag da cattura |
/api/catches/nearby | GET | ๐ | Ricerca geospaziale per raggio (lat, lon, radius km) |
/api/sessions | GET POST | ๐ | Lista + Crea sessioni di pesca |
/api/sessions/:id | GET DEL | ๐ | Dettaglio sessione con catture associate |
/api/sessions/:id/end | PUT | ๐ | Chiudi sessione con note finali |
/api/tags | GET POST | ๐ | CRUD tag (name UNIQUE + color hex) |
/api/tags/:id | DEL | ๐ | Elimina tag (CASCADE su associazioni) |
/api/geo-data | GET | ๐ | Dati geo-meteo live, integrato con location override |
/api/analytics/species/:name | GET | ๐ | AVG score/temp/pressure/wind per specie |
/api/analytics/location | GET | ๐ | Top 5 species + avg score per area geografica |
/api/photos/* | GET | ๐ | Serve foto da R2 (pubblico โ per tag <img>) |
| ๐ ADMIN โ Basic Auth (admin:<ADMIN_PASSWORD>) | |||
/api/admin | GET | ๐ | Pannello admin HTML (UI import/export/backup/luoghi/erase) |
/api/admin/import | POST | ๐ | Import bulk catture da Excel con coordinate default |
/api/admin/export | GET | ๐ | Export tutte le catture come JSON |
/api/admin/backup | GET | ๐ | โ Genera e scarica backup completo ZIP (DB + foto R2) |
/api/admin/backup | POST | ๐ | โ Ripristina da backup ZIP โ atomic erase + restore. Max 50MB |
/api/admin/catches | GET | ๐ | โ Records browser paginato โ lista catture con filtri |
/api/admin/catches/:id | GET PUT DEL | ๐ | โ Modifica/elimina singola cattura dall'admin |
/api/admin/catches/:id/re-enrich | POST | ๐ | โ Ri-arricchisce geo-meteo una cattura esistente |
/api/admin/locations | GET POST | ๐ | CRUD geo-fencing: lista + crea luogo (centro + raggio) |
/api/admin/locations/:id | PUT DEL | ๐ | Aggiorna/elimina luogo configurato |
/api/admin/erase-all | POST | ๐ | Reset completo DB. Richiede { confirm: "ERASE ALL" } |
#/catch/{id}) e views modulariweb/js/vendor/ e web/css/vendor/. Zero CDN esterni a runtimelocalStorage namespaced sotto geodata.* via utils/storage.js (low-level) + utils/persistence.js (domain accessors con sanitizzazione). Persisti: capture draft completo (tutti gli input + tag, esclusi foto/override), filtri Galleria, filtri Stats, API key, angler name, geo permission, grafici customCamera rear con preview live, cattura foto, form con autocomplete (21 esche, 29 specie), GPS lock, geo-data preview card, salvataggio con upload foto simultaneo. Upload da galleria con lettura automatica EXIF (GPS + data/ora) e dialog di conferma se i dati EXIF differiscono da quelli correnti. Override luogo/data tramite bottom-sheet unificato (geo-edit-sheet.js) con <dialog> nativo, swipe-to-dismiss, haptics e visualViewport tracking
Grid responsive con thumbnail da R2, ricerca per specie/luogo, filtro temporale (oggi/settimana). Tap โ modal dettaglio con SVG score gauge, mappa Leaflet, 6 sezioni dati
Contatori aggregati, top 5 specie, grafici avanzati (timeseries, scatter, doughnut, bar) via Chart.js wrapper, grafici personalizzabili dall'utente
| File | Righe | Responsabilitร |
|---|---|---|
app.js | ~110 | EntryPoint: init SPA, check intro video, event listener router globali |
state.js, constants.js | ~200 | Definizione degli stati globali e query selectors/options costanti |
views/* | ~600/ea | Moduli vista specifici: capture.js, gallery.js, stats.js separati per lazy logic |
components/* | var | UI isolati: toast.js, modal.js, photo-viewer.js, autocomplete.js, exif-confirm.js, location-picker.js, geo-edit-sheet.js |
api.js | ~156 | Client HTTP con X-API-Key, upload foto FormData/base64 |
chart-builder.js | ~340 | Chart.js wrapper: line, scatter, doughnut, bar. Config in localStorage |
utils/router.js | ~76 | Hash route parsing (pure functions, zero DOM, testabile) |
utils/geo.js | ~95 | Geolocation iOS-safe, permission tracking via localStorage |
utils/image.js, helpers.js | ~100 | Compressione immagini e DOM helpers condivisi |
utils/storage.js | ~90 | localStorage helpers namespaced (geodata.*), JSON/string round-trip, try/catch wrapped (private-mode/quota safe) |
utils/persistence.js | ~95 | Domain accessors: loadCaptureDraft/save/clear, load/saveGalleryFilters, load/saveStatsFilters. Sanitizza shape al load (corrupt JSON โ fallback) |
#0a0e1argba(15,23,42,.7) + blur<dialog>) con swipe-to-dismissdvh/dvwprefers-reduced-motion globaleapi.js)fetch() con header X-API-Key iniettato automaticamente. Key letta da <meta name="api-key"> (SSR) o localStorage (fallback). Upload foto supporta sia FormData (multipart) che JSON (base64).
#...) non viene mai inviato al server โ zero modifiche backend, massima sicurezza.| Hash | Risultato |
|---|---|
#/capture | Vista Cattura |
#/gallery | Vista Galleria |
#/stats | Vista Statistiche |
#/catch/{id} | Deep link โ gallery + modal dettaglio cattura |
| (vuoto o invalido) | Fallback sicuro โ vista Cattura |
Logica di parsing: parseRoute(hash), catchHash(id), viewHash(view). Zero dipendenze browser โ testabile con bun test.
parseHash() chiama parseRoute(location.hash). setHash() usa history.replaceState โ non inquina la history. init() gestisce route iniziale + listener hashchange.
Share button include location.origin + /#/catch/{id} via navigator.share(). Deep link carica gallery, poi apre modal. Cattura inesistente โ toast errore + fallback.
Il fragment hash non viene inviato al server โ la key resta nel header HTTP X-API-Key
Regex strict /^catch\/(\d+)$/, range 1..2ยณยน-1 (SQLite INTEGER safe)
Solo capture, gallery, stats accettati. Qualsiasi hash invalido โ fallback sicuro a capture
setHash() usa replaceState โ non pushState. Il click su una cattura NON crea entry nella browser history. Solo hashchange (back/forward) naviga via listener.closeModal() ripristina il hash alla vista corrente (#/gallery). Senza questo, il back button riaprirebbe il modal indefinitamente.#/catch/{id} carica prima la gallery, poi apre il modal. Se la cattura non esiste โ toast errore + fallback a #/gallery.router.js รจ nel precache manifest (sw.js). Se aggiungi un nuovo file JS in utils/, aggiungerlo anche lรฌ.| Strategia | Target | TTL / Cap |
|---|---|---|
| Precache | App shell: HTML, CSS, JS, manifest, icone, font, vendor libs (tutto self-hosted) | Versioned (APP_VERSION) |
| CacheFirst | Foto da R2 (/api/photos/*) | 7gg / 100 foto |
| NetworkOnly | Chiamate API (/api/* escluse foto) | Mai cachate โ dati live |
eb2ebb0, Leaflet, Chart.js, Workbox e il font Inter sono tutti self-hosted in web/js/vendor/ e web/assets/fonts/. La strategia StaleWhileRevalidate per Google Fonts รจ stata rimossa โ zero dipendenze CDN a runtime.bump-sw.mjs genera APP_VERSION = 'YYYY.MM.DD.HHmmss' e sovrascrive la costante in sw.js. Ogni entry nel precache usa questa versione come revision.
npm run deploy = bump-sw.mjs + wrangler deploy. Il browser rileva il diff di byte nel SW e forza l'installazione della nuova versione.
skipWaiting() + clients.claim() attivano il nuovo SW immediatamente. Evento controllerchange โ toast persistente "๐ Aggiornamento disponibile" con bottone Aggiorna. Il reload avviene solo su click utente โ nessun interrupt in uso.
maxTouchPoints โฅ 5 e pointer: coarseMacIntel senza touch coarse = desktop โ bottone nascostoapple-mobile-web-app-capable, apple-touch-icon, status bar black-translucentbeforeinstallprompt โ mostra bottone "Installa" nel headerdeferredPrompt.prompt() โ dialog installazione OSappinstalled โ bottone nascosto automaticamentedisplay-mode: standalone), bottone mai visibileicon-512.png (512ร512)icon-192.png (192ร192)apple-touch-icon.png (180ร180)icon-32.png, icon-16.png#060a14#060a14| File Test | Modulo Testato | Cosa Verifica |
|---|---|---|
| FASE 1 โ BACKEND LIB (UNIT TEST) | ||
scientific-geomagnetic.test.js | scientific.js (Kp) | NOAA legacy/new format parsing, Kp impact levels, HTTP/network errors, empty arrays |
scientific.test.js | scientific.js | Water temp estimation, twilight bonus, temp delta 24h, edge cases |
weather.test.js | weather.js | Historical weather parsing, pressure trend (6h), HTTP errors, sparse data |
weather-manager.test.js | weather-manager.js | Waterfall fallback open-meteo โ yr-no, all providers fail โ throws |
weather-providers.test.js | open-meteo.js, yr-no.js | Response parsing, field mapping, fallback to Yr.no, timeout handling |
location.test.js | location.js | Nominatim parsing, tides, Overpass mirror fallback, dams metadata, sorting |
geo-data.test.js | geo-data.js | Current + historical paths, data_quality tracking, partial/total API failure |
cache.test.js | cache.js | Radial lookup, TTL expiry, FIFO eviction, historical cache, distance threshold |
water-level.test.js | water-level.js | OAuth2 flow mock, station filtering (hwl), multi-sensor, Haversine, no-hwl fallback |
fishing-score.test.js | fishing-score.js | Tutti i fattori singoli e combinati, edge cases (tutti null, tutti ottimali), range 0-100 |
lunar.test.js | lunar.js | 8 fasi lunari, illumination, solunar periods, day rating, timing precision |
utils.test.js | utils.js | Cardinal conversion, geohash, haversine, weather codes, escapeHtml |
| FASE 2 โ REPOSITORY & ROUTES (INTEGRATION) | ||
repository-crud.test.js | repository.js (DAL) | Full CRUD: storeCatch (55 params), getCatch, updateCatch, photos, tags, sessions, locations, admin, analytics |
repository-analytics.test.js | repository.js (analytics) | Species analytics queries, location stats, aggregation functions |
worker.test.js | worker.js + routes | Integration test: mock D1/R2, auth guard, CRUD, photo upload, validation, error handling |
routes.test.js | All routes (62 test) | Catches PUT/DELETE, photo upload (base64/multipart), admin CRUD, analytics, sessions, tags, CORS |
routes-deep.test.js | Uncovered paths | Catches POST create E2E, geo matched_location, admin enriched import, enrich, location override |
catches-filters.test.js | catches.js (filters) | Gallery filter queries, angler/location/date filters, pagination |
analytics-api.test.js | analytics.js route | Timeseries, distribution endpoint, pagination, error handling |
admin-api.test.js | admin.js | Auth guard (Basic Auth), import validation, export, error handling |
admin-locations.test.js | admin.js + repository.js | Locations CRUD, validation water_type, findMatchingLocation, erase all |
location-override.test.js | catches.js + repository.js | Haversine sanity, geo-fencing match/miss, GET /api/catches override E2E |
app-syntax.test.js | admin-ui.js | Template literal syntax validation, escape characters check |
| FASE 3 โ FRONTEND (MOCK DOM) | ||
helpers.test.js | utils/helpers.js | getScoreClass/Color boundaries, formatDate/DateTime, escapeHtml XSS payload |
frontend-state.test.js | state.js | Default values, state shape integrity, mutability |
frontend-constants.test.js | constants.js | BAITS/SPECIES/BAIT_BRANDS: no duplicates, expected entries, non-empty strings |
frontend-geo.test.js | utils/geo.js | formatCoords, isIOS (navigator mock), hasGrantedGeolocation (localStorage), getPosition |
frontend-image.test.js | utils/image.js | dataURLtoBlob: JPEG/PNG/WebP, binary preservation, edge cases |
chart-builder.test.js | chart-builder.js | CRUD chart config, validation, all 4 renderers (mock DOM/Chart.js), lifecycle, horizontal bar |
router.test.js | utils/router.js | Hash parsing, catch ID validation (range, XSS), view whitelist, fallback |
persistence.test.js | utils/storage.js + utils/persistence.js | localStorage helpers + domain accessors (capture draft, gallery/stats filters): round-trip, defaults, sanitize shape, corrupt JSON fallback, namespacing |
prepare().bind().run()/first()/all() + prepareFn configurabileput(), get() e delete() con Map in-memoryglobalThis.fetch mockato per ogni provider API (7 provider)document.getElementById stub per Chart.js renderingnavigator.userAgent, platform, geolocation per iOS detectionimage.js richiede Canvas/Image browser API โ non testabile in bun. Escludendolo, coverage >97%.
wrangler secret put API_KEY โ imposta l'API key nel runtime produzione. Mai in codice.
wrangler d1 execute geodata-fishing --file=src/db/schema.sql โ crea le tabelle D1.
wrangler d1 execute geodata-fishing --file=src/db/migrations/003_water_levels.sql --remote ยท 004_bait_presentation.sql ยท 005_locations.sql ยท 006_dedup_catches.sql ยท 007_fix_column_scramble.sql โ aggiunge colonne, tabelle e corregge eventuali scramble. โ ๏ธ 006 usa colonne esplicite nel INSERTโฆSELECT (mai SELECT * tra tabelle che possono divergere per ALTER TABLE).
npm run deploy โ esegue bump-sw.mjs (versiona il Service Worker) poi wrangler deploy. Worker, static assets e bindings D1/R2 in un colpo solo.
wrangler.toml.