bep40 commited on
Commit
8f2a4a6
·
verified ·
1 Parent(s): 5d09ad3

v6.8: Fix page hang — remove VTV loadHome override (double-load), add cachedFetch to all API calls, debounce loadLivescore, global _homeLoaded guard

Browse files
Files changed (1) hide show
  1. static/vnews_all.js +36 -37
static/vnews_all.js CHANGED
@@ -26,10 +26,10 @@ function clearApiCache() { _apiCache.clear(); }
26
  // Updated: Voice selector + speed control + image gallery + auto voice detect
27
 
28
  // === LOAD HOME ===
29
- let _homeLoaded = false;
30
  let _homeLoading = false;
31
  async function loadHome(){
32
- if(_homeLoaded) return; // Already loaded, skip
33
  if(_homeLoading) return; // Currently loading, skip
34
  _homeLoading = true;
35
  // Load critical content first (featured + livescore HTML)
@@ -70,6 +70,9 @@ async function loadHome(){
70
  if(ai&&ai.length){h+='<div class="slider-wrap"><div class="slider-header"><span class="slider-label">🤖 Ứng dụng AI</span></div><div class="slider-track">';ai.slice(0,12).forEach(a=>{h+=`<div class="slider-item" onclick="readArticle('${esc(a.link)}')"><div class="slider-thumb">${a.img?`<img src="${a.img}">`:''}</div><div class="slider-title">${esc(a.title)}</div></div>`});h+='</div></div>';}
71
  document.getElementById('view-home').innerHTML=h;
72
  loadLivescore('today');loadHotTopics();
 
 
 
73
  if(_wc2026Data)switchWCTab('news');
74
  }
75
 
@@ -223,12 +226,18 @@ function renderSecondarySections(){
223
 
224
  function interleaveShorts(shorts){const dt=shorts.filter(s=>s.channel==='baodantri7941');const sk=shorts.filter(s=>s.channel==='baosuckhoedoisongboyte');const result=[];let i=0,j=0;while(i<dt.length||j<sk.length){if(i<dt.length)result.push(dt[i++]);if(j<sk.length)result.push(sk[j++]);}return result;}
225
  let _htPage=0,_htTopic='';
226
- async function loadHotTopics(){const j=await fetch('/api/hot_topics').then(r=>r.json()).catch(()=>({topics:[]}));const el=document.getElementById('hot-topics');if(!el)return;el.innerHTML=(j.topics||[]).slice(0,18).map(t=>{const topicText=t.topic||t.label.replace(/^#/,'');return`<button class="hot-chip" onclick="searchTopic('${topicText.replace(/'/g,"\\'")}')">${esc(t.label)}</button>`;}).join('');if(j.topics&&j.topics[0]){const firstTopic=j.topics[0].topic||j.topics[0].label.replace(/^#/,'');setTimeout(()=>searchTopic(firstTopic),800);}}
227
  function searchTopic(topic){if(!topic){topic=document.getElementById('topic-input')?.value.trim();if(!topic){alert('Nhập chủ đề');return;}}document.getElementById('topic-input').value='';_htTopic=topic;_htPage=0;showHashtagSources(topic,0);}
228
- async function showHashtagSources(topic,page){const box=document.getElementById('hashtag-box');if(!box)return;if(page===0)box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div class="hashtag-loading"><div class="hashtag-spinner"></div>Đang tìm...</div></div>`;try{const r=await fetch(`/api/hashtag/sources?topic=${encodeURIComponent(topic)}&page=${page}`);const j=await r.json();const sources=j.sources||[];if(!sources.length&&page===0){box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div style="color:#888;padding:8px">Không tìm được bài viết liên quan</div></div>`;return;}let h='';if(page===0)h=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)} <span style="font-size:10px;color:#888">(${j.total} bài từ 8 nguồn)</span></h3><div id="ht-list">`;sources.forEach((s,i)=>{const idx=page*8+i;h+=`<div class="hashtag-src-item" onclick="readArticle('${esc(s.url)}')"><div class="hashtag-src-img" id="ht-img-${idx}"></div><div class="hashtag-src-text"><div class="hashtag-src-title">${esc(s.title)}</div><div class="hashtag-src-via">${esc(s.via||'')}</div></div></div>`;});if(page===0){h+=`</div><button class="hashtag-rewrite-btn" onclick="rewriteHashtag('${esc(topic).replace(/'/g,"\\'")}')">🤖 Rewrite AI tổng hợp & đăng tường</button>`;if(j.has_more)h+=`<button class="hashtag-load-more" id="ht-more" onclick="loadMoreHashtag()">Tải thêm ▼</button>`;h+=`</div>`;box.innerHTML=h;}else{document.getElementById('ht-list')?.insertAdjacentHTML('beforeend',h);const btn=document.getElementById('ht-more');if(btn){if(!j.has_more)btn.remove();else{btn.disabled=false;btn.textContent='Tải thêm ▼';}}}sources.forEach((s,i)=>{const idx=page*8+i;if(!s.url)return;fetch('/api/article?url='+encodeURIComponent(s.url)).then(r=>r.json()).then(d=>{if(d&&(d.og_image||d.img)){const el=document.getElementById('ht-img-'+idx);if(el)el.innerHTML=`<img src="${esc(d.og_image||d.img)}" onerror="this.style.display='none'">`;}}).catch(()=>{});});}catch(e){box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div style="color:#e74c3c;padding:8px">Lỗi: ${esc(e.message)}</div></div>`;}}
229
  function loadMoreHashtag(){_htPage++;const btn=document.getElementById('ht-more');if(btn){btn.disabled=true;btn.textContent='Đang tải...';}showHashtagSources(_htTopic,_htPage);}
230
  async function rewriteHashtag(topic){const btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tổng hợp...';}try{const r=await fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic})});const j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');toast('✅ Đã đăng Tường AI!');if(btn)btn.textContent='✅ Đăng thành công!';if(j.post)prependWallPost(j.post);}catch(e){toast('❌ '+e.message);if(btn){btn.disabled=false;btn.textContent='🤖 Rewrite AI';}}}
231
- async function loadLivescore(tab){document.querySelectorAll('.ls-tab').forEach(t=>t.classList.remove('active'));document.querySelector(`.ls-tab[data-tab="${tab}"]`)?.classList.add('active');const el=document.getElementById('ls-content');if(!el)return;el.innerHTML='<div class="loading">Đang tải...</div>';let ep='/api/livescore/'+tab;if(tab.startsWith('bxh_'))ep='/api/livescore/standings/'+tab.replace('bxh_','');try{const r=await fetch(ep);const d=await r.json();el.innerHTML=d.html&&d.html.length>50?d.html:'<div class="loading">Không có dữ liệu</div>';bindMatchClicks(el);}catch(e){el.innerHTML='<div class="loading">Lỗi</div>';}}
 
 
 
 
 
 
232
  function bindMatchClicks(el){
233
  if(!el) return;
234
  // Handle direct <a> clicks inside match-detail (highest priority)
@@ -522,7 +531,7 @@ fetch('/api/storage_status').then(r=>r.json()).then(j=>{if(!j.persistent){const
522
 
523
  function updateMiniEpg(){
524
  if(!_currentCh) return;
525
- fetch('/api/vtv/epg/'+_currentCh).then(r=>r.json()).then(d=>{
526
  const progs = d.programs||[];
527
  const now = progs.find(p=>p.now);
528
  document.getElementById('vtv-mini-epg').textContent = now ? now.time+' '+now.title : (progs[0] ? progs[0].time+' '+progs[0].title : '');
@@ -864,26 +873,16 @@ fetch('/api/storage_status').then(r=>r.json()).then(j=>{if(!j.persistent){const
864
  });
865
  };
866
 
867
- // Override loadHome to inject VTV block
868
- const _origLoadHome = window.loadHome;
869
- if(_origLoadHome && !_origLoadHome.__vtvWrapped){
870
- window.loadHome = async function(){
871
- // Skip if already loaded (guard against double-wrap)
872
- if(window._homeLoaded) return;
873
- const old = document.getElementById('vtv-block');
874
- if(old) old.remove();
875
- _blockInserted = false;
876
- const r = await _origLoadHome.apply(this, arguments);
877
- try{
878
- pinBlock();
879
- // Inject MyTV section after VTV block
880
- setTimeout(()=>{ try{ injectMyTVBlock(); }catch(e){ console.warn('MyTV inject error:', e); } }, 500);
881
- setTimeout(()=>{ try{ injectMyTVBlock(); }catch(e){} }, 2000);
882
- }catch(e){ console.warn('VTV pinBlock error:', e); }
883
- return r;
884
- };
885
- window.loadHome.__vtvWrapped = true;
886
- }
887
  })();
888
 
889
  // ===== static/hot_multi.js =====
@@ -891,7 +890,7 @@ fetch('/api/storage_status').then(r=>r.json()).then(j=>{if(!j.persistent){const
891
  // This file loaded AFTER app_v2.js, overrides the function
892
 
893
  async function loadHotTopics(){
894
- const j=await fetch('/api/hot_topics').then(r=>r.json()).catch(()=>({topics:[]}));
895
  const el=document.getElementById('hot-topics');if(!el)return;
896
  const topics=j.topics||[];
897
  el.innerHTML=topics.slice(0,18).map(t=>{
@@ -911,7 +910,7 @@ async function loadMultiHashtag(topicList){
911
  box.innerHTML=`<div class="hashtag-sources"><h3>🔥 Tin HOT tổng hợp</h3><div class="hashtag-loading"><div class="hashtag-spinner"></div>Tổng hợp từ ${topicList.length} chủ đề nóng nhất...</div></div>`;
912
  try{
913
  const results=await Promise.all(topicList.map(topic=>
914
- fetch(`/api/hashtag/sources?topic=${encodeURIComponent(topic)}&page=0`).then(r=>r.json()).catch(()=>({sources:[]}))
915
  ));
916
  // Interleave from all topics
917
  const all=[];const seen=new Set();
@@ -932,7 +931,7 @@ async function loadMultiHashtag(topicList){
932
  h+=`</div></div>`;
933
  box.innerHTML=h;
934
  // Lazy load images
935
- all.slice(0,12).forEach((s,i)=>{if(!s.url)return;fetch('/api/article?url='+encodeURIComponent(s.url)).then(r=>r.json()).then(d=>{if(d&&(d.og_image||d.img)){const el=document.getElementById('ht-img-'+i);if(el)el.innerHTML=`<img src="${esc(d.og_image||d.img)}" onerror="this.style.display='none'">`;}}).catch(()=>{});});
936
  _htTopic=topicList[0];
937
  }catch(e){box.innerHTML=`<div class="hashtag-sources"><h3>🔥 Tin HOT</h3><div style="color:#e74c3c;padding:8px">Lỗi tải tin</div></div>`;}
938
  }
@@ -981,9 +980,9 @@ function startHomepageLive() {
981
  if (!document.getElementById('view-home')?.classList.contains('active')) return;
982
  _liveTick++;
983
  const lsTab = document.querySelector('.ls-tab.active');
984
- if (lsTab && lsTab.dataset.tab) loadLivescore(lsTab.dataset.tab);
985
  try {
986
- const f = await fetch('/api/livescore/featured').then(r => r.json()).catch(() => null);
987
  if (f && f.home) {
988
  const el = document.querySelector('.featured-match');
989
  if (el) {
@@ -1020,7 +1019,7 @@ function refreshHashtag() {
1020
  if (typeof _htTopic !== 'undefined' && _htTopic) {
1021
  const box = document.getElementById('hashtag-box');
1022
  if (box && box.innerHTML.length > 50) {
1023
- fetch(`/api/hashtag/sources?topic=${encodeURIComponent(_htTopic)}&page=0`)
1024
  .then(r => r.json())
1025
  .then(j => {
1026
  const sources = j.sources || [];
@@ -1034,7 +1033,7 @@ function refreshHashtag() {
1034
  list.innerHTML = h;
1035
  sources.forEach((s, i) => {
1036
  if (!s.url) return;
1037
- fetch('/api/article?url=' + encodeURIComponent(s.url)).then(r => r.json()).then(d => {
1038
  if (d && (d.og_image || d.img)) {
1039
  const el = document.getElementById('ht-img-' + i);
1040
  if (el) el.innerHTML = `<img src="${esc(d.og_image || d.img)}" onerror="this.style.display='none'">`;
@@ -1104,7 +1103,7 @@ function renderWCNews(el){
1104
  h+=`<div class="wc-news-item" onclick="readArticle('${esc(a.link)}')"><div class="wc-news-img" id="wcimg-${i}">${a.img?`<img src="${esc(a.img)}" onerror="this.parentElement.innerHTML=''">`:''}</div><div class="wc-news-text">${badge}<div class="wc-news-title">${esc(a.title)}</div><div class="wc-news-via">${esc(a.source||'')}</div></div></div>`;
1105
  });
1106
  h+='</div>';el.innerHTML=h;
1107
- combined.slice(0,20).forEach((a,i)=>{if(a.img)return;fetch('/api/article?url='+encodeURIComponent(a.link)).then(r=>r.json()).then(d=>{if(d&&(d.og_image||d.img)){const x=document.getElementById('wcimg-'+i);if(x)x.innerHTML=`<img src="${esc(d.og_image||d.img)}" onerror="this.parentElement.innerHTML=''">`;}}).catch(()=>{});});
1108
  }
1109
 
1110
  // === FIXTURES ===
@@ -1165,8 +1164,8 @@ async function renderWCHighlights(el){
1165
  try{
1166
  // Load both WC and Friendly highlights
1167
  const[wc,fr]=await Promise.all([
1168
- fetch('/api/highlights/world-cup').then(r=>r.json()).catch(()=>[]),
1169
- fetch('/api/highlights/friendly').then(r=>r.json()).catch(()=>[])
1170
  ]);
1171
  // Keep per-league index so the clicked card maps to the correct video in its own league array
1172
  const all=[...(wc||[]).map((v,j)=>({...v,_league:'WC',_li:j})),...(fr||[]).map((v,j)=>({...v,_league:'Friendly',_li:j}))];
@@ -1190,7 +1189,7 @@ function startWCLiveRefresh(){
1190
  _wcRefreshInterval=setInterval(async()=>{
1191
  if(!document.getElementById('view-home')?.classList.contains('active'))return;
1192
  try{
1193
- const data=await fetch('/api/wc2026').then(r=>r.json()).catch(()=>null);
1194
  if(data){_wc2026Data=data;const wcEl=document.getElementById('wc-content');if(wcEl){const tab=document.querySelector('.wc-tab.active');if(tab){const t=tab.textContent;if(t.includes('Tin'))renderWCNews(wcEl);else if(t.includes('Lịch'))renderWCFixtures(wcEl);else if(t.includes('BXH'))renderWCStandings(wcEl);else if(t.includes('Thống'))renderWCStats(wcEl);}}}
1195
  }catch(e){}
1196
  },90000);
 
26
  // Updated: Voice selector + speed control + image gallery + auto voice detect
27
 
28
  // === LOAD HOME ===
29
+ window._homeLoaded = window._homeLoaded || false;
30
  let _homeLoading = false;
31
  async function loadHome(){
32
+ if(window._homeLoaded) return; // Already loaded, skip
33
  if(_homeLoading) return; // Currently loading, skip
34
  _homeLoading = true;
35
  // Load critical content first (featured + livescore HTML)
 
70
  if(ai&&ai.length){h+='<div class="slider-wrap"><div class="slider-header"><span class="slider-label">🤖 Ứng dụng AI</span></div><div class="slider-track">';ai.slice(0,12).forEach(a=>{h+=`<div class="slider-item" onclick="readArticle('${esc(a.link)}')"><div class="slider-thumb">${a.img?`<img src="${a.img}">`:''}</div><div class="slider-title">${esc(a.title)}</div></div>`});h+='</div></div>';}
71
  document.getElementById('view-home').innerHTML=h;
72
  loadLivescore('today');loadHotTopics();
73
+ window._homeLoaded = true;
74
+ // Inject VTV block after home loaded
75
+ if(window._vtvInject) window._vtvInject();
76
  if(_wc2026Data)switchWCTab('news');
77
  }
78
 
 
226
 
227
  function interleaveShorts(shorts){const dt=shorts.filter(s=>s.channel==='baodantri7941');const sk=shorts.filter(s=>s.channel==='baosuckhoedoisongboyte');const result=[];let i=0,j=0;while(i<dt.length||j<sk.length){if(i<dt.length)result.push(dt[i++]);if(j<sk.length)result.push(sk[j++]);}return result;}
228
  let _htPage=0,_htTopic='';
229
+ async function loadHotTopics(){const j=await cachedFetch('/api/hot_topics', 60000).catch(()=>({topics:[]}));const el=document.getElementById('hot-topics');if(!el)return;el.innerHTML=(j.topics||[]).slice(0,18).map(t=>{const topicText=t.topic||t.label.replace(/^#/,'');return`<button class="hot-chip" onclick="searchTopic('${topicText.replace(/'/g,"\\'")}')">${esc(t.label)}</button>`;}).join('');if(j.topics&&j.topics[0]){const firstTopic=j.topics[0].topic||j.topics[0].label.replace(/^#/,'');setTimeout(()=>searchTopic(firstTopic),800);}}
230
  function searchTopic(topic){if(!topic){topic=document.getElementById('topic-input')?.value.trim();if(!topic){alert('Nhập chủ đề');return;}}document.getElementById('topic-input').value='';_htTopic=topic;_htPage=0;showHashtagSources(topic,0);}
231
+ async function showHashtagSources(topic,page){const box=document.getElementById('hashtag-box');if(!box)return;if(page===0)box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div class="hashtag-loading"><div class="hashtag-spinner"></div>Đang tìm...</div></div>`;try{const j=await cachedFetch(`/api/hashtag/sources?topic=${encodeURIComponent(topic)}&page=${page}`, 60000);const sources=j.sources||[];if(!sources.length&&page===0){box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div style="color:#888;padding:8px">Không tìm được bài viết liên quan</div></div>`;return;}let h='';if(page===0)h=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)} <span style="font-size:10px;color:#888">(${j.total} bài từ 8 nguồn)</span></h3><div id="ht-list">`;sources.forEach((s,i)=>{const idx=page*8+i;h+=`<div class="hashtag-src-item" onclick="readArticle('${esc(s.url)}')"><div class="hashtag-src-img" id="ht-img-${idx}"></div><div class="hashtag-src-text"><div class="hashtag-src-title">${esc(s.title)}</div><div class="hashtag-src-via">${esc(s.via||'')}</div></div></div>`;});if(page===0){h+=`</div><button class="hashtag-rewrite-btn" onclick="rewriteHashtag('${esc(topic).replace(/'/g,"\\'")}')">🤖 Rewrite AI tổng hợp & đăng tường</button>`;if(j.has_more)h+=`<button class="hashtag-load-more" id="ht-more" onclick="loadMoreHashtag()">Tải thêm ▼</button>`;h+=`</div>`;box.innerHTML=h;}else{document.getElementById('ht-list')?.insertAdjacentHTML('beforeend',h);const btn=document.getElementById('ht-more');if(btn){if(!j.has_more)btn.remove();else{btn.disabled=false;btn.textContent='Tải thêm ▼';}}}sources.forEach((s,i)=>{const idx=page*8+i;if(!s.url)return;cachedFetch('/api/article?url='+encodeURIComponent(s.url), 120000).then(d=>{if(d&&(d.og_image||d.img)){const el=document.getElementById('ht-img-'+idx);if(el)el.innerHTML=`<img src="${esc(d.og_image||d.img)}" onerror="this.style.display='none'">`;}}).catch(()=>{});});}catch(e){box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div style="color:#e74c3c;padding:8px">Lỗi: ${esc(e.message)}</div></div>`;}}
232
  function loadMoreHashtag(){_htPage++;const btn=document.getElementById('ht-more');if(btn){btn.disabled=true;btn.textContent='Đang tải...';}showHashtagSources(_htTopic,_htPage);}
233
  async function rewriteHashtag(topic){const btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tổng hợp...';}try{const r=await fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic})});const j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');toast('✅ Đã đăng Tường AI!');if(btn)btn.textContent='✅ Đăng thành công!';if(j.post)prependWallPost(j.post);}catch(e){toast('❌ '+e.message);if(btn){btn.disabled=false;btn.textContent='🤖 Rewrite AI';}}}
234
+ // Debounce livescore calls
235
+ let _lsLoading = false;
236
+ async function loadLivescore(tab){
237
+ if(_lsLoading) return; // Skip if already loading
238
+ _lsLoading = true;
239
+ setTimeout(()=>{_lsLoading=false;}, 500);
240
+ document.querySelectorAll('.ls-tab').forEach(t=>t.classList.remove('active'));document.querySelector(`.ls-tab[data-tab="${tab}"]`)?.classList.add('active');const el=document.getElementById('ls-content');if(!el)return;el.innerHTML='<div class="loading">Đang tải...</div>';let ep='/api/livescore/'+tab;if(tab.startsWith('bxh_'))ep='/api/livescore/standings/'+tab.replace('bxh_','');try{const d=await cachedFetch(ep, 30000);el.innerHTML=d.html&&d.html.length>50?d.html:'<div class="loading">Không có dữ liệu</div>';bindMatchClicks(el);}catch(e){el.innerHTML='<div class="loading">Lỗi</div>';}}
241
  function bindMatchClicks(el){
242
  if(!el) return;
243
  // Handle direct <a> clicks inside match-detail (highest priority)
 
531
 
532
  function updateMiniEpg(){
533
  if(!_currentCh) return;
534
+ cachedFetch('/api/vtv/epg/'+_currentCh, 60000).then(d=>{
535
  const progs = d.programs||[];
536
  const now = progs.find(p=>p.now);
537
  document.getElementById('vtv-mini-epg').textContent = now ? now.time+' '+now.title : (progs[0] ? progs[0].time+' '+progs[0].title : '');
 
873
  });
874
  };
875
 
876
+ // Inject VTV block AFTER loadHome completes (no override to avoid double-load)
877
+ // Use a one-time listener pattern
878
+ window._vtvInject = function(){
879
+ if(!window._homeLoaded) return; // loadHome not done yet
880
+ pinBlock();
881
+ // Inject MyTV section after VTV block (once)
882
+ if(!_mytvInserted){
883
+ setTimeout(()=>{ try{ injectMyTVBlock(); }catch(e){} }, 500);
884
+ }
885
+ };
 
 
 
 
 
 
 
 
 
 
886
  })();
887
 
888
  // ===== static/hot_multi.js =====
 
890
  // This file loaded AFTER app_v2.js, overrides the function
891
 
892
  async function loadHotTopics(){
893
+ const j=await cachedFetch('/api/hot_topics', 60000).catch(()=>({topics:[]}));
894
  const el=document.getElementById('hot-topics');if(!el)return;
895
  const topics=j.topics||[];
896
  el.innerHTML=topics.slice(0,18).map(t=>{
 
910
  box.innerHTML=`<div class="hashtag-sources"><h3>🔥 Tin HOT tổng hợp</h3><div class="hashtag-loading"><div class="hashtag-spinner"></div>Tổng hợp từ ${topicList.length} chủ đề nóng nhất...</div></div>`;
911
  try{
912
  const results=await Promise.all(topicList.map(topic=>
913
+ cachedFetch(`/api/hashtag/sources?topic=${encodeURIComponent(topic)}&page=0`, 60000).catch(()=>({sources:[]}))
914
  ));
915
  // Interleave from all topics
916
  const all=[];const seen=new Set();
 
931
  h+=`</div></div>`;
932
  box.innerHTML=h;
933
  // Lazy load images
934
+ all.slice(0,12).forEach((s,i)=>{if(!s.url)return;cachedFetch('/api/article?url='+encodeURIComponent(s.url), 120000).then(d=>{if(d&&(d.og_image||d.img)){const el=document.getElementById('ht-img-'+i);if(el)el.innerHTML=`<img src="${esc(d.og_image||d.img)}" onerror="this.style.display='none'">`;}}).catch(()=>{});});
935
  _htTopic=topicList[0];
936
  }catch(e){box.innerHTML=`<div class="hashtag-sources"><h3>🔥 Tin HOT</h3><div style="color:#e74c3c;padding:8px">Lỗi tải tin</div></div>`;}
937
  }
 
980
  if (!document.getElementById('view-home')?.classList.contains('active')) return;
981
  _liveTick++;
982
  const lsTab = document.querySelector('.ls-tab.active');
983
+ // loadLivescore already called in loadHome, skip to avoid duplicate
984
  try {
985
+ const f = await cachedFetch('/api/livescore/featured', 30000).catch(() => null);
986
  if (f && f.home) {
987
  const el = document.querySelector('.featured-match');
988
  if (el) {
 
1019
  if (typeof _htTopic !== 'undefined' && _htTopic) {
1020
  const box = document.getElementById('hashtag-box');
1021
  if (box && box.innerHTML.length > 50) {
1022
+ cachedFetch(`/api/hashtag/sources?topic=${encodeURIComponent(_htTopic)}&page=0`, 60000)
1023
  .then(r => r.json())
1024
  .then(j => {
1025
  const sources = j.sources || [];
 
1033
  list.innerHTML = h;
1034
  sources.forEach((s, i) => {
1035
  if (!s.url) return;
1036
+ cachedFetch('/api/article?url=' + encodeURIComponent(s.url), 120000).then(d => {
1037
  if (d && (d.og_image || d.img)) {
1038
  const el = document.getElementById('ht-img-' + i);
1039
  if (el) el.innerHTML = `<img src="${esc(d.og_image || d.img)}" onerror="this.style.display='none'">`;
 
1103
  h+=`<div class="wc-news-item" onclick="readArticle('${esc(a.link)}')"><div class="wc-news-img" id="wcimg-${i}">${a.img?`<img src="${esc(a.img)}" onerror="this.parentElement.innerHTML=''">`:''}</div><div class="wc-news-text">${badge}<div class="wc-news-title">${esc(a.title)}</div><div class="wc-news-via">${esc(a.source||'')}</div></div></div>`;
1104
  });
1105
  h+='</div>';el.innerHTML=h;
1106
+ combined.slice(0,20).forEach((a,i)=>{if(a.img)return;cachedFetch('/api/article?url='+encodeURIComponent(a.link), 120000).then(d=>{a.img=d.image;return d;}).then(r=>r.json()).then(d=>{if(d&&(d.og_image||d.img)){const x=document.getElementById('wcimg-'+i);if(x)x.innerHTML=`<img src="${esc(d.og_image||d.img)}" onerror="this.parentElement.innerHTML=''">`;}}).catch(()=>{});});
1107
  }
1108
 
1109
  // === FIXTURES ===
 
1164
  try{
1165
  // Load both WC and Friendly highlights
1166
  const[wc,fr]=await Promise.all([
1167
+ cachedFetch('/api/highlights/world-cup', 120000).catch(()=>[]),
1168
+ cachedFetch('/api/highlights/friendly', 120000).catch(()=>[])
1169
  ]);
1170
  // Keep per-league index so the clicked card maps to the correct video in its own league array
1171
  const all=[...(wc||[]).map((v,j)=>({...v,_league:'WC',_li:j})),...(fr||[]).map((v,j)=>({...v,_league:'Friendly',_li:j}))];
 
1189
  _wcRefreshInterval=setInterval(async()=>{
1190
  if(!document.getElementById('view-home')?.classList.contains('active'))return;
1191
  try{
1192
+ const data=await cachedFetch('/api/wc2026', 300000).catch(()=>null);
1193
  if(data){_wc2026Data=data;const wcEl=document.getElementById('wc-content');if(wcEl){const tab=document.querySelector('.wc-tab.active');if(tab){const t=tab.textContent;if(t.includes('Tin'))renderWCNews(wcEl);else if(t.includes('Lịch'))renderWCFixtures(wcEl);else if(t.includes('BXH'))renderWCStandings(wcEl);else if(t.includes('Thống'))renderWCStats(wcEl);}}}
1194
  }catch(e){}
1195
  },90000);