|
505 | 505 | <div> |
506 | 506 | <div style="font-size:12px;color:var(--text)" id="settings-username">—</div> |
507 | 507 | <div style="font-size:11px;color:var(--text-muted);margin-top:2px">GitHub OAuth session — 30 day token</div> |
| 508 | + <div style="margin-top:8px;font-size:11px;color:var(--text-muted)"> |
| 509 | + public profile: <a id="settings-public-url" href="#" target="_blank" style="color:var(--blue);text-decoration:none">—</a> |
| 510 | + <button onclick="copyPublicURL()" style="background:transparent;border:1px solid var(--border);color:var(--text-muted);font-family:var(--font-mono);font-size:10px;padding:2px 8px;cursor:pointer;margin-left:8px;letter-spacing:.5px" id="copy-pub-btn">copy</button> |
| 511 | + </div> |
508 | 512 | </div> |
509 | 513 | <button class="btn-outline danger" onclick="logout()">[ logout ]</button> |
510 | 514 | </div> |
|
680 | 684 | window.location.href=`${API_BASE}/auth/github/login`; |
681 | 685 | } |
682 | 686 |
|
| 687 | +function copyPublicURL(){ |
| 688 | + const url=window._publicURL||document.getElementById('settings-public-url')?.href; |
| 689 | + if(!url||url==='#')return; |
| 690 | + navigator.clipboard.writeText(url).then(()=>{ |
| 691 | + const btn=document.getElementById('copy-pub-btn'); |
| 692 | + if(btn){btn.textContent='copied!';setTimeout(()=>btn.textContent='copy',2000);} |
| 693 | + }); |
| 694 | +} |
| 695 | + |
683 | 696 | function logout(){ |
684 | 697 | confirm('Log out?') && (AUTH_TOKEN='',ghToken='',userData={},repoData=[],eventData=[],langMap={},todos=[],plans=[],savedKeys=[], |
685 | 698 | localStorage.removeItem('dd_token'),showScreen('login')); |
|
785 | 798 | document.getElementById('meta-repos').textContent=userData.public_repos||0; |
786 | 799 | document.getElementById('meta-followers').textContent=fmtNum(userData.followers||0); |
787 | 800 | document.getElementById('sb-user').textContent=userData.login; |
| 801 | + // wire public profile URL |
| 802 | + if(userData.public_slug){ |
| 803 | + const cleanPub=new URL('profile.html', window.location.href).href + '?u=' + userData.public_slug; |
| 804 | + const el=document.getElementById('settings-public-url'); |
| 805 | + if(el){el.href=cleanPub;el.textContent=cleanPub;} |
| 806 | + window._publicURL=cleanPub; |
| 807 | + } |
788 | 808 | document.getElementById('settings-username').textContent=`@${userData.login}`; |
789 | 809 |
|
790 | 810 | const stars=repoData.reduce((a,r)=>a+(r.stargazers_count||0),0); |
|
1009 | 1029 | const langs=Object.entries(langMap).sort((a,b)=>b[1]-a[1]).slice(0,6).map(([l])=>l); |
1010 | 1030 | const topRepos=[...repoData].sort((a,b)=>b.stargazers_count-a.stargazers_count).slice(0,5).map(r=>`${r.name}(★${r.stargazers_count},${r.language||'?'})`); |
1011 | 1031 | const stats={stars:repoData.reduce((a,r)=>a+(r.stargazers_count||0),0),forks:repoData.reduce((a,r)=>a+(r.forks_count||0),0),repos:userData.public_repos,followers:userData.followers}; |
1012 | | - const data=await api('POST','/api/insights',{languages:langs,top_repos:topRepos,stats,username:userData.login}); |
| 1032 | + const controller=new AbortController(); |
| 1033 | + const insightTimeout=setTimeout(()=>controller.abort(),15000); |
| 1034 | + let data; |
| 1035 | + try{ |
| 1036 | + const r=await fetch(API_BASE+'/api/insights',{method:'POST',headers:{'Content-Type':'application/json','Authorization':`Bearer ${AUTH_TOKEN}`},body:JSON.stringify({languages:langs,top_repos:topRepos,stats,username:userData.login}),signal:controller.signal}); |
| 1037 | + data=await r.json(); |
| 1038 | + }catch(e){ |
| 1039 | + clearTimeout(insightTimeout); |
| 1040 | + document.getElementById('insights-output').innerHTML=`<div style="color:var(--red);font-size:12px">// ${e.name==='AbortError'?'request timed out after 15s':e.message}</div>`; |
| 1041 | + return; |
| 1042 | + } |
| 1043 | + clearTimeout(insightTimeout); |
1013 | 1044 | if(data.error){document.getElementById('insights-output').innerHTML=`<div style="color:var(--red);font-size:12px">// ${data.message||data.error}</div>`;return} |
1014 | 1045 | document.getElementById('insights-output').innerHTML=(data.insights||[]).map(i=>`<div class="ai-line"><span class="ai-arrow">→</span><span>${i.insight}</span></div>`).join(''); |
1015 | 1046 | } |
|
1020 | 1051 | const langs=Object.entries(langMap).sort((a,b)=>b[1]-a[1]).slice(0,4).map(([l])=>l); |
1021 | 1052 | const userRepoNames=new Set(repoData.map(r=>r.full_name||r.name)); |
1022 | 1053 |
|
1023 | | - // Large topic pool — pick 3 random ones each refresh |
1024 | | - const ALL_TOPICS = [ |
1025 | | - 'topic:cli','topic:tool','topic:bot','topic:security','topic:hacking', |
1026 | | - 'topic:iot','topic:monitoring','topic:dashboard','topic:terminal', |
1027 | | - 'topic:automation','topic:devops','topic:api','topic:game','topic:productivity', |
1028 | | - 'topic:reverse-engineering','topic:penetration-testing','topic:fuzzing', |
1029 | | - 'topic:network','topic:linux','topic:raspberry-pi','topic:arduino', |
1030 | | - 'topic:machine-learning','topic:data-science','topic:visualization', |
1031 | | - 'topic:compiler','topic:interpreter','topic:editor','topic:shell', |
| 1054 | + // Topic buckets — each bucket has related topics, pick 1 topic per query (OR logic) |
| 1055 | + const TOPIC_BUCKETS = [ |
| 1056 | + ['topic:security','topic:hacking','topic:penetration-testing','topic:ctf','topic:exploit'], |
| 1057 | + ['topic:cli','topic:terminal','topic:shell','topic:command-line'], |
| 1058 | + ['topic:tool','topic:developer-tools','topic:productivity','topic:automation'], |
| 1059 | + ['topic:iot','topic:arduino','topic:raspberry-pi','topic:embedded'], |
| 1060 | + ['topic:monitoring','topic:dashboard','topic:devops','topic:sysadmin'], |
| 1061 | + ['topic:bot','topic:chatbot','topic:telegram-bot','topic:discord-bot'], |
| 1062 | + ['topic:game','topic:gamedev','topic:puzzle'], |
| 1063 | + ['topic:network','topic:networking','topic:proxy','topic:vpn'], |
| 1064 | + ['topic:reverse-engineering','topic:malware','topic:forensics'], |
| 1065 | + ['topic:api','topic:rest-api','topic:graphql'], |
1032 | 1066 | ]; |
1033 | 1067 |
|
1034 | | - // Shuffle helper |
1035 | 1068 | function shuffle(arr) { |
1036 | 1069 | const a = [...arr]; |
1037 | | - for (let i = a.length-1; i > 0; i--) { |
1038 | | - const j = Math.floor(Math.random() * (i+1)); |
1039 | | - [a[i],a[j]] = [a[j],a[i]]; |
1040 | | - } |
| 1070 | + for (let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];} |
1041 | 1071 | return a; |
1042 | 1072 | } |
1043 | 1073 |
|
1044 | | - // Random star threshold: 100–1000 |
1045 | | - function randStars() { return [100,200,300,500,1000][Math.floor(Math.random()*5)]; } |
1046 | | - // Random sort |
1047 | | - function randSort() { return ['updated','stars'][Math.floor(Math.random()*2)]; } |
1048 | | - // Random page (1-3) so same query returns different results |
1049 | | - function randPage() { return Math.floor(Math.random()*3)+1; } |
| 1074 | + function randPage() { return Math.floor(Math.random()*4)+1; } |
| 1075 | + function randSort() { return ['updated','stars','help-wanted-issues'][Math.floor(Math.random()*3)]; } |
1050 | 1076 |
|
1051 | | - // Pick 3 random topics per query |
1052 | | - const pickedTopics = shuffle(ALL_TOPICS).slice(0,3).join('+'); |
1053 | | - const pickedTopics2 = shuffle(ALL_TOPICS).slice(0,3).join('+'); |
1054 | | - const pickedTopics3 = shuffle(ALL_TOPICS).slice(0,3).join('+'); |
| 1077 | + // Pick a single topic from a random bucket |
| 1078 | + function pickTopic() { |
| 1079 | + const bucket = TOPIC_BUCKETS[Math.floor(Math.random()*TOPIC_BUCKETS.length)]; |
| 1080 | + return bucket[Math.floor(Math.random()*bucket.length)]; |
| 1081 | + } |
1055 | 1082 |
|
1056 | | - // Shuffle langs and pick randomly |
1057 | 1083 | const shuffledLangs = shuffle(langs); |
| 1084 | + const t1=pickTopic(), t2=pickTopic(), t3=pickTopic(); |
1058 | 1085 |
|
1059 | 1086 | try { |
1060 | 1087 | const headers={'Authorization':`token ${ghToken}`,'Accept':'application/vnd.github.v3+json'}; |
1061 | 1088 |
|
| 1089 | + // Each query uses ONE topic (not ANDed) — much more results |
1062 | 1090 | const queries = [ |
1063 | | - // User's top language + random topics |
1064 | | - `https://api.github.com/search/repositories?q=language:${encodeURIComponent(shuffledLangs[0]||'Python')}+${pickedTopics}+stars:>${randStars()}+is:public&sort=${randSort()}&order=desc&per_page=15&page=${randPage()}`, |
1065 | | - // Second language if exists |
1066 | | - shuffledLangs[1] |
1067 | | - ? `https://api.github.com/search/repositories?q=language:${encodeURIComponent(shuffledLangs[1])}+${pickedTopics2}+stars:>${randStars()}+is:public&sort=${randSort()}&order=desc&per_page=15&page=${randPage()}` |
1068 | | - : `https://api.github.com/search/repositories?q=${pickedTopics2}+stars:>500+is:public&sort=updated&order=desc&per_page=15&page=${randPage()}`, |
1069 | | - // Pure random topic set — no language filter |
1070 | | - `https://api.github.com/search/repositories?q=${pickedTopics3}+stars:>${randStars()}+is:public&sort=${randSort()}&order=desc&per_page=15&page=${randPage()}`, |
| 1091 | + // Language + single topic — broad |
| 1092 | + `https://api.github.com/search/repositories?q=language:${encodeURIComponent(shuffledLangs[0]||'Python')}+${t1}+stars:>50+is:public+fork:false&sort=${randSort()}&order=desc&per_page=20&page=${randPage()}`, |
| 1093 | + // Second lang + different topic |
| 1094 | + `https://api.github.com/search/repositories?q=language:${encodeURIComponent(shuffledLangs[1]||shuffledLangs[0]||'JavaScript')}+${t2}+stars:>50+is:public+fork:false&sort=${randSort()}&order=desc&per_page=20&page=${randPage()}`, |
| 1095 | + // No language filter — just a topic, broader catch |
| 1096 | + `https://api.github.com/search/repositories?q=${t3}+stars:>100+is:public+fork:false&sort=${randSort()}&order=desc&per_page=20&page=${randPage()}`, |
1071 | 1097 | ]; |
1072 | 1098 |
|
1073 | 1099 | const results = await Promise.all(queries.map(url=>fetch(url,{headers}).then(r=>r.json()).catch(()=>({items:[]})))); |
|
0 commit comments