case-montagna-data 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/build.js +323 -0
  2. package/data.json +227 -2001
  3. package/index.html +1268 -0
  4. package/package.json +3 -3
package/build.js ADDED
@@ -0,0 +1,323 @@
1
+ // Genera index.html dal file data.json — usato anche dagli aggiornamenti automatici
2
+ const fs = require('fs');
3
+ const DATA = JSON.parse(fs.readFileSync(__dirname + '/data.json', 'utf8'));
4
+ const L = DATA.listings;
5
+ const NEW_MS = 7*24*3600*1000;
6
+ const isNew = x => x.added && (Date.now() - new Date(x.added+'T00:00:00Z').getTime()) < NEW_MS;
7
+
8
+ const SL={rist:["s-rist","Ristrutturato"],buono:["s-buono","Buono stato"],abit:["s-abit","Abitabile"]};
9
+ const fmt=n=>n.toLocaleString("it-IT");
10
+ const esc=s=>s.replace(/&/g,"&amp;").replace(/</g,"&lt;");
11
+
12
+ const ACTS = `<div class="acts">
13
+ <button class="act fav" aria-label="Preferito" title="Segna come preferito"><svg viewBox="0 0 24 24"><path d="M12 21C7 16.6 3 13 3 8.9 3 6.2 5.2 4 7.9 4c1.6 0 3.1.8 4.1 2 1-1.2 2.5-2 4.1-2C18.8 4 21 6.2 21 8.9c0 4.1-4 7.7-9 12.1z"/></svg></button>
14
+ <button class="act del" aria-label="Nascondi" title="Nascondi annuncio"><svg viewBox="0 0 24 24"><path d="M4 7h16M9 7V5h6v2m-8 0 1 13h8l1-13M10 11v6M14 11v6"/></svg></button>
15
+ </div>`;
16
+
17
+ function card(x){
18
+ return `<div class="card${x.top?" top":""}${x.riserva?" riserva":""}" data-id="${esc(x.links[0][1])}" data-valle="${x.valle}" data-price="${x.price}" data-alt="${x.alt}" data-sqm="${x.sqm}" data-riserva="${x.riserva?1:0}">
19
+ ${ACTS}
20
+ <div class="valle">${x.valle} · ~${fmt(x.alt)} m slm</div>
21
+ <div class="loc">${esc(x.loc)}</div>
22
+ <div class="price">${fmt(x.price)} €</div>
23
+ <div class="meta"><span><b>${x.sqm} mq</b></span><span>${esc(x.tip)}</span><span><b>${Math.round(x.price/x.sqm)}</b> €/mq</span></div>
24
+ <div class="badges">${isNew(x)?'<span class="stato s-new">NUOVA</span>':''}<span class="stato ${SL[x.stato][0]}">${SL[x.stato][1]}</span>${x.lim?'<span class="stato s-lim">quota limite</span>':''}${x.garden?'<span class="stato s-gard">🌿 giardino/terreno</span>':''}</div>
25
+ ${x.note?`<div class="note">${esc(x.note)}</div>`:""}
26
+ <div class="links">${x.links.map(l=>`<a href="${l[1]}" target="_blank" rel="noopener">${esc(l[0])} →</a>`).join("")}</div>
27
+ </div>`;
28
+ }
29
+
30
+ function section(sec){
31
+ let d=L.filter(x=>x.sec===sec);
32
+ d.sort((a,b)=>a.price-b.price);
33
+ d.sort((a,b)=>(a.riserva?1:0)-(b.riserva?1:0));
34
+ d.sort((a,b)=>(isNew(b)?1:0)-(isNew(a)?1:0));
35
+ return d.map(card).join("\n");
36
+ }
37
+
38
+ const html = `<!DOCTYPE html>
39
+ <html lang="it">
40
+ <head>
41
+ <meta charset="UTF-8">
42
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
43
+ <title>Casa in montagna — Report ricerca</title>
44
+ <style>
45
+ :root{
46
+ --bg:#f6f5f1; --card:#ffffff; --ink:#2b2a26; --muted:#77746b;
47
+ --line:#e4e1d8; --accent:#3d5a45; --accent-soft:#e8efe9;
48
+ --gold:#a8823c; --gold-soft:#f4ecdc; --warn:#9a5b2f; --warn-soft:#f6e9dd;
49
+ }
50
+ *{box-sizing:border-box;margin:0;padding:0}
51
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--ink);padding:24px 16px 56px;line-height:1.45}
52
+ .wrap{max-width:1060px;margin:0 auto}
53
+ h1{font-size:23px;font-weight:700;letter-spacing:-.02em}
54
+ .sub{color:var(--muted);font-size:13px;margin-top:4px}
55
+ .criteria{display:flex;flex-wrap:wrap;gap:7px;margin:14px 0 18px}
56
+ .chip{background:var(--accent-soft);color:var(--accent);border-radius:999px;padding:5px 12px;font-size:12px;font-weight:600}
57
+ h2{font-size:18px;margin:26px 0 4px;color:var(--accent)}
58
+ h2 small{font-weight:500;color:var(--muted);font-size:13px}
59
+ .h2sub{font-size:12.5px;color:var(--muted);margin-bottom:12px}
60
+ .tabs{display:flex;gap:8px;margin:4px 0 12px}
61
+ .tab{flex:1;border:1px solid var(--line);background:var(--card);border-radius:12px;padding:11px 8px;font-size:13.5px;font-weight:650;color:var(--ink);text-align:center;text-decoration:none;cursor:pointer}
62
+ .tab small{display:block;font-weight:500;color:var(--muted);font-size:11px;margin-top:2px}
63
+ .tab.active{background:var(--accent);border-color:var(--accent);color:#fff}
64
+ .tab.active small{color:#dbe6dd}
65
+ body.js .sec.off{display:none}
66
+ .filters{display:none;flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;gap:8px;margin:4px 0 14px;padding-bottom:6px;scrollbar-width:none}
67
+ .filters.on{display:flex}
68
+ .filters::-webkit-scrollbar{display:none}
69
+ .fbtn{flex:0 0 auto;border:1px solid var(--line);background:var(--card);border-radius:999px;padding:9px 15px;font-size:13.5px;cursor:pointer;color:var(--ink)}
70
+ .fbtn.active{background:var(--accent);color:#fff;border-color:var(--accent)}
71
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px}
72
+ .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:15px 16px;display:flex;flex-direction:column;gap:8px;position:relative}
73
+ .card.hide{display:none}
74
+ .card.top::before{content:"★ consigliata";position:absolute;top:-9px;left:14px;background:var(--gold);color:#fff;font-size:11px;font-weight:700;border-radius:999px;padding:2px 10px}
75
+ .card.riserva{border-style:dashed}
76
+ .card.riserva::before{content:"con riserva";position:absolute;top:-9px;left:14px;background:var(--warn);color:#fff;font-size:11px;font-weight:700;border-radius:999px;padding:2px 10px}
77
+ .acts{position:absolute;top:9px;right:9px;display:flex;gap:2px;z-index:2}
78
+ .act{border:none;background:none;cursor:pointer;padding:6px;line-height:0;border-radius:8px}
79
+ .act:active{transform:scale(.88)}
80
+ .act svg{width:21px;height:21px;fill:none;stroke:#b9b4a7;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;transition:all .15s}
81
+ .act.fav.on svg{fill:#d64541;stroke:#d64541}
82
+ .act.del:hover svg{stroke:var(--warn)}
83
+ .card{padding-right:16px}
84
+ .card .valle{padding-right:72px}
85
+ .card.ghost{opacity:.55;border-style:dotted}
86
+ .confirm{position:absolute;top:6px;right:6px;background:var(--card);border:1px solid var(--warn);border-radius:10px;padding:7px 9px;font-size:12.5px;display:flex;gap:7px;align-items:center;z-index:3;box-shadow:0 2px 10px rgba(0,0,0,.12)}
87
+ .confirm button{border:1px solid var(--line);background:var(--bg);border-radius:7px;padding:4px 10px;font-size:12.5px;cursor:pointer;color:var(--ink)}
88
+ .confirm .yes{background:var(--warn);color:#fff;border-color:var(--warn)}
89
+ .fbtn.favchip.active{background:#d64541;border-color:#d64541;color:#fff}
90
+ .hidbar{display:none;font-size:12.5px;color:var(--muted);margin:-4px 0 12px}
91
+ .hidbar.on{display:block}
92
+ .hidbar a{color:var(--accent);font-weight:650;cursor:pointer;text-decoration:underline}
93
+ .price{font-size:21px;font-weight:750;letter-spacing:-.02em}
94
+ .loc{font-size:15px;font-weight:650}
95
+ .valle{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;font-weight:600}
96
+ .meta{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:13px;color:var(--muted)}
97
+ .meta b{color:var(--ink);font-weight:600}
98
+ .badges{display:flex;gap:6px;flex-wrap:wrap}
99
+ .stato{display:inline-block;font-size:11.5px;font-weight:700;border-radius:6px;padding:3px 8px}
100
+ .s-rist{background:var(--accent-soft);color:var(--accent)}
101
+ .s-buono{background:#e7ecf3;color:#3b5a7d}
102
+ .s-abit{background:var(--gold-soft);color:var(--gold)}
103
+ .s-lim{background:var(--warn-soft);color:var(--warn)}
104
+ .s-new{background:var(--accent);color:#fff;letter-spacing:.04em}
105
+ .s-gard{background:#e4efdc;color:#4a7031}
106
+ .note{font-size:13px;color:var(--muted)}
107
+ .links{display:flex;gap:6px 8px;flex-wrap:wrap;margin-top:auto;padding-top:6px}
108
+ .links a{font-size:13.5px;font-weight:650;color:var(--accent);text-decoration:none;padding:7px 11px;background:var(--accent-soft);border-radius:8px}
109
+ footer{margin-top:28px;font-size:12px;color:var(--muted);border-top:1px solid var(--line);padding-top:14px}
110
+ </style>
111
+ </head>
112
+ <body>
113
+ <div class="wrap">
114
+ <h1>Casa in montagna — report ricerca</h1>
115
+ <div class="sub">Aggiornato al ${DATA.updated_it || DATA.updated} · immobiliare.it, idealista, casa.it, subito.it, Gruppo Monviso + agenzie locali</div>
116
+ <div class="criteria">
117
+ <span class="chip">max 130.000 €</span>
118
+ <span class="chip">case indipendenti / 2-3 lati / porzioni ≥ 1000 m</span>
119
+ <span class="chip">alloggi solo ≥ 1500 m</span>
120
+ <span class="chip">no ruderi</span>
121
+ </div>
122
+
123
+ <div class="tabs" id="tabs">
124
+ <a class="tab active" data-t="case" href="#sec-case">Case indipendenti e porzioni<small>≥ 1000 m slm</small></a>
125
+ <a class="tab" data-t="apt" href="#sec-apt">Alloggi in quota<small>solo località ≥ 1500 m slm</small></a>
126
+ </div>
127
+
128
+ <div class="filters" id="filters">
129
+ <button class="fbtn active" data-v="all">Tutte le valli</button>
130
+ <button class="fbtn" data-v="Val Varaita">Varaita</button>
131
+ <button class="fbtn" data-v="Val Maira">Maira</button>
132
+ <button class="fbtn" data-v="Valle Po">Po</button>
133
+ <button class="fbtn" data-v="Val Chisone">Chisone</button>
134
+ <button class="fbtn" data-v="Alta Val Susa">Alta V. Susa</button>
135
+ <button class="fbtn" data-v="Valle Stura">Stura</button>
136
+ <button class="fbtn" data-v="Valle Orco">Orco</button>
137
+ <button class="fbtn" data-v="Monregalese">Monregalese</button>
138
+ <button class="fbtn favchip" data-f="fav">♥ Preferiti</button>
139
+ </div>
140
+ <div class="hidbar" id="hidbar"><span id="hidcount">0</span> <span id="hidlabel">annunci nascosti</span> · <a id="hidtoggle">mostra</a></div>
141
+
142
+ <div class="sec" id="sec-case">
143
+ <h2>Case indipendenti e porzioni <small>· ≥ 1000 m slm · ordinate per prezzo</small></h2>
144
+ <div class="h2sub">Le schede "con riserva" (bordo tratteggiato, in fondo) hanno la parte principale abitabile ma fienile/sottotetto/altre unità da recuperare.</div>
145
+ <div class="grid">
146
+ ${section("casa")}
147
+ </div>
148
+ </div>
149
+
150
+ <div class="sec" id="sec-apt">
151
+ <h2>Alloggi in quota <small>· solo località ≥ 1500 m slm · ordinati per prezzo</small></h2>
152
+ <div class="grid">
153
+ ${section("apt")}
154
+ </div>
155
+ </div>
156
+
157
+ <footer>
158
+ Cuori e annunci nascosti sono condivisi: chiunque apra questo link vede (e può modificare) gli stessi. Quote verificate su fonti pubbliche a livello di comune/frazione; per le borgate resta una stima da confermare prima del sopralluogo. Stato come dichiarato dall'inserzionista. Prezzi e disponibilità possono cambiare: verificare sempre sul portale.
159
+ </footer>
160
+ </div>
161
+ <script>
162
+ // Tab e filtri sono un extra: senza JavaScript la pagina mostra comunque tutto
163
+ // (i tab diventano semplici ancore alle due sezioni).
164
+ (function(){
165
+ document.body.classList.add("js");
166
+ var tabs=document.getElementById("tabs");
167
+ function show(t){
168
+ document.getElementById("sec-case").classList.toggle("off", t!=="case");
169
+ document.getElementById("sec-apt").classList.toggle("off", t!=="apt");
170
+ tabs.querySelectorAll(".tab").forEach(function(x){x.classList.toggle("active",x.dataset.t===t)});
171
+ }
172
+ tabs.addEventListener("click",function(e){
173
+ var b=e.target.closest(".tab"); if(!b)return;
174
+ e.preventDefault();
175
+ show(b.dataset.t);
176
+ window.scrollTo({top:0});
177
+ });
178
+ show("case");
179
+
180
+ // Preferiti e annunci nascosti CONDIVISI: salvati su Firebase, visibili a chiunque
181
+ // abbia il link. Se il database non risponde, si torna al salvataggio locale.
182
+ var DB='https://case-montagna-default-rtdb.europe-west1.firebasedatabase.app/shared';
183
+ var store={
184
+ get:function(k){try{return JSON.parse(localStorage.getItem(k))||[]}catch(e){return[]}},
185
+ set:function(k,v){try{localStorage.setItem(k,JSON.stringify(v))}catch(e){}}
186
+ };
187
+ function enc(id){return btoa(unescape(encodeURIComponent(id))).replace(/[+]/g,'-').replace(/[/]/g,'_').replace(/=+$/,'')}
188
+ var favs=store.get("cm_favs"), hidden=store.get("cm_hidden");
189
+ var remoteOk=false;
190
+ var curV="all", favOnly=false, showHidden=false;
191
+ function setRemote(kind,id,on){
192
+ if(!remoteOk) return;
193
+ fetch(DB+'/'+kind+'/'+enc(id)+'.json',{method:on?'PUT':'DELETE',body:on?JSON.stringify(id):undefined}).catch(function(){});
194
+ }
195
+ function syncFromRemote(){
196
+ return Promise.all([
197
+ fetch(DB+'/favs.json').then(function(r){return r.json()}),
198
+ fetch(DB+'/hidden.json').then(function(r){return r.json()})
199
+ ]).then(function(res){
200
+ remoteOk=true;
201
+ var f=res[0]||{}, h=res[1]||{};
202
+ try{
203
+ if(!localStorage.getItem('cm_migrated')){
204
+ store.get("cm_favs").forEach(function(id){var k=enc(id); if(!f[k]){f[k]=id; fetch(DB+'/favs/'+k+'.json',{method:'PUT',body:JSON.stringify(id)}).catch(function(){})}});
205
+ store.get("cm_hidden").forEach(function(id){var k=enc(id); if(!h[k]){h[k]=id; fetch(DB+'/hidden/'+k+'.json',{method:'PUT',body:JSON.stringify(id)}).catch(function(){})}});
206
+ localStorage.setItem('cm_migrated','1');
207
+ }
208
+ }catch(e){}
209
+ favs=Object.keys(f).map(function(k){return f[k]});
210
+ hidden=Object.keys(h).map(function(k){return h[k]});
211
+ update();
212
+ }).catch(function(){});
213
+ }
214
+
215
+ function update(){
216
+ var nHid=0;
217
+ document.querySelectorAll(".card").forEach(function(c){
218
+ var id=c.dataset.id;
219
+ var isFav=favs.indexOf(id)>=0, isHid=hidden.indexOf(id)>=0;
220
+ var fb=c.querySelector(".act.fav"); if(fb) fb.classList.toggle("on",isFav);
221
+ if(isHid) nHid++;
222
+ c.classList.toggle("ghost", isHid && showHidden);
223
+ var db=c.querySelector(".act.del");
224
+ if(db){db.setAttribute("aria-label", isHid?"Ripristina":"Nascondi"); db.setAttribute("title", isHid?"Ripristina annuncio":"Nascondi annuncio");}
225
+ var vis=(curV==="all"||c.dataset.valle===curV) && (!favOnly||isFav) && (!isHid||showHidden);
226
+ c.classList.toggle("hide",!vis);
227
+ });
228
+ var bar=document.getElementById("hidbar");
229
+ bar.classList.toggle("on", nHid>0);
230
+ document.getElementById("hidcount").textContent=nHid;
231
+ document.getElementById("hidlabel").textContent=nHid===1?"annuncio nascosto":"annunci nascosti";
232
+ document.getElementById("hidtoggle").textContent=showHidden?(nHid===1?"nascondilo di nuovo":"nascondili di nuovo"):"mostra";
233
+ }
234
+
235
+ var f=document.getElementById("filters");
236
+ f.classList.add("on");
237
+ f.addEventListener("click",function(e){
238
+ var b=e.target.closest(".fbtn"); if(!b)return;
239
+ if(b.dataset.f==="fav"){ favOnly=!favOnly; b.classList.toggle("active",favOnly); update(); return; }
240
+ curV=b.dataset.v;
241
+ f.querySelectorAll(".fbtn[data-v]").forEach(function(x){x.classList.toggle("active",x===b)});
242
+ update();
243
+ });
244
+
245
+ document.getElementById("hidtoggle").addEventListener("click",function(){ showHidden=!showHidden; update(); });
246
+ update();
247
+ syncFromRemote();
248
+ document.addEventListener("visibilitychange",function(){ if(document.visibilityState==="visible") syncFromRemote(); });
249
+
250
+ document.addEventListener("click",function(e){
251
+ var fav=e.target.closest(".act.fav");
252
+ if(fav){
253
+ var id=fav.closest(".card").dataset.id;
254
+ var i=favs.indexOf(id);
255
+ if(i>=0) favs.splice(i,1); else favs.push(id);
256
+ store.set("cm_favs",favs); setRemote('favs',id,i<0); update(); return;
257
+ }
258
+ var del=e.target.closest(".act.del");
259
+ if(del){
260
+ var c=del.closest(".card"), id=c.dataset.id;
261
+ if(hidden.indexOf(id)>=0){ hidden.splice(hidden.indexOf(id),1); store.set("cm_hidden",hidden); setRemote('hidden',id,false); update(); return; }
262
+ if(c.querySelector(".confirm")) return;
263
+ var box=document.createElement("div");
264
+ box.className="confirm";
265
+ box.innerHTML='Nascondere? <button class="yes">Sì</button> <button class="no">No</button>';
266
+ c.appendChild(box);
267
+ var t=setTimeout(function(){box.remove()},5000);
268
+ box.querySelector(".yes").addEventListener("click",function(ev){ev.stopPropagation();clearTimeout(t);box.remove();hidden.push(id);store.set("cm_hidden",hidden);setRemote('hidden',id,true);update();});
269
+ box.querySelector(".no").addEventListener("click",function(ev){ev.stopPropagation();clearTimeout(t);box.remove();});
270
+ }
271
+ });
272
+
273
+ // Aggiornamento live: la pagina prova a caricare i dati più recenti dal
274
+ // pacchetto npm "case-montagna-data". Se fallisce, restano le schede statiche.
275
+ var SL={rist:["s-rist","Ristrutturato"],buono:["s-buono","Buono stato"],abit:["s-abit","Abitabile"]};
276
+ function fmt(n){return n.toLocaleString("it-IT")}
277
+ function esc(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;")}
278
+ var ACTS='<div class="acts">'
279
+ +'<button class="act fav" aria-label="Preferito" title="Segna come preferito"><svg viewBox="0 0 24 24"><path d="M12 21C7 16.6 3 13 3 8.9 3 6.2 5.2 4 7.9 4c1.6 0 3.1.8 4.1 2 1-1.2 2.5-2 4.1-2C18.8 4 21 6.2 21 8.9c0 4.1-4 7.7-9 12.1z"/></svg></button>'
280
+ +'<button class="act del" aria-label="Nascondi" title="Nascondi annuncio"><svg viewBox="0 0 24 24"><path d="M4 7h16M9 7V5h6v2m-8 0 1 13h8l1-13M10 11v6M14 11v6"/></svg></button>'
281
+ +'</div>';
282
+ function isNew(x){return x.added && (Date.now()-new Date(x.added+'T00:00:00Z').getTime()) < 7*24*3600*1000}
283
+ function card(x){
284
+ return '<div class="card'+(x.top?' top':'')+(x.riserva?' riserva':'')+'" data-id="'+esc(x.links[0][1])+'" data-valle="'+esc(x.valle)+'">'
285
+ +ACTS
286
+ +'<div class="valle">'+esc(x.valle)+' · ~'+fmt(x.alt)+' m slm</div>'
287
+ +'<div class="loc">'+esc(x.loc)+'</div>'
288
+ +'<div class="price">'+fmt(x.price)+' €</div>'
289
+ +'<div class="meta"><span><b>'+x.sqm+' mq</b></span><span>'+esc(x.tip)+'</span><span><b>'+Math.round(x.price/x.sqm)+'</b> €/mq</span></div>'
290
+ +'<div class="badges">'+(isNew(x)?'<span class="stato s-new">NUOVA</span>':'')+'<span class="stato '+SL[x.stato][0]+'">'+SL[x.stato][1]+'</span>'+(x.lim?'<span class="stato s-lim">quota limite</span>':'')+(x.garden?'<span class="stato s-gard">🌿 giardino/terreno</span>':'')+'</div>'
291
+ +(x.note?'<div class="note">'+esc(x.note)+'</div>':'')
292
+ +'<div class="links">'+x.links.map(function(l){return '<a href="'+l[1]+'" target="_blank" rel="noopener">'+esc(l[0])+' →</a>'}).join('')+'</div>'
293
+ +'</div>';
294
+ }
295
+ function renderSec(d,sec,el){
296
+ var rows=d.listings.filter(function(x){return x.sec===sec});
297
+ rows.sort(function(a,b){return a.price-b.price});
298
+ rows.sort(function(a,b){return (a.riserva?1:0)-(b.riserva?1:0)});
299
+ rows.sort(function(a,b){return (isNew(b)?1:0)-(isNew(a)?1:0)});
300
+ el.innerHTML=rows.map(card).join("");
301
+ }
302
+ (function(){
303
+ fetch("https://registry.npmjs.org/case-montagna-data/latest").then(function(r){return r.json()}).then(function(meta){
304
+ var v=meta.version;
305
+ var urls=["https://cdn.jsdelivr.net/npm/case-montagna-data@"+v+"/data.json",
306
+ "https://unpkg.com/case-montagna-data@"+v+"/data.json"];
307
+ return urls.reduce(function(p,u){return p.catch(function(){return fetch(u).then(function(r){if(!r.ok)throw 0;return r.json()})})}, Promise.reject());
308
+ }).then(function(d){
309
+ if(!d||!d.listings||!d.listings.length) return;
310
+ renderSec(d,"casa",document.querySelector("#sec-case .grid"));
311
+ renderSec(d,"apt",document.querySelector("#sec-apt .grid"));
312
+ if(d.updated_it){var s=document.querySelector(".sub"); if(s) s.textContent="Aggiornato al "+d.updated_it+" · immobiliare.it, idealista, casa.it, subito.it, Gruppo Monviso + agenzie locali";}
313
+ update();
314
+ }).catch(function(){/* fallback statico */});
315
+ })();
316
+ })();
317
+ </script>
318
+ </body>
319
+ </html>
320
+ `;
321
+
322
+ fs.writeFileSync(__dirname + '/index.html', html);
323
+ console.log('OK, bytes:', html.length);