minispace commited on
Commit
12d8a9b
·
1 Parent(s): ae74c77

Strict deduplication: single row per visitor with consolidated visit count, auto-migration of legacy logs, and slide-in drawer detail animation

Browse files
Files changed (7) hide show
  1. beat.html +67 -44
  2. chat_pi.html +67 -44
  3. datos_de_visita.html +87 -36
  4. index.html +67 -44
  5. interactive.html +67 -44
  6. pi.html +67 -44
  7. stand.html +67 -44
beat.html CHANGED
@@ -1186,6 +1186,9 @@ function closeBeatModal() {
1186
 
1187
 
1188
 
 
 
 
1189
  <script>
1190
  (function() {
1191
  try {
@@ -1194,15 +1197,14 @@ function closeBeatModal() {
1194
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
1195
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
1196
 
1197
- // 1. Persistent User ID
1198
  let userId = localStorage.getItem('neo_visitor_uid');
1199
  if (!userId) {
1200
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
1201
  localStorage.setItem('neo_visitor_uid', userId);
1202
  }
1203
- let userName = localStorage.getItem('lead_user_name') || 'Visitante (' + userId + ')';
1204
 
1205
- // 2. Device & Brand Detection
1206
  const ua = navigator.userAgent || '';
1207
  let deviceType = 'Desktop';
1208
  let brandModel = 'PC / Genérico';
@@ -1239,7 +1241,7 @@ function closeBeatModal() {
1239
  osName = 'Linux';
1240
  }
1241
 
1242
- // 3. Browser
1243
  let browserName = 'Navegador Web';
1244
  let browserVer = '';
1245
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
@@ -1263,7 +1265,7 @@ function closeBeatModal() {
1263
  }
1264
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
1265
 
1266
- // 4. Hardware & Network Specs
1267
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
1268
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
1269
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
@@ -1284,7 +1286,7 @@ function closeBeatModal() {
1284
  }).catch(()=>{});
1285
  }
1286
 
1287
- // 5. Session Routes trail
1288
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
1289
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
1290
  sessionRoutes.push(pageTitle);
@@ -1293,36 +1295,35 @@ function closeBeatModal() {
1293
 
1294
  const sessionStart = Date.now();
1295
 
1296
- // 6. Save or Update Consolidated Daily Record
1297
- function updateConsolidatedLog(geo) {
1298
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1299
 
1300
- // Search for existing record of this user for today
1301
- let existing = logs.find(l => (l.uid === userId || (geo.ip && l.ip === geo.ip)) && l.date === dateStr);
1302
-
1303
- if (existing) {
1304
- existing.visitsToday = (existing.visitsToday || 1) + 1;
1305
- existing.lastTime = timeStr;
1306
- existing.page = pageTitle;
1307
-
1308
- // Merge routes
1309
- sessionRoutes.forEach(r => {
1310
- if (!existing.routes.includes(r)) existing.routes.push(r);
1311
- });
1312
-
1313
- // Update dynamic values
1314
- if (geo.ip && existing.ip === 'Oculta') existing.ip = geo.ip;
1315
- if (geo.city && existing.city === 'Desconocida') {
1316
- existing.city = geo.city;
1317
- existing.region = geo.region;
1318
- existing.country = geo.country_name;
1319
- existing.flag = geo.flag;
1320
- existing.isp = geo.org;
1321
  }
1322
- existing.connection = connType;
1323
- existing.battery = batteryLevel;
1324
- } else {
1325
- const newRecord = {
 
1326
  id: Date.now(),
1327
  uid: userId,
1328
  name: userName,
@@ -1332,9 +1333,10 @@ function closeBeatModal() {
1332
  visitsToday: 1,
1333
  page: pageTitle,
1334
  routes: sessionRoutes,
 
1335
  ip: geo.ip || 'Oculta',
1336
  city: geo.city || 'Desconocida',
1337
- region: geo.region || '—',
1338
  country: geo.country_name || 'Argentina',
1339
  flag: geo.flag || '🇦🇷',
1340
  isp: geo.org || '—',
@@ -1354,17 +1356,36 @@ function closeBeatModal() {
1354
  battery: batteryLevel,
1355
  durationSec: 1
1356
  };
1357
- logs.unshift(newRecord);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1358
  }
1359
 
1360
- if (logs.length > 500) logs.pop();
1361
- localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1362
  }
1363
 
1364
- // Fetch IP & Location
1365
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
1366
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
1367
- updateConsolidatedLog({
1368
  ip: d.ip,
1369
  city: d.city,
1370
  region: d.region || d.region_code || 'Buenos Aires',
@@ -1374,7 +1395,7 @@ function closeBeatModal() {
1374
  postal: d.postal
1375
  });
1376
  }).catch(() => {
1377
- updateConsolidatedLog({
1378
  ip: 'IP Local',
1379
  city: 'Buenos Aires',
1380
  region: 'Buenos Aires',
@@ -1387,11 +1408,13 @@ function closeBeatModal() {
1387
 
1388
  // Live screen timer
1389
  setInterval(() => {
1390
- const elapsed = Math.round((Date.now() - sessionStart) / 1000);
1391
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1392
- const cur = logs.find(l => l.uid === userId && l.date === dateStr);
1393
  if (cur) {
1394
  cur.durationSec = (cur.durationSec || 1) + 2;
 
 
 
1395
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1396
  }
1397
  }, 2000);
 
1186
 
1187
 
1188
 
1189
+
1190
+
1191
+
1192
  <script>
1193
  (function() {
1194
  try {
 
1197
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
1198
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
1199
 
 
1200
  let userId = localStorage.getItem('neo_visitor_uid');
1201
  if (!userId) {
1202
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
1203
  localStorage.setItem('neo_visitor_uid', userId);
1204
  }
1205
+ let userName = localStorage.getItem('lead_user_name') || 'Anónimo (' + userId + ')';
1206
 
1207
+ // Device & Brand
1208
  const ua = navigator.userAgent || '';
1209
  let deviceType = 'Desktop';
1210
  let brandModel = 'PC / Genérico';
 
1241
  osName = 'Linux';
1242
  }
1243
 
1244
+ // Browser
1245
  let browserName = 'Navegador Web';
1246
  let browserVer = '';
1247
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
 
1265
  }
1266
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
1267
 
1268
+ // Hardware & Screen Specs
1269
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
1270
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
1271
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
 
1286
  }).catch(()=>{});
1287
  }
1288
 
1289
+ // Navigation trail
1290
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
1291
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
1292
  sessionRoutes.push(pageTitle);
 
1295
 
1296
  const sessionStart = Date.now();
1297
 
1298
+ // Consolidated Saver
1299
+ function saveOrMergeVisitor(geo) {
1300
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1301
 
1302
+ // Deduplicate entire array by UID first
1303
+ let userMap = {};
1304
+ logs.forEach(l => {
1305
+ const key = l.uid || l.ip || 'USR-UNKNOWN';
1306
+ if (!userMap[key]) {
1307
+ userMap[key] = l;
1308
+ if (!l.sessions) {
1309
+ l.sessions = [{ time: l.firstTime || l.time, page: l.page, duration: l.durationSec || 1 }];
1310
+ }
1311
+ } else {
1312
+ userMap[key].visitsToday = (userMap[key].visitsToday || 1) + 1;
1313
+ userMap[key].durationSec = (userMap[key].durationSec || 0) + (l.durationSec || 1);
1314
+ userMap[key].lastTime = l.lastTime || l.time || userMap[key].lastTime;
1315
+ (l.routes || []).forEach(r => {
1316
+ if (!userMap[key].routes.includes(r)) userMap[key].routes.push(r);
1317
+ });
1318
+ if (l.sessions) {
1319
+ userMap[key].sessions = userMap[key].sessions.concat(l.sessions);
1320
+ }
 
 
1321
  }
1322
+ });
1323
+
1324
+ // Now update current user in userMap
1325
+ if (!userMap[userId]) {
1326
+ userMap[userId] = {
1327
  id: Date.now(),
1328
  uid: userId,
1329
  name: userName,
 
1333
  visitsToday: 1,
1334
  page: pageTitle,
1335
  routes: sessionRoutes,
1336
+ sessions: [{ time: timeStr, page: pageTitle, duration: 1 }],
1337
  ip: geo.ip || 'Oculta',
1338
  city: geo.city || 'Desconocida',
1339
+ region: geo.region || 'Buenos Aires',
1340
  country: geo.country_name || 'Argentina',
1341
  flag: geo.flag || '🇦🇷',
1342
  isp: geo.org || '—',
 
1356
  battery: batteryLevel,
1357
  durationSec: 1
1358
  };
1359
+ } else {
1360
+ const u = userMap[userId];
1361
+ u.visitsToday = (u.visitsToday || 1) + 1;
1362
+ u.lastTime = timeStr;
1363
+ u.page = pageTitle;
1364
+ sessionRoutes.forEach(r => {
1365
+ if (!u.routes.includes(r)) u.routes.push(r);
1366
+ });
1367
+ if (!u.sessions) u.sessions = [];
1368
+ u.sessions.push({ time: timeStr, page: pageTitle, duration: 1 });
1369
+ if (geo.ip && u.ip === 'Oculta') u.ip = geo.ip;
1370
+ if (geo.city && u.city === 'Desconocida') {
1371
+ u.city = geo.city;
1372
+ u.region = geo.region;
1373
+ u.country = geo.country_name;
1374
+ u.flag = geo.flag;
1375
+ u.isp = geo.org;
1376
+ }
1377
+ u.connection = connType;
1378
+ u.battery = batteryLevel;
1379
  }
1380
 
1381
+ const cleanList = Object.values(userMap);
1382
+ localStorage.setItem('site_visitor_history', JSON.stringify(cleanList));
1383
  }
1384
 
1385
+ // Fetch IP
1386
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
1387
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
1388
+ saveOrMergeVisitor({
1389
  ip: d.ip,
1390
  city: d.city,
1391
  region: d.region || d.region_code || 'Buenos Aires',
 
1395
  postal: d.postal
1396
  });
1397
  }).catch(() => {
1398
+ saveOrMergeVisitor({
1399
  ip: 'IP Local',
1400
  city: 'Buenos Aires',
1401
  region: 'Buenos Aires',
 
1408
 
1409
  // Live screen timer
1410
  setInterval(() => {
1411
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1412
+ const cur = logs.find(l => l.uid === userId);
 
1413
  if (cur) {
1414
  cur.durationSec = (cur.durationSec || 1) + 2;
1415
+ if (cur.sessions && cur.sessions.length > 0) {
1416
+ cur.sessions[cur.sessions.length - 1].duration = (cur.sessions[cur.sessions.length - 1].duration || 1) + 2;
1417
+ }
1418
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1419
  }
1420
  }, 2000);
chat_pi.html CHANGED
@@ -877,6 +877,9 @@
877
 
878
 
879
 
 
 
 
880
  <script>
881
  (function() {
882
  try {
@@ -885,15 +888,14 @@
885
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
886
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
887
 
888
- // 1. Persistent User ID
889
  let userId = localStorage.getItem('neo_visitor_uid');
890
  if (!userId) {
891
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
892
  localStorage.setItem('neo_visitor_uid', userId);
893
  }
894
- let userName = localStorage.getItem('lead_user_name') || 'Visitante (' + userId + ')';
895
 
896
- // 2. Device & Brand Detection
897
  const ua = navigator.userAgent || '';
898
  let deviceType = 'Desktop';
899
  let brandModel = 'PC / Genérico';
@@ -930,7 +932,7 @@
930
  osName = 'Linux';
931
  }
932
 
933
- // 3. Browser
934
  let browserName = 'Navegador Web';
935
  let browserVer = '';
936
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
@@ -954,7 +956,7 @@
954
  }
955
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
956
 
957
- // 4. Hardware & Network Specs
958
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
959
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
960
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
@@ -975,7 +977,7 @@
975
  }).catch(()=>{});
976
  }
977
 
978
- // 5. Session Routes trail
979
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
980
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
981
  sessionRoutes.push(pageTitle);
@@ -984,36 +986,35 @@
984
 
985
  const sessionStart = Date.now();
986
 
987
- // 6. Save or Update Consolidated Daily Record
988
- function updateConsolidatedLog(geo) {
989
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
990
 
991
- // Search for existing record of this user for today
992
- let existing = logs.find(l => (l.uid === userId || (geo.ip && l.ip === geo.ip)) && l.date === dateStr);
993
-
994
- if (existing) {
995
- existing.visitsToday = (existing.visitsToday || 1) + 1;
996
- existing.lastTime = timeStr;
997
- existing.page = pageTitle;
998
-
999
- // Merge routes
1000
- sessionRoutes.forEach(r => {
1001
- if (!existing.routes.includes(r)) existing.routes.push(r);
1002
- });
1003
-
1004
- // Update dynamic values
1005
- if (geo.ip && existing.ip === 'Oculta') existing.ip = geo.ip;
1006
- if (geo.city && existing.city === 'Desconocida') {
1007
- existing.city = geo.city;
1008
- existing.region = geo.region;
1009
- existing.country = geo.country_name;
1010
- existing.flag = geo.flag;
1011
- existing.isp = geo.org;
1012
  }
1013
- existing.connection = connType;
1014
- existing.battery = batteryLevel;
1015
- } else {
1016
- const newRecord = {
 
1017
  id: Date.now(),
1018
  uid: userId,
1019
  name: userName,
@@ -1023,9 +1024,10 @@
1023
  visitsToday: 1,
1024
  page: pageTitle,
1025
  routes: sessionRoutes,
 
1026
  ip: geo.ip || 'Oculta',
1027
  city: geo.city || 'Desconocida',
1028
- region: geo.region || '—',
1029
  country: geo.country_name || 'Argentina',
1030
  flag: geo.flag || '🇦🇷',
1031
  isp: geo.org || '—',
@@ -1045,17 +1047,36 @@
1045
  battery: batteryLevel,
1046
  durationSec: 1
1047
  };
1048
- logs.unshift(newRecord);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1049
  }
1050
 
1051
- if (logs.length > 500) logs.pop();
1052
- localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1053
  }
1054
 
1055
- // Fetch IP & Location
1056
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
1057
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
1058
- updateConsolidatedLog({
1059
  ip: d.ip,
1060
  city: d.city,
1061
  region: d.region || d.region_code || 'Buenos Aires',
@@ -1065,7 +1086,7 @@
1065
  postal: d.postal
1066
  });
1067
  }).catch(() => {
1068
- updateConsolidatedLog({
1069
  ip: 'IP Local',
1070
  city: 'Buenos Aires',
1071
  region: 'Buenos Aires',
@@ -1078,11 +1099,13 @@
1078
 
1079
  // Live screen timer
1080
  setInterval(() => {
1081
- const elapsed = Math.round((Date.now() - sessionStart) / 1000);
1082
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1083
- const cur = logs.find(l => l.uid === userId && l.date === dateStr);
1084
  if (cur) {
1085
  cur.durationSec = (cur.durationSec || 1) + 2;
 
 
 
1086
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1087
  }
1088
  }, 2000);
 
877
 
878
 
879
 
880
+
881
+
882
+
883
  <script>
884
  (function() {
885
  try {
 
888
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
889
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
890
 
 
891
  let userId = localStorage.getItem('neo_visitor_uid');
892
  if (!userId) {
893
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
894
  localStorage.setItem('neo_visitor_uid', userId);
895
  }
896
+ let userName = localStorage.getItem('lead_user_name') || 'Anónimo (' + userId + ')';
897
 
898
+ // Device & Brand
899
  const ua = navigator.userAgent || '';
900
  let deviceType = 'Desktop';
901
  let brandModel = 'PC / Genérico';
 
932
  osName = 'Linux';
933
  }
934
 
935
+ // Browser
936
  let browserName = 'Navegador Web';
937
  let browserVer = '';
938
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
 
956
  }
957
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
958
 
959
+ // Hardware & Screen Specs
960
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
961
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
962
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
 
977
  }).catch(()=>{});
978
  }
979
 
980
+ // Navigation trail
981
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
982
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
983
  sessionRoutes.push(pageTitle);
 
986
 
987
  const sessionStart = Date.now();
988
 
989
+ // Consolidated Saver
990
+ function saveOrMergeVisitor(geo) {
991
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
992
 
993
+ // Deduplicate entire array by UID first
994
+ let userMap = {};
995
+ logs.forEach(l => {
996
+ const key = l.uid || l.ip || 'USR-UNKNOWN';
997
+ if (!userMap[key]) {
998
+ userMap[key] = l;
999
+ if (!l.sessions) {
1000
+ l.sessions = [{ time: l.firstTime || l.time, page: l.page, duration: l.durationSec || 1 }];
1001
+ }
1002
+ } else {
1003
+ userMap[key].visitsToday = (userMap[key].visitsToday || 1) + 1;
1004
+ userMap[key].durationSec = (userMap[key].durationSec || 0) + (l.durationSec || 1);
1005
+ userMap[key].lastTime = l.lastTime || l.time || userMap[key].lastTime;
1006
+ (l.routes || []).forEach(r => {
1007
+ if (!userMap[key].routes.includes(r)) userMap[key].routes.push(r);
1008
+ });
1009
+ if (l.sessions) {
1010
+ userMap[key].sessions = userMap[key].sessions.concat(l.sessions);
1011
+ }
 
 
1012
  }
1013
+ });
1014
+
1015
+ // Now update current user in userMap
1016
+ if (!userMap[userId]) {
1017
+ userMap[userId] = {
1018
  id: Date.now(),
1019
  uid: userId,
1020
  name: userName,
 
1024
  visitsToday: 1,
1025
  page: pageTitle,
1026
  routes: sessionRoutes,
1027
+ sessions: [{ time: timeStr, page: pageTitle, duration: 1 }],
1028
  ip: geo.ip || 'Oculta',
1029
  city: geo.city || 'Desconocida',
1030
+ region: geo.region || 'Buenos Aires',
1031
  country: geo.country_name || 'Argentina',
1032
  flag: geo.flag || '🇦🇷',
1033
  isp: geo.org || '—',
 
1047
  battery: batteryLevel,
1048
  durationSec: 1
1049
  };
1050
+ } else {
1051
+ const u = userMap[userId];
1052
+ u.visitsToday = (u.visitsToday || 1) + 1;
1053
+ u.lastTime = timeStr;
1054
+ u.page = pageTitle;
1055
+ sessionRoutes.forEach(r => {
1056
+ if (!u.routes.includes(r)) u.routes.push(r);
1057
+ });
1058
+ if (!u.sessions) u.sessions = [];
1059
+ u.sessions.push({ time: timeStr, page: pageTitle, duration: 1 });
1060
+ if (geo.ip && u.ip === 'Oculta') u.ip = geo.ip;
1061
+ if (geo.city && u.city === 'Desconocida') {
1062
+ u.city = geo.city;
1063
+ u.region = geo.region;
1064
+ u.country = geo.country_name;
1065
+ u.flag = geo.flag;
1066
+ u.isp = geo.org;
1067
+ }
1068
+ u.connection = connType;
1069
+ u.battery = batteryLevel;
1070
  }
1071
 
1072
+ const cleanList = Object.values(userMap);
1073
+ localStorage.setItem('site_visitor_history', JSON.stringify(cleanList));
1074
  }
1075
 
1076
+ // Fetch IP
1077
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
1078
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
1079
+ saveOrMergeVisitor({
1080
  ip: d.ip,
1081
  city: d.city,
1082
  region: d.region || d.region_code || 'Buenos Aires',
 
1086
  postal: d.postal
1087
  });
1088
  }).catch(() => {
1089
+ saveOrMergeVisitor({
1090
  ip: 'IP Local',
1091
  city: 'Buenos Aires',
1092
  region: 'Buenos Aires',
 
1099
 
1100
  // Live screen timer
1101
  setInterval(() => {
1102
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1103
+ const cur = logs.find(l => l.uid === userId);
 
1104
  if (cur) {
1105
  cur.durationSec = (cur.durationSec || 1) + 2;
1106
+ if (cur.sessions && cur.sessions.length > 0) {
1107
+ cur.sessions[cur.sessions.length - 1].duration = (cur.sessions[cur.sessions.length - 1].duration || 1) + 2;
1108
+ }
1109
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1110
  }
1111
  }, 2000);
datos_de_visita.html CHANGED
@@ -839,7 +839,7 @@
839
  let currentRegion = 'all';
840
  let currentCity = 'all';
841
  let currentDevice = 'all';
842
- let activeUserForDrawer = null;
843
 
844
  function setGeoLevel(lvl, val) {
845
  if (lvl === 'all') {
@@ -866,15 +866,62 @@
866
  renderView();
867
  }
868
 
869
- function renderView() {
 
870
  const raw = localStorage.getItem('site_visitor_history') || '[]';
871
  const logs = JSON.parse(raw);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
872
  const query = (document.getElementById('searchInput').value || '').toLowerCase().trim();
873
 
874
- // Total Unique Users & Metrics
875
- const totalUsers = logs.length;
876
- const totalIngresos = logs.reduce((acc, l) => acc + (l.visitsToday || 1), 0);
877
- const totalSecs = logs.reduce((acc, l) => acc + (l.durationSec || 1), 0);
878
  const avgSecs = totalUsers > 0 ? Math.round(totalSecs / totalUsers) : 0;
879
 
880
  const devMap = {};
@@ -882,7 +929,7 @@
882
  const countryMap = {};
883
  const cityMap = {};
884
 
885
- logs.forEach(l => {
886
  const dev = l.deviceType || 'Desktop';
887
  devMap[dev] = (devMap[dev] || 0) + 1;
888
 
@@ -900,7 +947,7 @@
900
  const topDev = Object.entries(devMap).sort((a,b)=>b[1]-a[1])[0] || ['—', 0];
901
 
902
  document.getElementById('statUniqueUsers').textContent = totalUsers;
903
- document.getElementById('statTotalIngresos').textContent = `${totalIngresos} ingresos totales registrados`;
904
  document.getElementById('statTotalTime').textContent = Math.round(totalSecs / 60) + ' min';
905
  document.getElementById('statAvgTime').textContent = `Promedio: ${avgSecs}s por usuario`;
906
  document.getElementById('statTopLocation').textContent = topLoc[0];
@@ -908,7 +955,7 @@
908
  document.getElementById('statTopDevice').textContent = topDev[0];
909
  document.getElementById('statDevBreakdown').textContent = Object.entries(devMap).map(([k,v])=>`${k}: ${v}`).join(' · ');
910
 
911
- // Render Breadcrumbs
912
  const bc = document.getElementById('breadcrumbsBar');
913
  let bcHtml = `<button class="crumb-btn ${currentCountry === 'all' ? 'active' : ''}" onclick="setGeoLevel('all')">🗺️ Todos los Países</button>`;
914
  if (currentCountry !== 'all') {
@@ -922,7 +969,7 @@
922
  }
923
  bc.innerHTML = bcHtml;
924
 
925
- // Render Drilldown Group Chips
926
  const chipsRow = document.getElementById('groupChipsRow');
927
  let chipsHtml = '';
928
 
@@ -935,7 +982,7 @@
935
  `).join('');
936
  } else if (currentRegion === 'all') {
937
  const provsInCountry = {};
938
- logs.filter(l => (l.country || 'Argentina') === currentCountry).forEach(l => {
939
  const r = l.region || 'Buenos Aires';
940
  provsInCountry[r] = (provsInCountry[r] || 0) + 1;
941
  });
@@ -947,7 +994,7 @@
947
  `).join('');
948
  } else {
949
  const citiesInProv = {};
950
- logs.filter(l => (l.region || 'Buenos Aires') === currentRegion).forEach(l => {
951
  const ct = l.city || 'Desconocida';
952
  citiesInProv[ct] = (citiesInProv[ct] || 0) + 1;
953
  });
@@ -961,7 +1008,7 @@
961
  chipsRow.innerHTML = chipsHtml || '<span style="font-size:0.8rem; color:var(--text-muted);">No hay más sub-grupos para este filtro.</span>';
962
 
963
  // Filter List
964
- const filtered = logs.filter(l => {
965
  if (currentCountry !== 'all' && (l.country || 'Argentina') !== currentCountry) return false;
966
  if (currentRegion !== 'all' && (l.region || 'Buenos Aires') !== currentRegion) return false;
967
  if (currentCity !== 'all' && (l.city || 'Desconocida') !== currentCity) return false;
@@ -976,6 +1023,8 @@
976
  return true;
977
  });
978
 
 
 
979
  // Render Unique Users List (ONE CARD PER UNIQUE VISITOR)
980
  const vList = document.getElementById('visitorsList');
981
  if (!filtered.length) {
@@ -1021,11 +1070,8 @@
1021
  // SLIDE-IN DRAWER FUNCTIONS
1022
  // ==========================================
1023
  function openDrawer(filteredIdx) {
1024
- const raw = localStorage.getItem('site_visitor_history') || '[]';
1025
- const logs = JSON.parse(raw);
1026
- const user = logs[filteredIdx];
1027
  if (!user) return;
1028
- activeUserForDrawer = user;
1029
 
1030
  // Fill Drawer Details
1031
  document.getElementById('drawerAvatar').textContent = user.deviceType === 'Móvil' ? '📱' : (user.deviceType === 'Tablet' ? '📟' : '💻');
@@ -1046,25 +1092,30 @@
1046
  document.getElementById('drawerScreen').textContent = `${user.screen || '—'} (${user.orientation || 'Vertical'})`;
1047
  document.getElementById('drawerBattery').textContent = user.battery || 'N/D';
1048
 
1049
- // Build Timeline
1050
  const tl = document.getElementById('drawerTimeline');
1051
  const totalDuration = user.durationSec > 60 ? `${Math.floor(user.durationSec/60)}m ${user.durationSec%60}s` : `${user.durationSec} seg`;
1052
  const routesPills = (user.routes || [user.page || 'Inicio']).map(r => `<span class="route-mini-pill">${r}</span>`).join('');
1053
 
1054
- tl.innerHTML = `
1055
  <div class="timeline-item">
1056
- <div class="timeline-time">📅 ${user.date}</div>
1057
  <div style="font-size:0.82rem; color:#f1f5f9; margin-top:2px;">
1058
- Primer ingreso a las <strong>${user.firstTime || user.time}</strong> · Última actividad <strong>${user.lastTime || user.time}</strong>
1059
- </div>
1060
- <div style="font-size:0.78rem; color:#4ade80; font-weight:700; margin-top:2px;">
1061
- ⏱️ Tiempo total acumulado: ${totalDuration} (${user.visitsToday || 1} accesos en el día)
1062
- </div>
1063
- <div style="font-size:0.75rem; color:var(--text-muted); font-weight:700; text-transform:uppercase; margin-top:8px;">Secciones navegadas:</div>
1064
- <div class="timeline-routes">
1065
- ${routesPills}
1066
  </div>
1067
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
1068
  `;
1069
 
1070
  // Open Animation
@@ -1078,21 +1129,21 @@
1078
  }
1079
 
1080
  function exportJSON() {
1081
- const data = localStorage.getItem('site_visitor_history') || '[]';
1082
- const blob = new Blob([data], { type: 'application/json' });
1083
  const url = URL.createObjectURL(blob);
1084
  const a = document.createElement('a');
1085
  a.href = url;
1086
- a.download = `registro_usuarios_${Date.now()}.json`;
1087
  a.click();
1088
  }
1089
 
1090
  function exportCSV() {
1091
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1092
- if (!logs.length) { alert('No hay datos para exportar.'); return; }
1093
 
1094
  const headers = ['UID','Nombre','Fecha','Primera_Hora','Ultima_Hora','Ingresos_Hoy','Duracion_Seg','IP','Ciudad','Provincia','Pais','Dispositivo','Marca_Modelo','Sistema','Navegador','Pantalla','Rutas'];
1095
- const rows = logs.map(l => [
1096
  l.uid,
1097
  `"${(l.name||'').replace(/"/g, '""')}"`,
1098
  l.date,
@@ -1117,12 +1168,12 @@
1117
  const url = URL.createObjectURL(blob);
1118
  const a = document.createElement('a');
1119
  a.href = url;
1120
- a.download = `registro_usuarios_${Date.now()}.csv`;
1121
  a.click();
1122
  }
1123
 
1124
  function clearLogs() {
1125
- if (confirm('¿Eliminar todo el registro de usuarios únicos?')) {
1126
  localStorage.removeItem('site_visitor_history');
1127
  renderView();
1128
  }
 
839
  let currentRegion = 'all';
840
  let currentCity = 'all';
841
  let currentDevice = 'all';
842
+ let activeFilteredUsers = [];
843
 
844
  function setGeoLevel(lvl, val) {
845
  if (lvl === 'all') {
 
866
  renderView();
867
  }
868
 
869
+ // AGGREGATION & STRICT DEDUPLICATION LOGIC
870
+ function getGroupedUniqueVisitors() {
871
  const raw = localStorage.getItem('site_visitor_history') || '[]';
872
  const logs = JSON.parse(raw);
873
+ const userMap = {};
874
+
875
+ logs.forEach(l => {
876
+ const key = l.uid || l.ip || 'USR-UNKNOWN';
877
+ if (!userMap[key]) {
878
+ userMap[key] = {
879
+ ...l,
880
+ visitsToday: l.visitsToday || 1,
881
+ durationSec: l.durationSec || 1,
882
+ routes: Array.from(new Set(l.routes || [l.page || 'Inicio'])),
883
+ sessions: l.sessions || [{ time: l.firstTime || l.time, page: l.page || 'Inicio', duration: l.durationSec || 1 }]
884
+ };
885
+ } else {
886
+ const u = userMap[key];
887
+ u.visitsToday += (l.visitsToday || 1);
888
+ u.durationSec += (l.durationSec || 1);
889
+ u.lastTime = l.lastTime || l.time || u.lastTime;
890
+ (l.routes || []).forEach(r => {
891
+ if (!u.routes.includes(r)) u.routes.push(r);
892
+ });
893
+ if (l.sessions) {
894
+ u.sessions = u.sessions.concat(l.sessions);
895
+ } else {
896
+ u.sessions.push({ time: l.firstTime || l.time, page: l.page || 'Inicio', duration: l.durationSec || 1 });
897
+ }
898
+ if (l.ip && l.ip !== 'Oculta') u.ip = l.ip;
899
+ if (l.city && l.city !== 'Desconocida') {
900
+ u.city = l.city;
901
+ u.region = l.region;
902
+ u.country = l.country;
903
+ u.flag = l.flag;
904
+ u.isp = l.isp;
905
+ }
906
+ }
907
+ });
908
+
909
+ const uniqueList = Object.values(userMap);
910
+ // Sync clean consolidated list back to localStorage to clean old junk
911
+ if (uniqueList.length > 0 && uniqueList.length !== logs.length) {
912
+ localStorage.setItem('site_visitor_history', JSON.stringify(uniqueList));
913
+ }
914
+ return uniqueList;
915
+ }
916
+
917
+ function renderView() {
918
+ const uniqueUsers = getGroupedUniqueVisitors();
919
  const query = (document.getElementById('searchInput').value || '').toLowerCase().trim();
920
 
921
+ // Metrics
922
+ const totalUsers = uniqueUsers.length;
923
+ const totalIngresos = uniqueUsers.reduce((acc, l) => acc + (l.visitsToday || 1), 0);
924
+ const totalSecs = uniqueUsers.reduce((acc, l) => acc + (l.durationSec || 1), 0);
925
  const avgSecs = totalUsers > 0 ? Math.round(totalSecs / totalUsers) : 0;
926
 
927
  const devMap = {};
 
929
  const countryMap = {};
930
  const cityMap = {};
931
 
932
+ uniqueUsers.forEach(l => {
933
  const dev = l.deviceType || 'Desktop';
934
  devMap[dev] = (devMap[dev] || 0) + 1;
935
 
 
947
  const topDev = Object.entries(devMap).sort((a,b)=>b[1]-a[1])[0] || ['—', 0];
948
 
949
  document.getElementById('statUniqueUsers').textContent = totalUsers;
950
+ document.getElementById('statTotalIngresos').textContent = `${totalIngresos} visitas registradas`;
951
  document.getElementById('statTotalTime').textContent = Math.round(totalSecs / 60) + ' min';
952
  document.getElementById('statAvgTime').textContent = `Promedio: ${avgSecs}s por usuario`;
953
  document.getElementById('statTopLocation').textContent = topLoc[0];
 
955
  document.getElementById('statTopDevice').textContent = topDev[0];
956
  document.getElementById('statDevBreakdown').textContent = Object.entries(devMap).map(([k,v])=>`${k}: ${v}`).join(' · ');
957
 
958
+ // Breadcrumbs
959
  const bc = document.getElementById('breadcrumbsBar');
960
  let bcHtml = `<button class="crumb-btn ${currentCountry === 'all' ? 'active' : ''}" onclick="setGeoLevel('all')">🗺️ Todos los Países</button>`;
961
  if (currentCountry !== 'all') {
 
969
  }
970
  bc.innerHTML = bcHtml;
971
 
972
+ // Group Chips
973
  const chipsRow = document.getElementById('groupChipsRow');
974
  let chipsHtml = '';
975
 
 
982
  `).join('');
983
  } else if (currentRegion === 'all') {
984
  const provsInCountry = {};
985
+ uniqueUsers.filter(l => (l.country || 'Argentina') === currentCountry).forEach(l => {
986
  const r = l.region || 'Buenos Aires';
987
  provsInCountry[r] = (provsInCountry[r] || 0) + 1;
988
  });
 
994
  `).join('');
995
  } else {
996
  const citiesInProv = {};
997
+ uniqueUsers.filter(l => (l.region || 'Buenos Aires') === currentRegion).forEach(l => {
998
  const ct = l.city || 'Desconocida';
999
  citiesInProv[ct] = (citiesInProv[ct] || 0) + 1;
1000
  });
 
1008
  chipsRow.innerHTML = chipsHtml || '<span style="font-size:0.8rem; color:var(--text-muted);">No hay más sub-grupos para este filtro.</span>';
1009
 
1010
  // Filter List
1011
+ const filtered = uniqueUsers.filter(l => {
1012
  if (currentCountry !== 'all' && (l.country || 'Argentina') !== currentCountry) return false;
1013
  if (currentRegion !== 'all' && (l.region || 'Buenos Aires') !== currentRegion) return false;
1014
  if (currentCity !== 'all' && (l.city || 'Desconocida') !== currentCity) return false;
 
1023
  return true;
1024
  });
1025
 
1026
+ activeFilteredUsers = filtered;
1027
+
1028
  // Render Unique Users List (ONE CARD PER UNIQUE VISITOR)
1029
  const vList = document.getElementById('visitorsList');
1030
  if (!filtered.length) {
 
1070
  // SLIDE-IN DRAWER FUNCTIONS
1071
  // ==========================================
1072
  function openDrawer(filteredIdx) {
1073
+ const user = activeFilteredUsers[filteredIdx];
 
 
1074
  if (!user) return;
 
1075
 
1076
  // Fill Drawer Details
1077
  document.getElementById('drawerAvatar').textContent = user.deviceType === 'Móvil' ? '📱' : (user.deviceType === 'Tablet' ? '📟' : '💻');
 
1092
  document.getElementById('drawerScreen').textContent = `${user.screen || '—'} (${user.orientation || 'Vertical'})`;
1093
  document.getElementById('drawerBattery').textContent = user.battery || 'N/D';
1094
 
1095
+ // Build Timeline with all sessions
1096
  const tl = document.getElementById('drawerTimeline');
1097
  const totalDuration = user.durationSec > 60 ? `${Math.floor(user.durationSec/60)}m ${user.durationSec%60}s` : `${user.durationSec} seg`;
1098
  const routesPills = (user.routes || [user.page || 'Inicio']).map(r => `<span class="route-mini-pill">${r}</span>`).join('');
1099
 
1100
+ const sessionsList = (user.sessions || [{ time: user.firstTime || user.time, page: user.page, duration: user.durationSec }]).map((s, i) => `
1101
  <div class="timeline-item">
1102
+ <div class="timeline-time">Ingreso #${i + 1} · ${s.time || user.time}</div>
1103
  <div style="font-size:0.82rem; color:#f1f5f9; margin-top:2px;">
1104
+ Sección: <strong>${s.page || 'Inicio'}</strong> · Tiempo: <strong>${s.duration || 1}s</strong>
 
 
 
 
 
 
 
1105
  </div>
1106
  </div>
1107
+ `).join('');
1108
+
1109
+ tl.innerHTML = `
1110
+ <div style="font-size:0.82rem; color:#4ade80; font-weight:700; margin-bottom:12px;">
1111
+ ⏱️ Tiempo total acumulado hoy: ${totalDuration} (${user.visitsToday || 1} accesos)
1112
+ </div>
1113
+ <div style="font-size:0.75rem; color:var(--text-muted); font-weight:700; text-transform:uppercase; margin-bottom:6px;">Todas las secciones visitadas:</div>
1114
+ <div class="timeline-routes" style="margin-bottom:16px;">
1115
+ ${routesPills}
1116
+ </div>
1117
+ <div style="font-size:0.75rem; color:var(--text-muted); font-weight:700; text-transform:uppercase; margin-bottom:8px;">Detalle de ingresos en el día:</div>
1118
+ ${sessionsList}
1119
  `;
1120
 
1121
  // Open Animation
 
1129
  }
1130
 
1131
  function exportJSON() {
1132
+ const uniqueUsers = getGroupedUniqueVisitors();
1133
+ const blob = new Blob([JSON.stringify(uniqueUsers, null, 2)], { type: 'application/json' });
1134
  const url = URL.createObjectURL(blob);
1135
  const a = document.createElement('a');
1136
  a.href = url;
1137
+ a.download = `registro_usuarios_unicos_${Date.now()}.json`;
1138
  a.click();
1139
  }
1140
 
1141
  function exportCSV() {
1142
+ const uniqueUsers = getGroupedUniqueVisitors();
1143
+ if (!uniqueUsers.length) { alert('No hay datos para exportar.'); return; }
1144
 
1145
  const headers = ['UID','Nombre','Fecha','Primera_Hora','Ultima_Hora','Ingresos_Hoy','Duracion_Seg','IP','Ciudad','Provincia','Pais','Dispositivo','Marca_Modelo','Sistema','Navegador','Pantalla','Rutas'];
1146
+ const rows = uniqueUsers.map(l => [
1147
  l.uid,
1148
  `"${(l.name||'').replace(/"/g, '""')}"`,
1149
  l.date,
 
1168
  const url = URL.createObjectURL(blob);
1169
  const a = document.createElement('a');
1170
  a.href = url;
1171
+ a.download = `registro_usuarios_unicos_${Date.now()}.csv`;
1172
  a.click();
1173
  }
1174
 
1175
  function clearLogs() {
1176
+ if (confirm('¿Eliminar todo el registro de visitas?')) {
1177
  localStorage.removeItem('site_visitor_history');
1178
  renderView();
1179
  }
index.html CHANGED
@@ -183,6 +183,9 @@
183
 
184
 
185
 
 
 
 
186
  <script>
187
  (function() {
188
  try {
@@ -191,15 +194,14 @@
191
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
192
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
193
 
194
- // 1. Persistent User ID
195
  let userId = localStorage.getItem('neo_visitor_uid');
196
  if (!userId) {
197
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
198
  localStorage.setItem('neo_visitor_uid', userId);
199
  }
200
- let userName = localStorage.getItem('lead_user_name') || 'Visitante (' + userId + ')';
201
 
202
- // 2. Device & Brand Detection
203
  const ua = navigator.userAgent || '';
204
  let deviceType = 'Desktop';
205
  let brandModel = 'PC / Genérico';
@@ -236,7 +238,7 @@
236
  osName = 'Linux';
237
  }
238
 
239
- // 3. Browser
240
  let browserName = 'Navegador Web';
241
  let browserVer = '';
242
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
@@ -260,7 +262,7 @@
260
  }
261
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
262
 
263
- // 4. Hardware & Network Specs
264
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
265
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
266
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
@@ -281,7 +283,7 @@
281
  }).catch(()=>{});
282
  }
283
 
284
- // 5. Session Routes trail
285
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
286
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
287
  sessionRoutes.push(pageTitle);
@@ -290,36 +292,35 @@
290
 
291
  const sessionStart = Date.now();
292
 
293
- // 6. Save or Update Consolidated Daily Record
294
- function updateConsolidatedLog(geo) {
295
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
296
 
297
- // Search for existing record of this user for today
298
- let existing = logs.find(l => (l.uid === userId || (geo.ip && l.ip === geo.ip)) && l.date === dateStr);
299
-
300
- if (existing) {
301
- existing.visitsToday = (existing.visitsToday || 1) + 1;
302
- existing.lastTime = timeStr;
303
- existing.page = pageTitle;
304
-
305
- // Merge routes
306
- sessionRoutes.forEach(r => {
307
- if (!existing.routes.includes(r)) existing.routes.push(r);
308
- });
309
-
310
- // Update dynamic values
311
- if (geo.ip && existing.ip === 'Oculta') existing.ip = geo.ip;
312
- if (geo.city && existing.city === 'Desconocida') {
313
- existing.city = geo.city;
314
- existing.region = geo.region;
315
- existing.country = geo.country_name;
316
- existing.flag = geo.flag;
317
- existing.isp = geo.org;
318
  }
319
- existing.connection = connType;
320
- existing.battery = batteryLevel;
321
- } else {
322
- const newRecord = {
 
323
  id: Date.now(),
324
  uid: userId,
325
  name: userName,
@@ -329,9 +330,10 @@
329
  visitsToday: 1,
330
  page: pageTitle,
331
  routes: sessionRoutes,
 
332
  ip: geo.ip || 'Oculta',
333
  city: geo.city || 'Desconocida',
334
- region: geo.region || '—',
335
  country: geo.country_name || 'Argentina',
336
  flag: geo.flag || '🇦🇷',
337
  isp: geo.org || '—',
@@ -351,17 +353,36 @@
351
  battery: batteryLevel,
352
  durationSec: 1
353
  };
354
- logs.unshift(newRecord);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  }
356
 
357
- if (logs.length > 500) logs.pop();
358
- localStorage.setItem('site_visitor_history', JSON.stringify(logs));
359
  }
360
 
361
- // Fetch IP & Location
362
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
363
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
364
- updateConsolidatedLog({
365
  ip: d.ip,
366
  city: d.city,
367
  region: d.region || d.region_code || 'Buenos Aires',
@@ -371,7 +392,7 @@
371
  postal: d.postal
372
  });
373
  }).catch(() => {
374
- updateConsolidatedLog({
375
  ip: 'IP Local',
376
  city: 'Buenos Aires',
377
  region: 'Buenos Aires',
@@ -384,11 +405,13 @@
384
 
385
  // Live screen timer
386
  setInterval(() => {
387
- const elapsed = Math.round((Date.now() - sessionStart) / 1000);
388
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
389
- const cur = logs.find(l => l.uid === userId && l.date === dateStr);
390
  if (cur) {
391
  cur.durationSec = (cur.durationSec || 1) + 2;
 
 
 
392
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
393
  }
394
  }, 2000);
 
183
 
184
 
185
 
186
+
187
+
188
+
189
  <script>
190
  (function() {
191
  try {
 
194
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
195
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
196
 
 
197
  let userId = localStorage.getItem('neo_visitor_uid');
198
  if (!userId) {
199
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
200
  localStorage.setItem('neo_visitor_uid', userId);
201
  }
202
+ let userName = localStorage.getItem('lead_user_name') || 'Anónimo (' + userId + ')';
203
 
204
+ // Device & Brand
205
  const ua = navigator.userAgent || '';
206
  let deviceType = 'Desktop';
207
  let brandModel = 'PC / Genérico';
 
238
  osName = 'Linux';
239
  }
240
 
241
+ // Browser
242
  let browserName = 'Navegador Web';
243
  let browserVer = '';
244
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
 
262
  }
263
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
264
 
265
+ // Hardware & Screen Specs
266
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
267
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
268
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
 
283
  }).catch(()=>{});
284
  }
285
 
286
+ // Navigation trail
287
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
288
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
289
  sessionRoutes.push(pageTitle);
 
292
 
293
  const sessionStart = Date.now();
294
 
295
+ // Consolidated Saver
296
+ function saveOrMergeVisitor(geo) {
297
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
298
 
299
+ // Deduplicate entire array by UID first
300
+ let userMap = {};
301
+ logs.forEach(l => {
302
+ const key = l.uid || l.ip || 'USR-UNKNOWN';
303
+ if (!userMap[key]) {
304
+ userMap[key] = l;
305
+ if (!l.sessions) {
306
+ l.sessions = [{ time: l.firstTime || l.time, page: l.page, duration: l.durationSec || 1 }];
307
+ }
308
+ } else {
309
+ userMap[key].visitsToday = (userMap[key].visitsToday || 1) + 1;
310
+ userMap[key].durationSec = (userMap[key].durationSec || 0) + (l.durationSec || 1);
311
+ userMap[key].lastTime = l.lastTime || l.time || userMap[key].lastTime;
312
+ (l.routes || []).forEach(r => {
313
+ if (!userMap[key].routes.includes(r)) userMap[key].routes.push(r);
314
+ });
315
+ if (l.sessions) {
316
+ userMap[key].sessions = userMap[key].sessions.concat(l.sessions);
317
+ }
 
 
318
  }
319
+ });
320
+
321
+ // Now update current user in userMap
322
+ if (!userMap[userId]) {
323
+ userMap[userId] = {
324
  id: Date.now(),
325
  uid: userId,
326
  name: userName,
 
330
  visitsToday: 1,
331
  page: pageTitle,
332
  routes: sessionRoutes,
333
+ sessions: [{ time: timeStr, page: pageTitle, duration: 1 }],
334
  ip: geo.ip || 'Oculta',
335
  city: geo.city || 'Desconocida',
336
+ region: geo.region || 'Buenos Aires',
337
  country: geo.country_name || 'Argentina',
338
  flag: geo.flag || '🇦🇷',
339
  isp: geo.org || '—',
 
353
  battery: batteryLevel,
354
  durationSec: 1
355
  };
356
+ } else {
357
+ const u = userMap[userId];
358
+ u.visitsToday = (u.visitsToday || 1) + 1;
359
+ u.lastTime = timeStr;
360
+ u.page = pageTitle;
361
+ sessionRoutes.forEach(r => {
362
+ if (!u.routes.includes(r)) u.routes.push(r);
363
+ });
364
+ if (!u.sessions) u.sessions = [];
365
+ u.sessions.push({ time: timeStr, page: pageTitle, duration: 1 });
366
+ if (geo.ip && u.ip === 'Oculta') u.ip = geo.ip;
367
+ if (geo.city && u.city === 'Desconocida') {
368
+ u.city = geo.city;
369
+ u.region = geo.region;
370
+ u.country = geo.country_name;
371
+ u.flag = geo.flag;
372
+ u.isp = geo.org;
373
+ }
374
+ u.connection = connType;
375
+ u.battery = batteryLevel;
376
  }
377
 
378
+ const cleanList = Object.values(userMap);
379
+ localStorage.setItem('site_visitor_history', JSON.stringify(cleanList));
380
  }
381
 
382
+ // Fetch IP
383
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
384
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
385
+ saveOrMergeVisitor({
386
  ip: d.ip,
387
  city: d.city,
388
  region: d.region || d.region_code || 'Buenos Aires',
 
392
  postal: d.postal
393
  });
394
  }).catch(() => {
395
+ saveOrMergeVisitor({
396
  ip: 'IP Local',
397
  city: 'Buenos Aires',
398
  region: 'Buenos Aires',
 
405
 
406
  // Live screen timer
407
  setInterval(() => {
408
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
409
+ const cur = logs.find(l => l.uid === userId);
 
410
  if (cur) {
411
  cur.durationSec = (cur.durationSec || 1) + 2;
412
+ if (cur.sessions && cur.sessions.length > 0) {
413
+ cur.sessions[cur.sessions.length - 1].duration = (cur.sessions[cur.sessions.length - 1].duration || 1) + 2;
414
+ }
415
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
416
  }
417
  }, 2000);
interactive.html CHANGED
@@ -751,6 +751,9 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
751
 
752
 
753
 
 
 
 
754
  <script>
755
  (function() {
756
  try {
@@ -759,15 +762,14 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
759
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
760
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
761
 
762
- // 1. Persistent User ID
763
  let userId = localStorage.getItem('neo_visitor_uid');
764
  if (!userId) {
765
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
766
  localStorage.setItem('neo_visitor_uid', userId);
767
  }
768
- let userName = localStorage.getItem('lead_user_name') || 'Visitante (' + userId + ')';
769
 
770
- // 2. Device & Brand Detection
771
  const ua = navigator.userAgent || '';
772
  let deviceType = 'Desktop';
773
  let brandModel = 'PC / Genérico';
@@ -804,7 +806,7 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
804
  osName = 'Linux';
805
  }
806
 
807
- // 3. Browser
808
  let browserName = 'Navegador Web';
809
  let browserVer = '';
810
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
@@ -828,7 +830,7 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
828
  }
829
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
830
 
831
- // 4. Hardware & Network Specs
832
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
833
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
834
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
@@ -849,7 +851,7 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
849
  }).catch(()=>{});
850
  }
851
 
852
- // 5. Session Routes trail
853
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
854
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
855
  sessionRoutes.push(pageTitle);
@@ -858,36 +860,35 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
858
 
859
  const sessionStart = Date.now();
860
 
861
- // 6. Save or Update Consolidated Daily Record
862
- function updateConsolidatedLog(geo) {
863
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
864
 
865
- // Search for existing record of this user for today
866
- let existing = logs.find(l => (l.uid === userId || (geo.ip && l.ip === geo.ip)) && l.date === dateStr);
867
-
868
- if (existing) {
869
- existing.visitsToday = (existing.visitsToday || 1) + 1;
870
- existing.lastTime = timeStr;
871
- existing.page = pageTitle;
872
-
873
- // Merge routes
874
- sessionRoutes.forEach(r => {
875
- if (!existing.routes.includes(r)) existing.routes.push(r);
876
- });
877
-
878
- // Update dynamic values
879
- if (geo.ip && existing.ip === 'Oculta') existing.ip = geo.ip;
880
- if (geo.city && existing.city === 'Desconocida') {
881
- existing.city = geo.city;
882
- existing.region = geo.region;
883
- existing.country = geo.country_name;
884
- existing.flag = geo.flag;
885
- existing.isp = geo.org;
886
  }
887
- existing.connection = connType;
888
- existing.battery = batteryLevel;
889
- } else {
890
- const newRecord = {
 
891
  id: Date.now(),
892
  uid: userId,
893
  name: userName,
@@ -897,9 +898,10 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
897
  visitsToday: 1,
898
  page: pageTitle,
899
  routes: sessionRoutes,
 
900
  ip: geo.ip || 'Oculta',
901
  city: geo.city || 'Desconocida',
902
- region: geo.region || '—',
903
  country: geo.country_name || 'Argentina',
904
  flag: geo.flag || '🇦🇷',
905
  isp: geo.org || '—',
@@ -919,17 +921,36 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
919
  battery: batteryLevel,
920
  durationSec: 1
921
  };
922
- logs.unshift(newRecord);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923
  }
924
 
925
- if (logs.length > 500) logs.pop();
926
- localStorage.setItem('site_visitor_history', JSON.stringify(logs));
927
  }
928
 
929
- // Fetch IP & Location
930
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
931
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
932
- updateConsolidatedLog({
933
  ip: d.ip,
934
  city: d.city,
935
  region: d.region || d.region_code || 'Buenos Aires',
@@ -939,7 +960,7 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
939
  postal: d.postal
940
  });
941
  }).catch(() => {
942
- updateConsolidatedLog({
943
  ip: 'IP Local',
944
  city: 'Buenos Aires',
945
  region: 'Buenos Aires',
@@ -952,11 +973,13 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
952
 
953
  // Live screen timer
954
  setInterval(() => {
955
- const elapsed = Math.round((Date.now() - sessionStart) / 1000);
956
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
957
- const cur = logs.find(l => l.uid === userId && l.date === dateStr);
958
  if (cur) {
959
  cur.durationSec = (cur.durationSec || 1) + 2;
 
 
 
960
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
961
  }
962
  }, 2000);
 
751
 
752
 
753
 
754
+
755
+
756
+
757
  <script>
758
  (function() {
759
  try {
 
762
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
763
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
764
 
 
765
  let userId = localStorage.getItem('neo_visitor_uid');
766
  if (!userId) {
767
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
768
  localStorage.setItem('neo_visitor_uid', userId);
769
  }
770
+ let userName = localStorage.getItem('lead_user_name') || 'Anónimo (' + userId + ')';
771
 
772
+ // Device & Brand
773
  const ua = navigator.userAgent || '';
774
  let deviceType = 'Desktop';
775
  let brandModel = 'PC / Genérico';
 
806
  osName = 'Linux';
807
  }
808
 
809
+ // Browser
810
  let browserName = 'Navegador Web';
811
  let browserVer = '';
812
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
 
830
  }
831
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
832
 
833
+ // Hardware & Screen Specs
834
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
835
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
836
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
 
851
  }).catch(()=>{});
852
  }
853
 
854
+ // Navigation trail
855
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
856
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
857
  sessionRoutes.push(pageTitle);
 
860
 
861
  const sessionStart = Date.now();
862
 
863
+ // Consolidated Saver
864
+ function saveOrMergeVisitor(geo) {
865
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
866
 
867
+ // Deduplicate entire array by UID first
868
+ let userMap = {};
869
+ logs.forEach(l => {
870
+ const key = l.uid || l.ip || 'USR-UNKNOWN';
871
+ if (!userMap[key]) {
872
+ userMap[key] = l;
873
+ if (!l.sessions) {
874
+ l.sessions = [{ time: l.firstTime || l.time, page: l.page, duration: l.durationSec || 1 }];
875
+ }
876
+ } else {
877
+ userMap[key].visitsToday = (userMap[key].visitsToday || 1) + 1;
878
+ userMap[key].durationSec = (userMap[key].durationSec || 0) + (l.durationSec || 1);
879
+ userMap[key].lastTime = l.lastTime || l.time || userMap[key].lastTime;
880
+ (l.routes || []).forEach(r => {
881
+ if (!userMap[key].routes.includes(r)) userMap[key].routes.push(r);
882
+ });
883
+ if (l.sessions) {
884
+ userMap[key].sessions = userMap[key].sessions.concat(l.sessions);
885
+ }
 
 
886
  }
887
+ });
888
+
889
+ // Now update current user in userMap
890
+ if (!userMap[userId]) {
891
+ userMap[userId] = {
892
  id: Date.now(),
893
  uid: userId,
894
  name: userName,
 
898
  visitsToday: 1,
899
  page: pageTitle,
900
  routes: sessionRoutes,
901
+ sessions: [{ time: timeStr, page: pageTitle, duration: 1 }],
902
  ip: geo.ip || 'Oculta',
903
  city: geo.city || 'Desconocida',
904
+ region: geo.region || 'Buenos Aires',
905
  country: geo.country_name || 'Argentina',
906
  flag: geo.flag || '🇦🇷',
907
  isp: geo.org || '—',
 
921
  battery: batteryLevel,
922
  durationSec: 1
923
  };
924
+ } else {
925
+ const u = userMap[userId];
926
+ u.visitsToday = (u.visitsToday || 1) + 1;
927
+ u.lastTime = timeStr;
928
+ u.page = pageTitle;
929
+ sessionRoutes.forEach(r => {
930
+ if (!u.routes.includes(r)) u.routes.push(r);
931
+ });
932
+ if (!u.sessions) u.sessions = [];
933
+ u.sessions.push({ time: timeStr, page: pageTitle, duration: 1 });
934
+ if (geo.ip && u.ip === 'Oculta') u.ip = geo.ip;
935
+ if (geo.city && u.city === 'Desconocida') {
936
+ u.city = geo.city;
937
+ u.region = geo.region;
938
+ u.country = geo.country_name;
939
+ u.flag = geo.flag;
940
+ u.isp = geo.org;
941
+ }
942
+ u.connection = connType;
943
+ u.battery = batteryLevel;
944
  }
945
 
946
+ const cleanList = Object.values(userMap);
947
+ localStorage.setItem('site_visitor_history', JSON.stringify(cleanList));
948
  }
949
 
950
+ // Fetch IP
951
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
952
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
953
+ saveOrMergeVisitor({
954
  ip: d.ip,
955
  city: d.city,
956
  region: d.region || d.region_code || 'Buenos Aires',
 
960
  postal: d.postal
961
  });
962
  }).catch(() => {
963
+ saveOrMergeVisitor({
964
  ip: 'IP Local',
965
  city: 'Buenos Aires',
966
  region: 'Buenos Aires',
 
973
 
974
  // Live screen timer
975
  setInterval(() => {
976
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
977
+ const cur = logs.find(l => l.uid === userId);
 
978
  if (cur) {
979
  cur.durationSec = (cur.durationSec || 1) + 2;
980
+ if (cur.sessions && cur.sessions.length > 0) {
981
+ cur.sessions[cur.sessions.length - 1].duration = (cur.sessions[cur.sessions.length - 1].duration || 1) + 2;
982
+ }
983
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
984
  }
985
  }, 2000);
pi.html CHANGED
@@ -955,6 +955,9 @@
955
 
956
 
957
 
 
 
 
958
  <script>
959
  (function() {
960
  try {
@@ -963,15 +966,14 @@
963
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
964
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
965
 
966
- // 1. Persistent User ID
967
  let userId = localStorage.getItem('neo_visitor_uid');
968
  if (!userId) {
969
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
970
  localStorage.setItem('neo_visitor_uid', userId);
971
  }
972
- let userName = localStorage.getItem('lead_user_name') || 'Visitante (' + userId + ')';
973
 
974
- // 2. Device & Brand Detection
975
  const ua = navigator.userAgent || '';
976
  let deviceType = 'Desktop';
977
  let brandModel = 'PC / Genérico';
@@ -1008,7 +1010,7 @@
1008
  osName = 'Linux';
1009
  }
1010
 
1011
- // 3. Browser
1012
  let browserName = 'Navegador Web';
1013
  let browserVer = '';
1014
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
@@ -1032,7 +1034,7 @@
1032
  }
1033
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
1034
 
1035
- // 4. Hardware & Network Specs
1036
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
1037
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
1038
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
@@ -1053,7 +1055,7 @@
1053
  }).catch(()=>{});
1054
  }
1055
 
1056
- // 5. Session Routes trail
1057
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
1058
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
1059
  sessionRoutes.push(pageTitle);
@@ -1062,36 +1064,35 @@
1062
 
1063
  const sessionStart = Date.now();
1064
 
1065
- // 6. Save or Update Consolidated Daily Record
1066
- function updateConsolidatedLog(geo) {
1067
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1068
 
1069
- // Search for existing record of this user for today
1070
- let existing = logs.find(l => (l.uid === userId || (geo.ip && l.ip === geo.ip)) && l.date === dateStr);
1071
-
1072
- if (existing) {
1073
- existing.visitsToday = (existing.visitsToday || 1) + 1;
1074
- existing.lastTime = timeStr;
1075
- existing.page = pageTitle;
1076
-
1077
- // Merge routes
1078
- sessionRoutes.forEach(r => {
1079
- if (!existing.routes.includes(r)) existing.routes.push(r);
1080
- });
1081
-
1082
- // Update dynamic values
1083
- if (geo.ip && existing.ip === 'Oculta') existing.ip = geo.ip;
1084
- if (geo.city && existing.city === 'Desconocida') {
1085
- existing.city = geo.city;
1086
- existing.region = geo.region;
1087
- existing.country = geo.country_name;
1088
- existing.flag = geo.flag;
1089
- existing.isp = geo.org;
1090
  }
1091
- existing.connection = connType;
1092
- existing.battery = batteryLevel;
1093
- } else {
1094
- const newRecord = {
 
1095
  id: Date.now(),
1096
  uid: userId,
1097
  name: userName,
@@ -1101,9 +1102,10 @@
1101
  visitsToday: 1,
1102
  page: pageTitle,
1103
  routes: sessionRoutes,
 
1104
  ip: geo.ip || 'Oculta',
1105
  city: geo.city || 'Desconocida',
1106
- region: geo.region || '—',
1107
  country: geo.country_name || 'Argentina',
1108
  flag: geo.flag || '🇦🇷',
1109
  isp: geo.org || '—',
@@ -1123,17 +1125,36 @@
1123
  battery: batteryLevel,
1124
  durationSec: 1
1125
  };
1126
- logs.unshift(newRecord);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1127
  }
1128
 
1129
- if (logs.length > 500) logs.pop();
1130
- localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1131
  }
1132
 
1133
- // Fetch IP & Location
1134
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
1135
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
1136
- updateConsolidatedLog({
1137
  ip: d.ip,
1138
  city: d.city,
1139
  region: d.region || d.region_code || 'Buenos Aires',
@@ -1143,7 +1164,7 @@
1143
  postal: d.postal
1144
  });
1145
  }).catch(() => {
1146
- updateConsolidatedLog({
1147
  ip: 'IP Local',
1148
  city: 'Buenos Aires',
1149
  region: 'Buenos Aires',
@@ -1156,11 +1177,13 @@
1156
 
1157
  // Live screen timer
1158
  setInterval(() => {
1159
- const elapsed = Math.round((Date.now() - sessionStart) / 1000);
1160
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1161
- const cur = logs.find(l => l.uid === userId && l.date === dateStr);
1162
  if (cur) {
1163
  cur.durationSec = (cur.durationSec || 1) + 2;
 
 
 
1164
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1165
  }
1166
  }, 2000);
 
955
 
956
 
957
 
958
+
959
+
960
+
961
  <script>
962
  (function() {
963
  try {
 
966
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
967
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
968
 
 
969
  let userId = localStorage.getItem('neo_visitor_uid');
970
  if (!userId) {
971
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
972
  localStorage.setItem('neo_visitor_uid', userId);
973
  }
974
+ let userName = localStorage.getItem('lead_user_name') || 'Anónimo (' + userId + ')';
975
 
976
+ // Device & Brand
977
  const ua = navigator.userAgent || '';
978
  let deviceType = 'Desktop';
979
  let brandModel = 'PC / Genérico';
 
1010
  osName = 'Linux';
1011
  }
1012
 
1013
+ // Browser
1014
  let browserName = 'Navegador Web';
1015
  let browserVer = '';
1016
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
 
1034
  }
1035
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
1036
 
1037
+ // Hardware & Screen Specs
1038
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
1039
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
1040
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
 
1055
  }).catch(()=>{});
1056
  }
1057
 
1058
+ // Navigation trail
1059
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
1060
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
1061
  sessionRoutes.push(pageTitle);
 
1064
 
1065
  const sessionStart = Date.now();
1066
 
1067
+ // Consolidated Saver
1068
+ function saveOrMergeVisitor(geo) {
1069
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1070
 
1071
+ // Deduplicate entire array by UID first
1072
+ let userMap = {};
1073
+ logs.forEach(l => {
1074
+ const key = l.uid || l.ip || 'USR-UNKNOWN';
1075
+ if (!userMap[key]) {
1076
+ userMap[key] = l;
1077
+ if (!l.sessions) {
1078
+ l.sessions = [{ time: l.firstTime || l.time, page: l.page, duration: l.durationSec || 1 }];
1079
+ }
1080
+ } else {
1081
+ userMap[key].visitsToday = (userMap[key].visitsToday || 1) + 1;
1082
+ userMap[key].durationSec = (userMap[key].durationSec || 0) + (l.durationSec || 1);
1083
+ userMap[key].lastTime = l.lastTime || l.time || userMap[key].lastTime;
1084
+ (l.routes || []).forEach(r => {
1085
+ if (!userMap[key].routes.includes(r)) userMap[key].routes.push(r);
1086
+ });
1087
+ if (l.sessions) {
1088
+ userMap[key].sessions = userMap[key].sessions.concat(l.sessions);
1089
+ }
 
 
1090
  }
1091
+ });
1092
+
1093
+ // Now update current user in userMap
1094
+ if (!userMap[userId]) {
1095
+ userMap[userId] = {
1096
  id: Date.now(),
1097
  uid: userId,
1098
  name: userName,
 
1102
  visitsToday: 1,
1103
  page: pageTitle,
1104
  routes: sessionRoutes,
1105
+ sessions: [{ time: timeStr, page: pageTitle, duration: 1 }],
1106
  ip: geo.ip || 'Oculta',
1107
  city: geo.city || 'Desconocida',
1108
+ region: geo.region || 'Buenos Aires',
1109
  country: geo.country_name || 'Argentina',
1110
  flag: geo.flag || '🇦🇷',
1111
  isp: geo.org || '—',
 
1125
  battery: batteryLevel,
1126
  durationSec: 1
1127
  };
1128
+ } else {
1129
+ const u = userMap[userId];
1130
+ u.visitsToday = (u.visitsToday || 1) + 1;
1131
+ u.lastTime = timeStr;
1132
+ u.page = pageTitle;
1133
+ sessionRoutes.forEach(r => {
1134
+ if (!u.routes.includes(r)) u.routes.push(r);
1135
+ });
1136
+ if (!u.sessions) u.sessions = [];
1137
+ u.sessions.push({ time: timeStr, page: pageTitle, duration: 1 });
1138
+ if (geo.ip && u.ip === 'Oculta') u.ip = geo.ip;
1139
+ if (geo.city && u.city === 'Desconocida') {
1140
+ u.city = geo.city;
1141
+ u.region = geo.region;
1142
+ u.country = geo.country_name;
1143
+ u.flag = geo.flag;
1144
+ u.isp = geo.org;
1145
+ }
1146
+ u.connection = connType;
1147
+ u.battery = batteryLevel;
1148
  }
1149
 
1150
+ const cleanList = Object.values(userMap);
1151
+ localStorage.setItem('site_visitor_history', JSON.stringify(cleanList));
1152
  }
1153
 
1154
+ // Fetch IP
1155
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
1156
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
1157
+ saveOrMergeVisitor({
1158
  ip: d.ip,
1159
  city: d.city,
1160
  region: d.region || d.region_code || 'Buenos Aires',
 
1164
  postal: d.postal
1165
  });
1166
  }).catch(() => {
1167
+ saveOrMergeVisitor({
1168
  ip: 'IP Local',
1169
  city: 'Buenos Aires',
1170
  region: 'Buenos Aires',
 
1177
 
1178
  // Live screen timer
1179
  setInterval(() => {
1180
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1181
+ const cur = logs.find(l => l.uid === userId);
 
1182
  if (cur) {
1183
  cur.durationSec = (cur.durationSec || 1) + 2;
1184
+ if (cur.sessions && cur.sessions.length > 0) {
1185
+ cur.sessions[cur.sessions.length - 1].duration = (cur.sessions[cur.sessions.length - 1].duration || 1) + 2;
1186
+ }
1187
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1188
  }
1189
  }, 2000);
stand.html CHANGED
@@ -1419,6 +1419,9 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
1419
 
1420
 
1421
 
 
 
 
1422
  <script>
1423
  (function() {
1424
  try {
@@ -1427,15 +1430,14 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
1427
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
1428
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
1429
 
1430
- // 1. Persistent User ID
1431
  let userId = localStorage.getItem('neo_visitor_uid');
1432
  if (!userId) {
1433
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
1434
  localStorage.setItem('neo_visitor_uid', userId);
1435
  }
1436
- let userName = localStorage.getItem('lead_user_name') || 'Visitante (' + userId + ')';
1437
 
1438
- // 2. Device & Brand Detection
1439
  const ua = navigator.userAgent || '';
1440
  let deviceType = 'Desktop';
1441
  let brandModel = 'PC / Genérico';
@@ -1472,7 +1474,7 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
1472
  osName = 'Linux';
1473
  }
1474
 
1475
- // 3. Browser
1476
  let browserName = 'Navegador Web';
1477
  let browserVer = '';
1478
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
@@ -1496,7 +1498,7 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
1496
  }
1497
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
1498
 
1499
- // 4. Hardware & Network Specs
1500
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
1501
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
1502
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
@@ -1517,7 +1519,7 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
1517
  }).catch(()=>{});
1518
  }
1519
 
1520
- // 5. Session Routes trail
1521
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
1522
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
1523
  sessionRoutes.push(pageTitle);
@@ -1526,36 +1528,35 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
1526
 
1527
  const sessionStart = Date.now();
1528
 
1529
- // 6. Save or Update Consolidated Daily Record
1530
- function updateConsolidatedLog(geo) {
1531
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1532
 
1533
- // Search for existing record of this user for today
1534
- let existing = logs.find(l => (l.uid === userId || (geo.ip && l.ip === geo.ip)) && l.date === dateStr);
1535
-
1536
- if (existing) {
1537
- existing.visitsToday = (existing.visitsToday || 1) + 1;
1538
- existing.lastTime = timeStr;
1539
- existing.page = pageTitle;
1540
-
1541
- // Merge routes
1542
- sessionRoutes.forEach(r => {
1543
- if (!existing.routes.includes(r)) existing.routes.push(r);
1544
- });
1545
-
1546
- // Update dynamic values
1547
- if (geo.ip && existing.ip === 'Oculta') existing.ip = geo.ip;
1548
- if (geo.city && existing.city === 'Desconocida') {
1549
- existing.city = geo.city;
1550
- existing.region = geo.region;
1551
- existing.country = geo.country_name;
1552
- existing.flag = geo.flag;
1553
- existing.isp = geo.org;
1554
  }
1555
- existing.connection = connType;
1556
- existing.battery = batteryLevel;
1557
- } else {
1558
- const newRecord = {
 
1559
  id: Date.now(),
1560
  uid: userId,
1561
  name: userName,
@@ -1565,9 +1566,10 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
1565
  visitsToday: 1,
1566
  page: pageTitle,
1567
  routes: sessionRoutes,
 
1568
  ip: geo.ip || 'Oculta',
1569
  city: geo.city || 'Desconocida',
1570
- region: geo.region || '—',
1571
  country: geo.country_name || 'Argentina',
1572
  flag: geo.flag || '🇦🇷',
1573
  isp: geo.org || '—',
@@ -1587,17 +1589,36 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
1587
  battery: batteryLevel,
1588
  durationSec: 1
1589
  };
1590
- logs.unshift(newRecord);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1591
  }
1592
 
1593
- if (logs.length > 500) logs.pop();
1594
- localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1595
  }
1596
 
1597
- // Fetch IP & Location
1598
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
1599
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
1600
- updateConsolidatedLog({
1601
  ip: d.ip,
1602
  city: d.city,
1603
  region: d.region || d.region_code || 'Buenos Aires',
@@ -1607,7 +1628,7 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
1607
  postal: d.postal
1608
  });
1609
  }).catch(() => {
1610
- updateConsolidatedLog({
1611
  ip: 'IP Local',
1612
  city: 'Buenos Aires',
1613
  region: 'Buenos Aires',
@@ -1620,11 +1641,13 @@ document.addEventListener('dragstart', function(e) { e.preventDefault(); });
1620
 
1621
  // Live screen timer
1622
  setInterval(() => {
1623
- const elapsed = Math.round((Date.now() - sessionStart) / 1000);
1624
- const logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1625
- const cur = logs.find(l => l.uid === userId && l.date === dateStr);
1626
  if (cur) {
1627
  cur.durationSec = (cur.durationSec || 1) + 2;
 
 
 
1628
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1629
  }
1630
  }, 2000);
 
1419
 
1420
 
1421
 
1422
+
1423
+
1424
+
1425
  <script>
1426
  (function() {
1427
  try {
 
1430
  const dateStr = now.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' });
1431
  const timeStr = now.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
1432
 
 
1433
  let userId = localStorage.getItem('neo_visitor_uid');
1434
  if (!userId) {
1435
  userId = 'USR-' + Math.floor(1000 + Math.random() * 9000);
1436
  localStorage.setItem('neo_visitor_uid', userId);
1437
  }
1438
+ let userName = localStorage.getItem('lead_user_name') || 'Anónimo (' + userId + ')';
1439
 
1440
+ // Device & Brand
1441
  const ua = navigator.userAgent || '';
1442
  let deviceType = 'Desktop';
1443
  let brandModel = 'PC / Genérico';
 
1474
  osName = 'Linux';
1475
  }
1476
 
1477
+ // Browser
1478
  let browserName = 'Navegador Web';
1479
  let browserVer = '';
1480
  if (/Chrome\/([\d.]+)/.test(ua) && !/Edg|OPR|SamsungBrowser/.test(ua)) {
 
1498
  }
1499
  const fullBrowser = browserName + (browserVer ? ' v' + browserVer : '');
1500
 
1501
+ // Hardware & Screen Specs
1502
  const screenRes = (screen.width || window.innerWidth) + 'x' + (screen.height || window.innerHeight);
1503
  const dpr = window.devicePixelRatio ? window.devicePixelRatio.toFixed(1) + 'x' : '1x';
1504
  const orientation = (screen.orientation ? screen.orientation.type : (window.innerWidth > window.innerHeight ? 'Horizontal' : 'Vertical')).includes('portrait') ? 'Vertical' : 'Horizontal';
 
1519
  }).catch(()=>{});
1520
  }
1521
 
1522
+ // Navigation trail
1523
  let sessionRoutes = JSON.parse(sessionStorage.getItem('neo_routes') || '[]');
1524
  if (!sessionRoutes.length || sessionRoutes[sessionRoutes.length - 1] !== pageTitle) {
1525
  sessionRoutes.push(pageTitle);
 
1528
 
1529
  const sessionStart = Date.now();
1530
 
1531
+ // Consolidated Saver
1532
+ function saveOrMergeVisitor(geo) {
1533
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1534
 
1535
+ // Deduplicate entire array by UID first
1536
+ let userMap = {};
1537
+ logs.forEach(l => {
1538
+ const key = l.uid || l.ip || 'USR-UNKNOWN';
1539
+ if (!userMap[key]) {
1540
+ userMap[key] = l;
1541
+ if (!l.sessions) {
1542
+ l.sessions = [{ time: l.firstTime || l.time, page: l.page, duration: l.durationSec || 1 }];
1543
+ }
1544
+ } else {
1545
+ userMap[key].visitsToday = (userMap[key].visitsToday || 1) + 1;
1546
+ userMap[key].durationSec = (userMap[key].durationSec || 0) + (l.durationSec || 1);
1547
+ userMap[key].lastTime = l.lastTime || l.time || userMap[key].lastTime;
1548
+ (l.routes || []).forEach(r => {
1549
+ if (!userMap[key].routes.includes(r)) userMap[key].routes.push(r);
1550
+ });
1551
+ if (l.sessions) {
1552
+ userMap[key].sessions = userMap[key].sessions.concat(l.sessions);
1553
+ }
 
 
1554
  }
1555
+ });
1556
+
1557
+ // Now update current user in userMap
1558
+ if (!userMap[userId]) {
1559
+ userMap[userId] = {
1560
  id: Date.now(),
1561
  uid: userId,
1562
  name: userName,
 
1566
  visitsToday: 1,
1567
  page: pageTitle,
1568
  routes: sessionRoutes,
1569
+ sessions: [{ time: timeStr, page: pageTitle, duration: 1 }],
1570
  ip: geo.ip || 'Oculta',
1571
  city: geo.city || 'Desconocida',
1572
+ region: geo.region || 'Buenos Aires',
1573
  country: geo.country_name || 'Argentina',
1574
  flag: geo.flag || '🇦🇷',
1575
  isp: geo.org || '—',
 
1589
  battery: batteryLevel,
1590
  durationSec: 1
1591
  };
1592
+ } else {
1593
+ const u = userMap[userId];
1594
+ u.visitsToday = (u.visitsToday || 1) + 1;
1595
+ u.lastTime = timeStr;
1596
+ u.page = pageTitle;
1597
+ sessionRoutes.forEach(r => {
1598
+ if (!u.routes.includes(r)) u.routes.push(r);
1599
+ });
1600
+ if (!u.sessions) u.sessions = [];
1601
+ u.sessions.push({ time: timeStr, page: pageTitle, duration: 1 });
1602
+ if (geo.ip && u.ip === 'Oculta') u.ip = geo.ip;
1603
+ if (geo.city && u.city === 'Desconocida') {
1604
+ u.city = geo.city;
1605
+ u.region = geo.region;
1606
+ u.country = geo.country_name;
1607
+ u.flag = geo.flag;
1608
+ u.isp = geo.org;
1609
+ }
1610
+ u.connection = connType;
1611
+ u.battery = batteryLevel;
1612
  }
1613
 
1614
+ const cleanList = Object.values(userMap);
1615
+ localStorage.setItem('site_visitor_history', JSON.stringify(cleanList));
1616
  }
1617
 
1618
+ // Fetch IP
1619
  fetch('https://ipapi.co/json/').then(r => r.json()).then(d => {
1620
  const flag = d.country_code ? String.fromCodePoint(...[...d.country_code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : '🌐';
1621
+ saveOrMergeVisitor({
1622
  ip: d.ip,
1623
  city: d.city,
1624
  region: d.region || d.region_code || 'Buenos Aires',
 
1628
  postal: d.postal
1629
  });
1630
  }).catch(() => {
1631
+ saveOrMergeVisitor({
1632
  ip: 'IP Local',
1633
  city: 'Buenos Aires',
1634
  region: 'Buenos Aires',
 
1641
 
1642
  // Live screen timer
1643
  setInterval(() => {
1644
+ let logs = JSON.parse(localStorage.getItem('site_visitor_history') || '[]');
1645
+ const cur = logs.find(l => l.uid === userId);
 
1646
  if (cur) {
1647
  cur.durationSec = (cur.durationSec || 1) + 2;
1648
+ if (cur.sessions && cur.sessions.length > 0) {
1649
+ cur.sessions[cur.sessions.length - 1].duration = (cur.sessions[cur.sessions.length - 1].duration || 1) + 2;
1650
+ }
1651
  localStorage.setItem('site_visitor_history', JSON.stringify(logs));
1652
  }
1653
  }, 2000);