Skip to content

Commit a897363

Browse files
committed
Stats: totals at the top, all-time visitors, and 34 duplicated rows removed
Three things, asked for together. TOTALS AT THE TOP. The download numbers lived halfway down the page, under eight tables. They are the figures you look at first, so they now sit in a second row of tiles right under the visits: total downloads, downloads in the last 30 days, downloads started from the site, and how many people started them. The computation is unchanged — only where it happens moved, because in PHP a variable does not exist until it has been filled and the tiles come first. The SourceForge panel further down no longer repeats the two figures that went up; it keeps the one that did not, how many of those downloads are actually ISO images. ALL-TIME VISITORS, said honestly. 3,275 since 15/06/2026 — and the page now explains what that means. The visitor id contains the DAY, which is how we stay cookieless and cannot trace anyone back, so somebody who returns tomorrow counts as somebody new. It is the sum of daily visitors, which is the most that can be said without tracking people; calling it "different people" would be false. Each total also carries its OWN start date rather than a shared one: the site has counted visits since 15/06, but SourceForge was counting downloads from 06/06, before the site existed. DUPLICATES. There were 34: 29 visits and 5 download clicks written twice with the same timestamp, path and visitor. Not a person reloading — that lands in a different second — the same event recorded twice. They were 0.4%, small but they inflate every count. Removed once by a migration that runs on the first database open, and stopped from returning by a unique index on (ts, path, vis) with INSERT OR IGNORE on both writers. ⚠️ What I did NOT delete matters more. There were also 299 pairs of hits less than two seconds apart, which looked like the same bug. They were not: they touched 1.7% of visitor-days and 76% of them came from FIVE visitors churning through pages. That is real traffic, or at worst a crawler we have not recognised, and deleting it would have pushed the numbers down for no reason. A duplicate is the same event written twice, not the same person appearing twice. Verified by running the migration against a COPY of the live database — 8143 to 8114 rows, both indexes created, a duplicate insert rejected and a genuine visit one second later still accepted — then by rendering the whole page against that copy in a throwaway PHP instance. That render is what caught the one real bug. page_head('SkillFishOS · Statistiche') appears TWICE in the file, once for the login screen and once for the dashboard, and I had inserted the new block before the first. It landed in the login screen, where the database is not open yet, and the page answered 500 to anyone trying to sign in. php -l reported no syntax errors. Only running it found it.
1 parent c06fe80 commit a897363

4 files changed

Lines changed: 199 additions & 86 deletions

File tree

website/public/_sfstats.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,45 @@ function sfstats_db() {
4949
}
5050
$db->exec('CREATE INDEX IF NOT EXISTS idx_day ON hits(day)');
5151
$db->exec('CREATE INDEX IF NOT EXISTS idx_vis ON hits(vis)');
52+
53+
// ---- doppioni: si tolgono una volta, e poi non tornano piu' --------
54+
// Nel database c'erano 29 visite e 5 clic registrati DUE VOLTE: stesso
55+
// secondo, stessa pagina, stesso visitatore. Non e' una persona che
56+
// ricarica - quella cade in un altro secondo - e' lo stesso evento
57+
// scritto due volte. Erano lo 0,4%: poco, ma gonfiano ogni conteggio.
58+
//
59+
// ⚠️ NON si toccano le visite ravvicinate ma non identiche. Ne avevamo
60+
// 299 e sembravano lo stesso guaio: non lo erano. Riguardavano l'1,7%
61+
// dei visitatori e il 76% veniva da CINQUE persone che macinavano
62+
// pagine - traffico vero, o al limite un raccoglitore che non abbiamo
63+
// riconosciuto. Cancellarle avrebbe falsato i numeri al ribasso.
64+
//
65+
// L'indice unico e' cio' che impedisce il ritorno: da qui in poi un
66+
// secondo inserimento identico viene semplicemente ignorato (le
67+
// scritture usano INSERT OR IGNORE), senza errori e senza doppioni.
68+
$fatto = $dir . '/.doppioni-tolti';
69+
if (!is_file($fatto)) {
70+
try {
71+
$db->exec('DELETE FROM hits WHERE id NOT IN
72+
(SELECT MIN(id) FROM hits GROUP BY ts, path, vis)');
73+
$db->exec('CREATE TABLE IF NOT EXISTS dl(
74+
id INTEGER PRIMARY KEY AUTOINCREMENT,
75+
ts INTEGER NOT NULL, day TEXT NOT NULL,
76+
file TEXT NOT NULL, kind TEXT NOT NULL DEFAULT "iso",
77+
vis TEXT NOT NULL, bot INTEGER NOT NULL DEFAULT 0,
78+
country TEXT NOT NULL DEFAULT "", cname TEXT NOT NULL DEFAULT "",
79+
lang TEXT NOT NULL DEFAULT "")');
80+
$db->exec('DELETE FROM dl WHERE id NOT IN
81+
(SELECT MIN(id) FROM dl GROUP BY ts, file, vis)');
82+
$db->exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_unico ON hits(ts, path, vis)');
83+
$db->exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_unico_dl ON dl(ts, file, vis)');
84+
@file_put_contents($fatto, gmdate('c') . "
85+
");
86+
} catch (Throwable $e) {
87+
// Se qualcosa va storto si riprova al prossimo giro: meglio
88+
// ritentare che lasciare il segno e non farlo mai piu'.
89+
}
90+
}
5291
return $db;
5392
}
5493

website/public/go.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@
9090
: ((strpos($f, 'sha') === 0) ? 'sha' : (($f === 'note' || $f === 'tutti') ? 'altro' : 'iso'))));
9191

9292
list($cc, $cn) = sfstats_country();
93-
$st = $db->prepare('INSERT INTO dl(ts,day,file,kind,vis,bot,country,cname,lang,ver)
93+
// INSERT OR IGNORE: stesso motivo di pulse.php - due clic registrati nello
94+
// stesso secondo sullo stesso file sono lo stesso clic scritto due volte.
95+
$st = $db->prepare('INSERT OR IGNORE INTO dl(ts,day,file,kind,vis,bot,country,cname,lang,ver)
9496
VALUES(:ts,:day,:f,:k,:v,:b,:cc,:cn,:l,:ver)');
9597
$st->execute(array(
9698
':ts' => time(), ':day' => gmdate('Y-m-d'),

website/public/pulse.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434

3535
try {
3636
$db = sfstats_db();
37-
$st = $db->prepare('INSERT INTO hits(ts,day,path,ref,vis,bot,browser,os,country,cname,ref_full)
37+
// INSERT OR IGNORE: con l'indice unico su (ts,path,vis) un secondo colpo
38+
// identico nello stesso secondo viene lasciato cadere invece di
39+
// sollevare un errore. E' il doppione che non deve piu' entrare.
40+
$st = $db->prepare('INSERT OR IGNORE INTO hits(ts,day,path,ref,vis,bot,browser,os,country,cname,ref_full)
3841
VALUES(:ts,:day,:path,:ref,:vis,:bot,:br,:os,:cc,:cn,:rf)');
3942
$st->execute(array(
4043
':ts' => time(),

website/public/stats.php

Lines changed: 153 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ function page_head($title) {
103103
// ----------------------------------------------------------------------------
104104
// Not authenticated → setup or login screen
105105
if (!$authed) {
106-
page_head('SkillFishOS · Statistiche');
106+
page_head('SkillFishOS · Statistiche');
107107
echo '<div class="mid"><div class="card lbox">';
108108
echo '<div class="brand">SkillFish<span class="g">OS</span> · Statistiche</div>';
109109
if (!$HASH) {
@@ -173,6 +173,116 @@ function toprows($db, $col, $where, $limit = 8) {
173173
$recent = $db->query("SELECT ts,path,ref,ref_full,browser,os,bot,country,cname
174174
FROM hits ORDER BY id DESC LIMIT 20")->fetchAll(PDO::FETCH_ASSOC);
175175

176+
// ⚠️ QUESTO BLOCCO VA DOPO `$db = sfstats_db()` E PRIMA DEI RIQUADRI.
177+
// `page_head('SkillFishOS · Statistiche')` compare DUE VOLTE nel file: una per
178+
// la schermata di accesso e una per il cruscotto. Mettendolo prima della prima
179+
// occorrenza finiva nella schermata di accesso, dove il database non e' ancora
180+
// aperto: la pagina rispondeva 500 a chiunque provasse a entrare. Non lo
181+
// avrebbe detto nessun controllo di sintassi — si e' visto solo eseguendola.
182+
183+
// --- i totali, calcolati PRIMA dei riquadri ---------------------------------
184+
// Questo blocco stava a meta' pagina, sotto le tabelle dei download. I numeri
185+
// che riassumono tutto - quanti download in tutto, quanti visitatori da quando
186+
// il sito esiste - servono in ALTO, dove si guarda per primo, non in fondo dopo
187+
// aver scorso otto tabelle. Il calcolo e' lo stesso; e' cambiato solo il punto
188+
// in cui viene fatto, perche' i riquadri stanno sopra e in PHP una variabile
189+
// non esiste finche' non la si e' riempita.
190+
191+
// --- i numeri veri, presi da SourceForge -------------------------------------
192+
// Si interroga ogni mezz'ora e si tiene in cache: l'API e' pubblica ma non va
193+
// martellata, e questa pagina si ricarica spesso. Mezz'ora e' la stessa cadenza
194+
// del raccoglitore sul container, cosi' i due numeri non si contraddicono.
195+
define('SF_CACHE', 1800);
196+
$sf = null; $sf_err = '';
197+
try {
198+
$cache = sfstats_dir() . '/sf-downloads.json';
199+
if (is_file($cache) && (time() - filemtime($cache) < SF_CACHE)) {
200+
$sf = json_decode((string)file_get_contents($cache), true);
201+
} else {
202+
$url = 'https://sourceforge.net/projects/skillfishos/files/stats/json'
203+
. '?start_date=' . gmdate('Y-m-d', time() - 30 * 86400)
204+
. '&end_date=' . gmdate('Y-m-d');
205+
$ctx = stream_context_create(array('http' => array(
206+
'timeout' => 8, 'header' => "User-Agent: SkillFishOS-stats\r\n")));
207+
$raw = @file_get_contents($url, false, $ctx);
208+
if ($raw) { @file_put_contents($cache, $raw); $sf = json_decode($raw, true); }
209+
else $sf_err = 'SourceForge non ha risposto';
210+
}
211+
} catch (Throwable $e) { $sf_err = 'errore nella lettura'; }
212+
213+
// --- il totale da sempre -----------------------------------------------------
214+
// L'API accetta qualunque data d'inizio e taglia da sola al primo caricamento,
215+
// quindi partiamo da prima che il progetto esistesse e lasciamo fare a lei.
216+
// Anche questo ogni mezz'ora: prima erano sei ore, e nei giorni in cui i
217+
// download si muovono davvero - un video, un articolo - sei ore di ritardo
218+
// vogliono dire guardare una pagina che dice il falso.
219+
$sf_tot = null; $sf_dal = '';
220+
try {
221+
$cache_tot = sfstats_dir() . '/sf-downloads-sempre.json';
222+
$d = null;
223+
if (is_file($cache_tot) && (time() - filemtime($cache_tot) < SF_CACHE)) {
224+
$d = json_decode((string)file_get_contents($cache_tot), true);
225+
} else {
226+
$url = 'https://sourceforge.net/projects/skillfishos/files/stats/json'
227+
. '?start_date=2026-01-01&end_date=' . gmdate('Y-m-d');
228+
$ctx = stream_context_create(array('http' => array(
229+
'timeout' => 12, 'header' => "User-Agent: SkillFishOS-stats\r\n")));
230+
$raw = @file_get_contents($url, false, $ctx);
231+
if ($raw) { @file_put_contents($cache_tot, $raw); $d = json_decode($raw, true); }
232+
elseif (is_file($cache_tot)) {
233+
// se SourceForge non risponde meglio un numero vecchio che nessun numero
234+
$d = json_decode((string)file_get_contents($cache_tot), true);
235+
}
236+
}
237+
if (is_array($d) && isset($d['total'])) {
238+
$sf_tot = (int)$d['total'];
239+
foreach ((array)($d['downloads'] ?? array()) as $g) {
240+
if (is_array($g) && (int)$g[1] > 0) { $sf_dal = substr((string)$g[0], 0, 10); break; }
241+
}
242+
}
243+
} catch (Throwable $e) { }
244+
245+
// --- il dettaglio file per file ----------------------------------------------
246+
// Lo raccoglie il container una volta al giorno (skillfish-stat-sourceforge):
247+
// sono quasi trenta file e ognuno vuole una richiesta all'API, farlo mentre la
248+
// pagina si carica vorrebbe dire mezzo minuto di attesa a ogni apertura.
249+
$det = null;
250+
$f_det = sfstats_dir() . '/sf-dettaglio.json';
251+
if (is_file($f_det)) {
252+
$d2 = json_decode((string)file_get_contents($f_det), true);
253+
if (is_array($d2) && isset($d2['totale'])) {
254+
$det = $d2;
255+
$sf_tot = (int)$d2['totale']; // piu' attendibile: e' la stessa fonte
256+
if (!empty($d2['dal'])) $sf_dal = (string)$d2['dal'];
257+
}
258+
}
259+
260+
// I clic sui nostri pulsanti, in totale. Non sono i download veri: dicono
261+
// quanti li hanno AVVIATI dal sito. Il numero di SourceForge e' sempre piu'
262+
// alto, perche' comprende anche chi arriva li' senza passare da noi.
263+
$dl_clic = 0; $dl_persone = 0;
264+
try {
265+
$dl_clic = one($db, 'SELECT COUNT(*) FROM dl WHERE bot=0');
266+
$dl_persone = one($db, 'SELECT COUNT(DISTINCT vis) FROM dl WHERE bot=0');
267+
} catch (Throwable $e) { /* la tabella arriva col primo clic */ }
268+
269+
// ⚠️ Da quando esiste il sito: il PRIMO giorno che risulta nel database, non
270+
// una data scritta a mano che poi resta indietro.
271+
$dal_giorno = '';
272+
try {
273+
$s = $db->query('SELECT MIN(day) FROM hits');
274+
$r = $s->fetch(PDO::FETCH_NUM);
275+
if ($r && $r[0]) $dal_giorno = (string)$r[0];
276+
} catch (Throwable $e) { }
277+
278+
// ⚠️ VISITATORI TOTALI, e va detto cosa sono davvero. L'identificativo del
279+
// visitatore contiene il GIORNO (e' cosi' che restiamo senza cookie e senza
280+
// poter risalire alla persona), quindi chi torna domani conta come qualcuno di
281+
// nuovo. Questo numero e' la somma dei visitatori giornalieri: e' il massimo
282+
// che si puo' dire senza tracciare le persone, e chiamarlo "persone diverse"
283+
// sarebbe falso.
284+
$uniq_total = one($db, "SELECT COUNT(DISTINCT vis) FROM hits WHERE 1=1$botw");
285+
176286
page_head('SkillFishOS · Statistiche');
177287
echo '<div class="wrap">';
178288
echo '<header><div class="brand">SkillFish<span class="g">OS</span> · Statistiche</div><div class="sp"></div>';
@@ -182,11 +292,41 @@ function toprows($db, $col, $where, $limit = 8) {
182292

183293
// KPIs
184294
$kpi = function ($n, $l) { echo '<div class="kpi"><div class="n">' . number_format($n, 0, ',', '.') . '</div><div class="l">' . h($l) . '</div></div>'; };
295+
296+
// Prima fila: il movimento recente.
185297
echo '<div class="kpis">';
186298
$kpi($views_today, 'Visite oggi'); $kpi($uniq_today, 'Visitatori oggi');
187299
$kpi($views_7, 'Visite 7 giorni'); $kpi($uniq_7, 'Visitatori 7 giorni');
188-
$kpi($views_30, 'Visite 30 giorni'); $kpi($views_total, 'Visite totali');
300+
$kpi($views_30, 'Visite 30 giorni'); $kpi($uniq_30, 'Visitatori 30 giorni');
301+
echo '</div>';
302+
303+
// Seconda fila: i totali da quando il sito esiste, download compresi. Stavano
304+
// sparsi in fondo alla pagina, sotto le tabelle; sono i numeri che si vogliono
305+
// vedere per primi, quindi stanno accanto alle visite.
306+
// ⚠️ Ogni totale porta la SUA data d'inizio, non una comune. Il sito conta le
307+
// visite dal 15/06; SourceForge contava i download da prima che il sito
308+
// esistesse. Una data sola in cima al gruppo le avrebbe attribuite entrambe al
309+
// giorno sbagliato.
310+
$da_sito = $dal_giorno ? ' · dal ' . date('d/m/Y', strtotime($dal_giorno)) : '';
311+
$da_sf = $sf_dal ? ' · dal ' . date('d/m/Y', strtotime($sf_dal)) : '';
312+
echo '<h3 style="margin:26px 0 10px">Da sempre</h3>';
313+
echo '<div class="kpis">';
314+
$kpi($views_total, 'Visite totali' . $da_sito);
315+
$kpi($uniq_total, 'Visitatori totali' . $da_sito);
316+
if ($sf_tot !== null) $kpi($sf_tot, 'Download completati' . $da_sf);
317+
if ($sf && isset($sf['total'])) $kpi((int)$sf['total'], 'Download 30 giorni');
318+
$kpi($dl_clic, 'Download avviati dal sito');
319+
$kpi($dl_persone, 'Chi li ha avviati');
189320
echo '</div>';
321+
// ⚠️ Gli accenti si scrivono accentati. La prima stesura usava l'apostrofo
322+
// (perche', meta') per non litigare con le virgolette del PHP, e sulla pagina
323+
// si leggeva cosi': in mezzo a un testo che per il resto ha gli accenti giusti.
324+
// Con le virgolette doppie il problema non esiste.
325+
echo '<div class="muted" style="font-size:.78rem;margin:8px 2px 0">'
326+
. "<strong>Download completati</strong>: i numeri veri di SourceForge, compreso chi ci arriva senza passare dal sito. "
327+
. "<strong>Avviati dal sito</strong>: i clic sui nostri pulsanti — sempre di meno, perché chi cambia idea a metà non conta. "
328+
. "<strong>Visitatori totali</strong>: la somma dei visitatori giornalieri. Restiamo senza cookie, quindi chi torna un altro giorno non lo possiamo riconoscere e conta due volte."
329+
. '</div>';
190330

191331
// chart
192332
echo '<div class="panel"><h3>Andamento ultimi 30 giorni</h3>';
@@ -383,90 +523,19 @@ function tbl($title, $rows, $klabel, $linkpath = false) {
383523
}
384524
echo '</div>';
385525

386-
// --- i numeri veri, presi da SourceForge -------------------------------------
387-
// Si interroga ogni mezz'ora e si tiene in cache: l'API e' pubblica ma non va
388-
// martellata, e questa pagina si ricarica spesso. Mezz'ora e' la stessa cadenza
389-
// del raccoglitore sul container, cosi' i due numeri non si contraddicono.
390-
define('SF_CACHE', 1800);
391-
$sf = null; $sf_err = '';
392-
try {
393-
$cache = sfstats_dir() . '/sf-downloads.json';
394-
if (is_file($cache) && (time() - filemtime($cache) < SF_CACHE)) {
395-
$sf = json_decode((string)file_get_contents($cache), true);
396-
} else {
397-
$url = 'https://sourceforge.net/projects/skillfishos/files/stats/json'
398-
. '?start_date=' . gmdate('Y-m-d', time() - 30 * 86400)
399-
. '&end_date=' . gmdate('Y-m-d');
400-
$ctx = stream_context_create(array('http' => array(
401-
'timeout' => 8, 'header' => "User-Agent: SkillFishOS-stats\r\n")));
402-
$raw = @file_get_contents($url, false, $ctx);
403-
if ($raw) { @file_put_contents($cache, $raw); $sf = json_decode($raw, true); }
404-
else $sf_err = 'SourceForge non ha risposto';
405-
}
406-
} catch (Throwable $e) { $sf_err = 'errore nella lettura'; }
407-
408-
// --- il totale da sempre -----------------------------------------------------
409-
// L'API accetta qualunque data d'inizio e taglia da sola al primo caricamento,
410-
// quindi partiamo da prima che il progetto esistesse e lasciamo fare a lei.
411-
// Anche questo ogni mezz'ora: prima erano sei ore, e nei giorni in cui i
412-
// download si muovono davvero - un video, un articolo - sei ore di ritardo
413-
// vogliono dire guardare una pagina che dice il falso.
414-
$sf_tot = null; $sf_dal = '';
415-
try {
416-
$cache_tot = sfstats_dir() . '/sf-downloads-sempre.json';
417-
$d = null;
418-
if (is_file($cache_tot) && (time() - filemtime($cache_tot) < SF_CACHE)) {
419-
$d = json_decode((string)file_get_contents($cache_tot), true);
420-
} else {
421-
$url = 'https://sourceforge.net/projects/skillfishos/files/stats/json'
422-
. '?start_date=2026-01-01&end_date=' . gmdate('Y-m-d');
423-
$ctx = stream_context_create(array('http' => array(
424-
'timeout' => 12, 'header' => "User-Agent: SkillFishOS-stats\r\n")));
425-
$raw = @file_get_contents($url, false, $ctx);
426-
if ($raw) { @file_put_contents($cache_tot, $raw); $d = json_decode($raw, true); }
427-
elseif (is_file($cache_tot)) {
428-
// se SourceForge non risponde meglio un numero vecchio che nessun numero
429-
$d = json_decode((string)file_get_contents($cache_tot), true);
430-
}
431-
}
432-
if (is_array($d) && isset($d['total'])) {
433-
$sf_tot = (int)$d['total'];
434-
foreach ((array)($d['downloads'] ?? array()) as $g) {
435-
if (is_array($g) && (int)$g[1] > 0) { $sf_dal = substr((string)$g[0], 0, 10); break; }
436-
}
437-
}
438-
} catch (Throwable $e) { }
439-
440-
// --- il dettaglio file per file ----------------------------------------------
441-
// Lo raccoglie il container una volta al giorno (skillfish-stat-sourceforge):
442-
// sono quasi trenta file e ognuno vuole una richiesta all'API, farlo mentre la
443-
// pagina si carica vorrebbe dire mezzo minuto di attesa a ogni apertura.
444-
$det = null;
445-
$f_det = sfstats_dir() . '/sf-dettaglio.json';
446-
if (is_file($f_det)) {
447-
$d2 = json_decode((string)file_get_contents($f_det), true);
448-
if (is_array($d2) && isset($d2['totale'])) {
449-
$det = $d2;
450-
$sf_tot = (int)$d2['totale']; // piu' attendibile: e' la stessa fonte
451-
if (!empty($d2['dal'])) $sf_dal = (string)$d2['dal'];
452-
}
453-
}
454-
455526
echo '<div class="panel"><h3>Download completati su SourceForge</h3>';
456-
if ($sf_tot !== null) {
527+
// ⚠️ Il totale da sempre e quello a 30 giorni sono saliti in cima alla pagina.
528+
// Ripeterli qui non aggiungeva niente e faceva sembrare che fossero due
529+
// misure diverse. Resta il numero che in alto non c'e': quanti di quei
530+
// download sono immagini ISO e non firme, note o elenchi.
531+
if ($sf_tot !== null && $det && !empty($det['per_tipo'])) {
532+
$iso = 0;
533+
foreach ($det['per_tipo'] as $tp => $n) if (strpos($tp, 'ISO') === 0) $iso += (int)$n;
457534
echo '<div class="kpis">';
458-
echo '<div class="kpi"><div class="n">' . number_format($sf_tot, 0, ',', '.') . '</div>'
459-
. '<div class="l">totale da sempre'
460-
. ($sf_dal ? ' · dal ' . h(date('d/m/Y', strtotime($sf_dal))) : '') . '</div></div>';
461-
if ($sf && isset($sf['total']))
462-
echo '<div class="kpi"><div class="n">' . number_format((int)$sf['total'], 0, ',', '.') . '</div>'
463-
. '<div class="l">ultimi 30 giorni</div></div>';
464-
if ($det && !empty($det['per_tipo'])) {
465-
$iso = 0;
466-
foreach ($det['per_tipo'] as $tp => $n) if (strpos($tp, 'ISO') === 0) $iso += (int)$n;
467-
echo '<div class="kpi"><div class="n">' . number_format($iso, 0, ',', '.') . '</div>'
468-
. '<div class="l">solo immagini ISO</div></div>';
469-
}
535+
echo '<div class="kpi"><div class="n">' . number_format($iso, 0, ',', '.') . '</div>'
536+
. '<div class="l">di cui immagini ISO</div></div>';
537+
echo '<div class="kpi"><div class="n">' . ($sf_tot ? round(100 * $iso / $sf_tot) : 0) . '%</div>'
538+
. '<div class="l">del totale</div></div>';
470539
echo '</div>';
471540
}
472541

0 commit comments

Comments
 (0)