{
  "name": "Developer Insights",
  "slug": "developer-insights",
  "version": "1.0.0",
  "description": "A real activity dashboard in developer profiles: heatmap, trends, discussions, reviews, contributions and projects. No external services.",
  "manifest": {
    "schema": 2,
    "kind": "extension",
    "permissions": [
      "ui.panel",
      "data.read",
      "ui.navigate",
      "ui.commands"
    ],
    "entry": "main.js",
    "files": {
      "main.js": "(()=>{(async()=>{let I=document.getElementById(\"root\"),r=t=>String(t??\"\").replace(/[&<>\"']/g,a=>({\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\",\"'\":\"&#39;\"})[a]),s=t=>new Intl.NumberFormat(\"en\").format(Number(t)||0),w=[\"#b69bff\",\"#77d8ca\",\"#ffbf81\",\"#f5a4cd\",\"#91b9ff\",\"#dbb4fa\",\"#d7da83\",\"#9ae0ed\"],A={conversation:\"Conversations\",reply:\"Discussion replies\",code_feedback:\"Code feedback\",feedback_reply:\"Feedback replies\",formal_review:\"Formal reviews\",contribution:\"Contribution submissions\",task_update:\"Task state changes\",decision_update:\"Decision revisions\"},N=`<style>\n  *{box-sizing:border-box}body{margin:0;background:#12101f;color:#f3edff;font:14px/1.55 system-ui,sans-serif;--card:#1c182d;--line:#b49ad32e;--muted:#b4a9c7;--accent:#baa3ff;--soft:#b69bff16}body[data-mode=light]{background:#fbf9ff;color:#271b3b;--card:#fff;--line:#64468724;--muted:#776586;--accent:#7551bf;--soft:#7753bd0b}\n  main{padding:24px;max-width:1600px;margin:auto}h1,h2,h3,p{margin:0}h1{font-size:clamp(24px,3.5vw,38px);letter-spacing:-1px;line-height:1.2}h2{font-size:15px;letter-spacing:-.2px}h3{font-size:14px}.eyebrow{font:10px ui-monospace,monospace;letter-spacing:2px;color:var(--accent);margin-bottom:9px}.muted,small{color:var(--muted)}.intro{display:flex;align-items:center;justify-content:space-between;gap:18px;margin-bottom:22px}.intro p{margin-top:10px;max-width:620px}.pill{color:var(--accent);border:1px solid var(--line);border-radius:24px;padding:7px 12px;font-size:11px;white-space:nowrap}\n  .controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:16px 0 22px}.controls label{display:flex;gap:7px;align-items:center;font-size:12px}button,select{font:inherit;color:inherit;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:8px 12px;cursor:pointer}button:hover{border-color:var(--accent);background:var(--soft)}button:focus-visible,select:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button:disabled{opacity:.45;cursor:default}.controls small{margin-left:auto}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.metric,.card{border:1px solid var(--line);background:var(--card);border-radius:18px;padding:18px;min-width:0}.metric span{color:var(--muted);font-size:11px}.metric strong{display:block;font:600 32px/1.35 ui-monospace,monospace;letter-spacing:-1px;color:var(--accent);font-variant-numeric:tabular-nums}.metric small{font-size:10px}.card{margin-top:14px}.card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:16px}.card-head small{font-size:10px}.period{display:flex;gap:18px;flex-wrap:wrap;margin-top:8px;font-size:11px;color:var(--muted)}.period strong{color:var(--accent)}.chart{width:100%;display:block}.calendar{display:grid;grid-template-rows:repeat(7,10px);grid-auto-flow:column;grid-auto-columns:10px;gap:4px;overflow:auto;padding:2px;justify-content:start}.cell{border-radius:3px;background:var(--soft);border:1px solid var(--line)}.cell[data-level=\"1\"]{background:#b69bff40}.cell[data-level=\"2\"]{background:#b69bff77}.cell[data-level=\"3\"]{background:#b69bffae}.cell[data-level=\"4\"]{background:#b69bff}.legend{display:flex;align-items:center;justify-content:flex-end;gap:6px;margin-top:10px;font-size:10px;color:var(--muted)}.legend i{width:9px;height:9px;border-radius:2px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}.two .card{height:calc(100% - 14px)}.donut-wrap{display:flex;align-items:center;gap:18px}.donut{width:130px;height:130px;flex-shrink:0}.rows{flex:1;min-width:0}.rows>div{display:flex;justify-content:space-between;gap:10px;font-size:11px;padding:4px 0}.rows i{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px}.bar{height:5px;background:var(--soft);border-radius:4px;margin:4px 0 10px;overflow:hidden}.bar>span{display:block;height:100%;border-radius:4px;background:var(--accent)}.project{padding:13px 0;border-top:1px solid var(--line);display:flex;align-items:center;gap:12px;justify-content:space-between}.project p{font-size:11px;color:var(--muted);margin-top:4px}.project button{text-align:left;max-width:100%;overflow-wrap:anywhere}.project small{font-size:10px}.facts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.facts strong{display:block;font:600 20px ui-monospace,monospace;color:var(--accent)}.facts small{font-size:10px}.empty{padding:25px 0;color:var(--muted);font-size:12px}.foot{font-size:10px;color:var(--muted);margin-top:20px;border-top:1px solid var(--line);padding-top:14px}.pager{display:flex;gap:12px;align-items:center;justify-content:flex-end;margin-top:12px;font-size:11px}details{font-size:11px;color:var(--muted);margin-top:14px}summary{cursor:pointer}table{border-collapse:collapse;width:100%;font-size:11px}td,th{padding:5px;text-align:left;border-bottom:1px solid var(--line)}.table-scroll{max-height:200px;overflow:auto}.error{padding:18px;border:1px solid #ffbf8155;border-radius:14px;color:#ffbf81}.ready{animation:appear .25s ease-out}@keyframes appear{from{opacity:.5;transform:translateY(4px)}to{opacity:1;transform:none}}@media(max-width:700px){main{padding:16px}.intro{align-items:flex-start;flex-direction:column}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.two{grid-template-columns:1fr}.controls small{margin-left:0;width:100%}.metric{padding:14px}.metric strong{font-size:27px}.facts{grid-template-columns:repeat(2,minmax(0,1fr))}.donut{width:110px;height:110px}.card{padding:15px}.card-head{align-items:flex-start}.project{align-items:flex-start}.period{gap:10px}}@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}\n  </style>`,C=await devdevian.context(),h=null,d=90,$=\"public\",l=1,z=0,E=0,M=t=>devdevian.panel.configure({placement:\"profile\",title:\"Developer Insights\",width:1200,height:t});await M(1100),new ResizeObserver(()=>{let t=Math.max(180,Math.min(3200,Math.ceil(document.querySelector(\"main\")?.getBoundingClientRect().height||1100)+2));Math.abs(t-E)>12&&(E=t,M(t).catch(()=>{}))}).observe(I);let _=t=>{document.body.dataset.mode=t};_(C.mode),devdevian.onTheme(_);let k=t=>{I.innerHTML=N+`<main class=\"ready\">${t}</main>`},v=(t,a,i)=>`<div class=\"metric\"><span>${t}</span><strong>${s(a)}</strong><small>${i}</small></div>`,R=(t,a,i)=>{let m=0,x=t.map((c,u)=>{let b=a?c.count/a:0,g=b*100,f=`<circle cx=\"60\" cy=\"60\" r=\"45\" pathLength=\"100\" fill=\"none\" stroke=\"${w[u%w.length]}\" stroke-width=\"11\" stroke-dasharray=\"${g} ${100-g}\" stroke-dashoffset=\"${-m}\" transform=\"rotate(-90 60 60)\"/>`;return m+=g,f}).join(\"\");return`<div class=\"donut-wrap\"><svg class=\"donut\" viewBox=\"0 0 120 120\" role=\"img\" aria-label=\"${r(i)}\"><circle cx=\"60\" cy=\"60\" r=\"45\" fill=\"none\" stroke=\"var(--line)\" stroke-width=\"11\"/>${x}<text x=\"60\" y=\"60\" text-anchor=\"middle\" fill=\"currentColor\" font-size=\"20\" font-family=\"monospace\">${s(a)}</text><text x=\"60\" y=\"75\" text-anchor=\"middle\" fill=\"var(--muted)\" font-size=\"9\">${r(i)}</text></svg><div class=\"rows\">${t.map((c,u)=>`<div><span><i style=\"background:${w[u%w.length]}\"></i>${r(c.label)}</span><strong>${s(c.count)} \\xB7 ${a?Math.round(c.count/a*100):0}%</strong></div>`).join(\"\")||'<p class=\"empty\">No records yet.</p>'}</div></div>`};function P(t){let a=new Map(t.activity.map(e=>[e.day,e.count])),i=[];for(let e=0;e<d;e++){let n=new Date(Date.parse(t.window.from+\"T00:00:00Z\")+e*864e5).toISOString().slice(0,10);i.push({day:n,count:a.get(n)||0})}let m=i.reduce((e,n)=>e+n.count,0),x=i.filter(e=>e.count>0).length,c=Math.max(1,...i.map(e=>e.count)),u=0,b=0;for(let e of i)b=e.count?b+1:0,u=Math.max(u,b);let g=0,f=i.length-1;for(i[f].count||f--;f>=0&&i[f].count;f--)g++;let q=i.map((e,n)=>`${(n/(d-1)*680).toFixed(2)},${(150-e.count/c*125).toFixed(2)}`),T=q.join(\" \"),o=t.totals,j=[\"submitted\",\"changes_requested\",\"accepted\",\"withdrawn\"].map(e=>({label:e.replaceAll(\"_\",\" \"),count:t.contributions.find(n=>n.state===e)?.count||0})),y=j.reduce((e,n)=>e+n.count,0);k(`<div class=\"intro\"><div><p class=\"eyebrow\">DEVELOPER INSIGHTS / REAL WORK</p><h1>${r(t.person.name)}'s rhythm.</h1><p class=\"muted\">A little perspective on conversations, reviews and work built together.</p></div><span class=\"pill\">${t.scope===\"all\"?\"Your accessible work \\xB7 private included\":\"Public activity only\"}</span></div>\n    <div class=\"controls\"><label>Activity window <select id=\"days\">${[30,90,365].map(e=>`<option value=\"${e}\" ${d===e?\"selected\":\"\"}>${e} days</option>`).join(\"\")}</select></label><button id=\"refresh\">Refresh</button>${t.own?`<label><input id=\"private\" type=\"checkbox\" ${$===\"all\"?\"checked\":\"\"}/>Include my accessible private work</label>`:\"\"}<small>Calendar in UTC \\xB7 counters below are lifetime retained records</small></div>\n    <div class=\"grid\">${v(\"Conversations\",o.conversations,`${s(o.questions)} questions \\xB7 ${s(o.review_requests)} review requests`)}${v(\"Discussion replies\",o.replies,\"Retained replies, including nested replies\")}${v(\"Accepted solutions\",o.accepted_solutions,\"Answers currently marked as solutions\")}${v(\"Contributions\",y,`${s(j[2].count)} currently accepted`)}${v(\"Projects with work\",o.projects_participated,`${s(o.projects_owned)} owned projects`)}${v(\"Review actions\",o.code_feedback+o.feedback_replies+o.formal_reviews,`${s(o.formal_reviews)} formal review outcomes`)}</div>\n    <section class=\"card\"><div class=\"card-head\"><h2>Your activity calendar</h2><small>${r(t.window.from)} \\u2014 ${r(t.window.to)}</small></div><div class=\"calendar\" role=\"img\" aria-label=\"${m} recorded actions across ${x} active days in the selected ${d}-day window\">${i.map(e=>`<div class=\"cell\" data-level=\"${e.count?Math.max(1,Math.ceil(e.count/c*4)):0}\" title=\"${e.day}: ${s(e.count)} recorded actions\"></div>`).join(\"\")}</div><div class=\"legend\">Less ${[0,1,2,3,4].map(e=>`<i style=\"background:${e?\"#b69bff\"+[\"\",\"40\",\"77\",\"ae\",\"ff\"][e]:\"var(--soft)\"}\"></i>`).join(\"\")} More</div><div class=\"period\"><span><strong>${s(m)}</strong> recorded actions</span><span><strong>${x}/${d} \\xB7 ${Math.round(x/d*100)}%</strong> active days</span><span><strong>${g}</strong> current streak*</span><span><strong>${u}</strong> longest streak in this window</span></div></section>\n    <section class=\"card\"><div class=\"card-head\"><h2>Activity over time</h2><small>Daily recorded actions \\xB7 not reading time</small></div>${m?`<svg class=\"chart\" viewBox=\"0 0 680 180\" role=\"img\" aria-label=\"Daily activity, maximum ${c} actions\"><defs><linearGradient id=\"area\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop stop-color=\"#b69bff\" stop-opacity=\".4\"/><stop offset=\"1\" stop-color=\"#b69bff\" stop-opacity=\"0\"/></linearGradient></defs>${[25,87,150].map(e=>`<line x1=\"0\" y1=\"${e}\" x2=\"680\" y2=\"${e}\" stroke=\"var(--line)\" stroke-dasharray=\"3 5\"/>`).join(\"\")}<polygon points=\"0,150 ${T} 680,150\" fill=\"url(#area)\"/><polyline points=\"${T}\" fill=\"none\" stroke=\"var(--accent)\" stroke-width=\"2.5\" stroke-linejoin=\"round\"/><text x=\"0\" y=\"174\" fill=\"var(--muted)\" font-size=\"10\">${i[0].day}</text><text x=\"680\" y=\"174\" text-anchor=\"end\" fill=\"var(--muted)\" font-size=\"10\">${i.at(-1).day}</text></svg>`:'<p class=\"empty\">No recorded activity in this window. Nothing is estimated.</p>'}<details><summary>Read the daily figures</summary><div class=\"table-scroll\"><table><thead><tr><th>UTC date</th><th>Actions</th></tr></thead><tbody>${i.map(e=>`<tr><td>${e.day}</td><td>${e.count}</td></tr>`).join(\"\")}</tbody></table></div></details></section>\n    <div class=\"two\"><section class=\"card\"><div class=\"card-head\"><h2>How this window was spent</h2><small>Share of recorded actions</small></div>${R(t.breakdown.map(e=>({...e,label:A[e.category]||e.category})),m,\"actions\")}</section><section class=\"card\"><div class=\"card-head\"><h2>Contribution states</h2><small>Current lifetime states</small></div>${R(j,y,\"submissions\")}<p class=\"muted\" style=\"font-size:10px;margin-top:12px\">${y?Math.round(j[2].count/y*100):0}% accepted out of ${s(y)} retained submissions. Pending and withdrawn records are included in this denominator; this is not a quality score.</p></section></div>\n    <div class=\"two\"><section class=\"card\"><div class=\"card-head\"><h2>More of the picture</h2><small>Lifetime retained records</small></div><div class=\"facts\">${[[\"Discussions\",o.discussions],[\"Show & tell\",o.showcases],[\"Code feedback\",o.code_feedback],[\"Feedback replies\",o.feedback_replies],[\"Assigned tasks\",o.assigned_tasks],[\"Hosted events\",o.hosted_events],[\"Published extensions\",o.published_extensions],[\"Formal reviews\",o.formal_reviews]].map(([e,n])=>`<div><strong>${s(n)}</strong><small>${e}</small></div>`).join(\"\")}</div></section><section class=\"card\"><div class=\"card-head\"><h2>Topics you bring to the table</h2><small>Authored conversations \\xB7 top 8</small></div>${t.topics.length?t.topics.map(e=>`<div class=\"rows\"><div><span>${r(e.name)}</span><strong>${s(e.count)}</strong></div></div><div class=\"bar\"><span style=\"width:${e.count/Math.max(1,o.conversations)*100}%\"></span></div>`).join(\"\"):'<p class=\"empty\">No authored conversations in this scope yet.</p>'}</section></div>\n    <section class=\"card\"><div class=\"card-head\"><h2>Tools in your conversations</h2><small>Declared stack \\xB7 top 8 \\xB7 not inferred expertise</small></div><div class=\"grid\">${t.technologies.map(e=>`<div class=\"rows\"><div><span>${r(e.name)}</span><strong>${s(e.count)} posts</strong></div></div>`).join(\"\")||'<p class=\"empty\">No stack labels on authored conversations yet.</p>'}</div></section>\n    <section class=\"card\"><div class=\"card-head\"><h2>Projects with your work</h2><small>${s(t.projects_total)} projects \\xB7 lifetime attribution</small></div>${t.projects.length?t.projects.map(e=>`<div class=\"project\"><div><button data-project=\"${e.id}\">${r(e.name)}</button><p>${r(e.roles)} \\xB7 ${r(e.stage)}</p></div><small>${s(e.contributions)} submissions<br/>${s(e.accepted)} accepted</small></div>`).join(\"\"):'<p class=\"empty\">No visible project attribution yet. Permission to participate alone is not counted as work.</p>'}<div class=\"pager\"><button id=\"prev\" ${l<=1?\"disabled\":\"\"}>Previous</button><span>${l}/${t.project_pagination.pages}</span><button id=\"next\" ${l>=t.project_pagination.pages?\"disabled\":\"\"}>Next</button></div></section>\n    ${t.personal?`<section class=\"card\"><div class=\"card-head\"><h2>Only you</h2><small>Personal counters \\xB7 never shown to another viewer</small></div><div class=\"facts\">${[[\"Saved conversations\",t.personal.bookmarks],[\"Followed conversations\",t.personal.following_conversations],[\"Event RSVPs\",t.personal.event_rsvps],[\"Installed extensions\",t.personal.installed_extensions]].map(([e,n])=>`<div><strong>${s(n)}</strong><small>${e}</small></div>`).join(\"\")}</div></section>`:\"\"}\n    <p class=\"foot\">Counts reflect retained, currently readable records. Removed/deleted work is excluded. *Current streak ends today or yesterday in UTC and is bounded by this window. Reading time, visits, online hours and GitHub-wide activity are not tracked. Refresh to reflect changed permissions. This extension has no external network access.</p>`),document.getElementById(\"days\").onchange=e=>{d=Number(e.target.value),l=1,p()},document.getElementById(\"refresh\").onclick=()=>p();let D=document.getElementById(\"private\");D&&(D.onchange=e=>{$=e.target.checked?\"all\":\"public\",l=1,p()}),document.getElementById(\"prev\").onclick=()=>{l--,p()},document.getElementById(\"next\").onclick=()=>{l++,p()},document.querySelectorAll(\"[data-project]\").forEach(e=>e.onclick=()=>devdevian.navigate(\"workspace/\"+e.dataset.project))}async function p(){let t=++z;if(!h){k('<p class=\"eyebrow\">DEVELOPER INSIGHTS</p><h2>Open a developer profile.</h2><p class=\"muted\">Your statistics appear below its header.</p>');return}k('<p class=\"eyebrow\">DEVELOPER INSIGHTS</p><h2 role=\"status\">Loading recorded work\\u2026</h2>');try{let a=await devdevian.data.get(`/users/${h}/insights?days=${d}&scope=${$}&project_page=${l}`);t===z&&P(a)}catch(a){t===z&&(k(`<h2>Could not load insights</h2><p class=\"error\" role=\"alert\">${r(a.message)}</p><p class=\"muted\">Requires Devdevian Browser/API 1.6 or newer. Your account must still be able to access the profile.</p><button id=\"retry\">Try again</button>`),document.getElementById(\"retry\").onclick=()=>p())}}function S(t){_(t.mode);let a=t.profile?.id||null;a!==h&&(h=a,$=\"public\",l=1,p())}devdevian.onContext&&devdevian.onContext(S),await devdevian.commands.register({id:\"open-insights\",title:\"Open my insights\",shortcut:\"Alt+Shift+I\"},()=>devdevian.navigate(\"profile\")),S(C),h||await p()})();})();\n",
      "sources/main.js": "(async () => {\n  const root = document.getElementById(\"root\");\n  // All interpolated account/topic/project text is escaped. SVG paths contain numbers only.\n  const esc = (v) =>\n    String(v ?? \"\").replace(\n      /[&<>\"']/g,\n      (c) =>\n        ({\n          \"&\": \"&amp;\",\n          \"<\": \"&lt;\",\n          \">\": \"&gt;\",\n          '\"': \"&quot;\",\n          \"'\": \"&#39;\",\n        })[c],\n    );\n  const num = (n) => new Intl.NumberFormat(\"en\").format(Number(n) || 0);\n  const colors = [\n    \"#b69bff\",\n    \"#77d8ca\",\n    \"#ffbf81\",\n    \"#f5a4cd\",\n    \"#91b9ff\",\n    \"#dbb4fa\",\n    \"#d7da83\",\n    \"#9ae0ed\",\n  ];\n  const labels = {\n    conversation: \"Conversations\",\n    reply: \"Discussion replies\",\n    code_feedback: \"Code feedback\",\n    feedback_reply: \"Feedback replies\",\n    formal_review: \"Formal reviews\",\n    contribution: \"Contribution submissions\",\n    task_update: \"Task state changes\",\n    decision_update: \"Decision revisions\",\n  };\n  const style = `<style>\n  *{box-sizing:border-box}body{margin:0;background:#12101f;color:#f3edff;font:14px/1.55 system-ui,sans-serif;--card:#1c182d;--line:#b49ad32e;--muted:#b4a9c7;--accent:#baa3ff;--soft:#b69bff16}body[data-mode=light]{background:#fbf9ff;color:#271b3b;--card:#fff;--line:#64468724;--muted:#776586;--accent:#7551bf;--soft:#7753bd0b}\n  main{padding:24px;max-width:1600px;margin:auto}h1,h2,h3,p{margin:0}h1{font-size:clamp(24px,3.5vw,38px);letter-spacing:-1px;line-height:1.2}h2{font-size:15px;letter-spacing:-.2px}h3{font-size:14px}.eyebrow{font:10px ui-monospace,monospace;letter-spacing:2px;color:var(--accent);margin-bottom:9px}.muted,small{color:var(--muted)}.intro{display:flex;align-items:center;justify-content:space-between;gap:18px;margin-bottom:22px}.intro p{margin-top:10px;max-width:620px}.pill{color:var(--accent);border:1px solid var(--line);border-radius:24px;padding:7px 12px;font-size:11px;white-space:nowrap}\n  .controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:16px 0 22px}.controls label{display:flex;gap:7px;align-items:center;font-size:12px}button,select{font:inherit;color:inherit;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:8px 12px;cursor:pointer}button:hover{border-color:var(--accent);background:var(--soft)}button:focus-visible,select:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button:disabled{opacity:.45;cursor:default}.controls small{margin-left:auto}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.metric,.card{border:1px solid var(--line);background:var(--card);border-radius:18px;padding:18px;min-width:0}.metric span{color:var(--muted);font-size:11px}.metric strong{display:block;font:600 32px/1.35 ui-monospace,monospace;letter-spacing:-1px;color:var(--accent);font-variant-numeric:tabular-nums}.metric small{font-size:10px}.card{margin-top:14px}.card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:16px}.card-head small{font-size:10px}.period{display:flex;gap:18px;flex-wrap:wrap;margin-top:8px;font-size:11px;color:var(--muted)}.period strong{color:var(--accent)}.chart{width:100%;display:block}.calendar{display:grid;grid-template-rows:repeat(7,10px);grid-auto-flow:column;grid-auto-columns:10px;gap:4px;overflow:auto;padding:2px;justify-content:start}.cell{border-radius:3px;background:var(--soft);border:1px solid var(--line)}.cell[data-level=\"1\"]{background:#b69bff40}.cell[data-level=\"2\"]{background:#b69bff77}.cell[data-level=\"3\"]{background:#b69bffae}.cell[data-level=\"4\"]{background:#b69bff}.legend{display:flex;align-items:center;justify-content:flex-end;gap:6px;margin-top:10px;font-size:10px;color:var(--muted)}.legend i{width:9px;height:9px;border-radius:2px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}.two .card{height:calc(100% - 14px)}.donut-wrap{display:flex;align-items:center;gap:18px}.donut{width:130px;height:130px;flex-shrink:0}.rows{flex:1;min-width:0}.rows>div{display:flex;justify-content:space-between;gap:10px;font-size:11px;padding:4px 0}.rows i{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px}.bar{height:5px;background:var(--soft);border-radius:4px;margin:4px 0 10px;overflow:hidden}.bar>span{display:block;height:100%;border-radius:4px;background:var(--accent)}.project{padding:13px 0;border-top:1px solid var(--line);display:flex;align-items:center;gap:12px;justify-content:space-between}.project p{font-size:11px;color:var(--muted);margin-top:4px}.project button{text-align:left;max-width:100%;overflow-wrap:anywhere}.project small{font-size:10px}.facts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.facts strong{display:block;font:600 20px ui-monospace,monospace;color:var(--accent)}.facts small{font-size:10px}.empty{padding:25px 0;color:var(--muted);font-size:12px}.foot{font-size:10px;color:var(--muted);margin-top:20px;border-top:1px solid var(--line);padding-top:14px}.pager{display:flex;gap:12px;align-items:center;justify-content:flex-end;margin-top:12px;font-size:11px}details{font-size:11px;color:var(--muted);margin-top:14px}summary{cursor:pointer}table{border-collapse:collapse;width:100%;font-size:11px}td,th{padding:5px;text-align:left;border-bottom:1px solid var(--line)}.table-scroll{max-height:200px;overflow:auto}.error{padding:18px;border:1px solid #ffbf8155;border-radius:14px;color:#ffbf81}.ready{animation:appear .25s ease-out}@keyframes appear{from{opacity:.5;transform:translateY(4px)}to{opacity:1;transform:none}}@media(max-width:700px){main{padding:16px}.intro{align-items:flex-start;flex-direction:column}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.two{grid-template-columns:1fr}.controls small{margin-left:0;width:100%}.metric{padding:14px}.metric strong{font-size:27px}.facts{grid-template-columns:repeat(2,minmax(0,1fr))}.donut{width:110px;height:110px}.card{padding:15px}.card-head{align-items:flex-start}.project{align-items:flex-start}.period{gap:10px}}@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}\n  </style>`;\n  let context = await devdevian.context(),\n    person = null,\n    days = 90,\n    scope = \"public\",\n    page = 1,\n    sequence = 0,\n    lastHeight = 0;\n  const configure = (height) =>\n    devdevian.panel.configure({\n      placement: \"profile\",\n      title: \"Developer Insights\",\n      width: 1200,\n      height,\n    });\n  await configure(1100);\n  const resize = new ResizeObserver(() => {\n    const height = Math.max(\n      180,\n      Math.min(\n        3200,\n        Math.ceil(\n          document.querySelector(\"main\")?.getBoundingClientRect().height ||\n            1100,\n        ) + 2,\n      ),\n    );\n    if (Math.abs(height - lastHeight) > 12) {\n      lastHeight = height;\n      configure(height).catch(() => {});\n    }\n  });\n  resize.observe(root);\n  const setTheme = (mode) => {\n    document.body.dataset.mode = mode;\n  };\n  setTheme(context.mode);\n  devdevian.onTheme(setTheme);\n  const shell = (body) => {\n    root.innerHTML = style + `<main class=\"ready\">${body}</main>`;\n  };\n  const metric = (label, value, note) =>\n    `<div class=\"metric\"><span>${label}</span><strong>${num(value)}</strong><small>${note}</small></div>`;\n  const donut = (rows, total, caption) => {\n    let offset = 0;\n    const arcs = rows\n      .map((r, i) => {\n        const share = total ? r.count / total : 0,\n          part = share * 100;\n        const s = `<circle cx=\"60\" cy=\"60\" r=\"45\" pathLength=\"100\" fill=\"none\" stroke=\"${colors[i % colors.length]}\" stroke-width=\"11\" stroke-dasharray=\"${part} ${100 - part}\" stroke-dashoffset=\"${-offset}\" transform=\"rotate(-90 60 60)\"/>`;\n        offset += part;\n        return s;\n      })\n      .join(\"\");\n    return `<div class=\"donut-wrap\"><svg class=\"donut\" viewBox=\"0 0 120 120\" role=\"img\" aria-label=\"${esc(caption)}\"><circle cx=\"60\" cy=\"60\" r=\"45\" fill=\"none\" stroke=\"var(--line)\" stroke-width=\"11\"/>${arcs}<text x=\"60\" y=\"60\" text-anchor=\"middle\" fill=\"currentColor\" font-size=\"20\" font-family=\"monospace\">${num(total)}</text><text x=\"60\" y=\"75\" text-anchor=\"middle\" fill=\"var(--muted)\" font-size=\"9\">${esc(caption)}</text></svg><div class=\"rows\">${rows.map((r, i) => `<div><span><i style=\"background:${colors[i % colors.length]}\"></i>${esc(r.label)}</span><strong>${num(r.count)} · ${total ? Math.round((r.count / total) * 100) : 0}%</strong></div>`).join(\"\") || '<p class=\"empty\">No records yet.</p>'}</div></div>`;\n  };\n  function render(data) {\n    const map = new Map(data.activity.map((x) => [x.day, x.count])),\n      daily = [];\n    for (let i = 0; i < days; i++) {\n      const day = new Date(\n        Date.parse(data.window.from + \"T00:00:00Z\") + i * 86400000,\n      )\n        .toISOString()\n        .slice(0, 10);\n      daily.push({ day, count: map.get(day) || 0 });\n    }\n    const total = daily.reduce((s, d) => s + d.count, 0),\n      active = daily.filter((d) => d.count > 0).length,\n      max = Math.max(1, ...daily.map((d) => d.count));\n    let best = 0,\n      run = 0;\n    for (const d of daily) {\n      run = d.count ? run + 1 : 0;\n      best = Math.max(best, run);\n    }\n    let current = 0,\n      index = daily.length - 1;\n    if (!daily[index].count) index--;\n    for (; index >= 0 && daily[index].count; index--) current++;\n    const points = daily.map(\n        (d, i) =>\n          `${((i / (days - 1)) * 680).toFixed(2)},${(150 - (d.count / max) * 125).toFixed(2)}`,\n      ),\n      path = points.join(\" \"),\n      c = data.totals;\n    const states = [\n        \"submitted\",\n        \"changes_requested\",\n        \"accepted\",\n        \"withdrawn\",\n      ].map((state) => ({\n        label: state.replaceAll(\"_\", \" \"),\n        count: data.contributions.find((c) => c.state === state)?.count || 0,\n      })),\n      contributions = states.reduce((s, r) => s + r.count, 0);\n    shell(`<div class=\"intro\"><div><p class=\"eyebrow\">DEVELOPER INSIGHTS / REAL WORK</p><h1>${esc(data.person.name)}'s rhythm.</h1><p class=\"muted\">A little perspective on conversations, reviews and work built together.</p></div><span class=\"pill\">${data.scope === \"all\" ? \"Your accessible work · private included\" : \"Public activity only\"}</span></div>\n    <div class=\"controls\"><label>Activity window <select id=\"days\">${[30, 90, 365].map((n) => `<option value=\"${n}\" ${days === n ? \"selected\" : \"\"}>${n} days</option>`).join(\"\")}</select></label><button id=\"refresh\">Refresh</button>${data.own ? `<label><input id=\"private\" type=\"checkbox\" ${scope === \"all\" ? \"checked\" : \"\"}/>Include my accessible private work</label>` : \"\"}<small>Calendar in UTC · counters below are lifetime retained records</small></div>\n    <div class=\"grid\">${metric(\"Conversations\", c.conversations, `${num(c.questions)} questions · ${num(c.review_requests)} review requests`)}${metric(\"Discussion replies\", c.replies, \"Retained replies, including nested replies\")}${metric(\"Accepted solutions\", c.accepted_solutions, \"Answers currently marked as solutions\")}${metric(\"Contributions\", contributions, `${num(states[2].count)} currently accepted`)}${metric(\"Projects with work\", c.projects_participated, `${num(c.projects_owned)} owned projects`)}${metric(\"Review actions\", c.code_feedback + c.feedback_replies + c.formal_reviews, `${num(c.formal_reviews)} formal review outcomes`)}</div>\n    <section class=\"card\"><div class=\"card-head\"><h2>Your activity calendar</h2><small>${esc(data.window.from)} — ${esc(data.window.to)}</small></div><div class=\"calendar\" role=\"img\" aria-label=\"${total} recorded actions across ${active} active days in the selected ${days}-day window\">${daily.map((d) => `<div class=\"cell\" data-level=\"${d.count ? Math.max(1, Math.ceil((d.count / max) * 4)) : 0}\" title=\"${d.day}: ${num(d.count)} recorded actions\"></div>`).join(\"\")}</div><div class=\"legend\">Less ${[0, 1, 2, 3, 4].map((n) => `<i style=\"background:${n ? \"#b69bff\" + [\"\", \"40\", \"77\", \"ae\", \"ff\"][n] : \"var(--soft)\"}\"></i>`).join(\"\")} More</div><div class=\"period\"><span><strong>${num(total)}</strong> recorded actions</span><span><strong>${active}/${days} · ${Math.round((active / days) * 100)}%</strong> active days</span><span><strong>${current}</strong> current streak*</span><span><strong>${best}</strong> longest streak in this window</span></div></section>\n    <section class=\"card\"><div class=\"card-head\"><h2>Activity over time</h2><small>Daily recorded actions · not reading time</small></div>${total ? `<svg class=\"chart\" viewBox=\"0 0 680 180\" role=\"img\" aria-label=\"Daily activity, maximum ${max} actions\"><defs><linearGradient id=\"area\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop stop-color=\"#b69bff\" stop-opacity=\".4\"/><stop offset=\"1\" stop-color=\"#b69bff\" stop-opacity=\"0\"/></linearGradient></defs>${[25, 87, 150].map((y) => `<line x1=\"0\" y1=\"${y}\" x2=\"680\" y2=\"${y}\" stroke=\"var(--line)\" stroke-dasharray=\"3 5\"/>`).join(\"\")}<polygon points=\"0,150 ${path} 680,150\" fill=\"url(#area)\"/><polyline points=\"${path}\" fill=\"none\" stroke=\"var(--accent)\" stroke-width=\"2.5\" stroke-linejoin=\"round\"/><text x=\"0\" y=\"174\" fill=\"var(--muted)\" font-size=\"10\">${daily[0].day}</text><text x=\"680\" y=\"174\" text-anchor=\"end\" fill=\"var(--muted)\" font-size=\"10\">${daily.at(-1).day}</text></svg>` : '<p class=\"empty\">No recorded activity in this window. Nothing is estimated.</p>'}<details><summary>Read the daily figures</summary><div class=\"table-scroll\"><table><thead><tr><th>UTC date</th><th>Actions</th></tr></thead><tbody>${daily.map((d) => `<tr><td>${d.day}</td><td>${d.count}</td></tr>`).join(\"\")}</tbody></table></div></details></section>\n    <div class=\"two\"><section class=\"card\"><div class=\"card-head\"><h2>How this window was spent</h2><small>Share of recorded actions</small></div>${donut(\n      data.breakdown.map((r) => ({\n        ...r,\n        label: labels[r.category] || r.category,\n      })),\n      total,\n      \"actions\",\n    )}</section><section class=\"card\"><div class=\"card-head\"><h2>Contribution states</h2><small>Current lifetime states</small></div>${donut(states, contributions, \"submissions\")}<p class=\"muted\" style=\"font-size:10px;margin-top:12px\">${contributions ? Math.round((states[2].count / contributions) * 100) : 0}% accepted out of ${num(contributions)} retained submissions. Pending and withdrawn records are included in this denominator; this is not a quality score.</p></section></div>\n    <div class=\"two\"><section class=\"card\"><div class=\"card-head\"><h2>More of the picture</h2><small>Lifetime retained records</small></div><div class=\"facts\">${[\n      [\"Discussions\", c.discussions],\n      [\"Show & tell\", c.showcases],\n      [\"Code feedback\", c.code_feedback],\n      [\"Feedback replies\", c.feedback_replies],\n      [\"Assigned tasks\", c.assigned_tasks],\n      [\"Hosted events\", c.hosted_events],\n      [\"Published extensions\", c.published_extensions],\n      [\"Formal reviews\", c.formal_reviews],\n    ]\n      .map(\n        ([l, n]) => `<div><strong>${num(n)}</strong><small>${l}</small></div>`,\n      )\n      .join(\n        \"\",\n      )}</div></section><section class=\"card\"><div class=\"card-head\"><h2>Topics you bring to the table</h2><small>Authored conversations · top 8</small></div>${data.topics.length ? data.topics.map((t) => `<div class=\"rows\"><div><span>${esc(t.name)}</span><strong>${num(t.count)}</strong></div></div><div class=\"bar\"><span style=\"width:${(t.count / Math.max(1, c.conversations)) * 100}%\"></span></div>`).join(\"\") : '<p class=\"empty\">No authored conversations in this scope yet.</p>'}</section></div>\n    <section class=\"card\"><div class=\"card-head\"><h2>Tools in your conversations</h2><small>Declared stack · top 8 · not inferred expertise</small></div><div class=\"grid\">${data.technologies.map((t) => `<div class=\"rows\"><div><span>${esc(t.name)}</span><strong>${num(t.count)} posts</strong></div></div>`).join(\"\") || '<p class=\"empty\">No stack labels on authored conversations yet.</p>'}</div></section>\n    <section class=\"card\"><div class=\"card-head\"><h2>Projects with your work</h2><small>${num(data.projects_total)} projects · lifetime attribution</small></div>${data.projects.length ? data.projects.map((pr) => `<div class=\"project\"><div><button data-project=\"${pr.id}\">${esc(pr.name)}</button><p>${esc(pr.roles)} · ${esc(pr.stage)}</p></div><small>${num(pr.contributions)} submissions<br/>${num(pr.accepted)} accepted</small></div>`).join(\"\") : '<p class=\"empty\">No visible project attribution yet. Permission to participate alone is not counted as work.</p>'}<div class=\"pager\"><button id=\"prev\" ${page <= 1 ? \"disabled\" : \"\"}>Previous</button><span>${page}/${data.project_pagination.pages}</span><button id=\"next\" ${page >= data.project_pagination.pages ? \"disabled\" : \"\"}>Next</button></div></section>\n    ${\n      data.personal\n        ? `<section class=\"card\"><div class=\"card-head\"><h2>Only you</h2><small>Personal counters · never shown to another viewer</small></div><div class=\"facts\">${[\n            [\"Saved conversations\", data.personal.bookmarks],\n            [\"Followed conversations\", data.personal.following_conversations],\n            [\"Event RSVPs\", data.personal.event_rsvps],\n            [\"Installed extensions\", data.personal.installed_extensions],\n          ]\n            .map(\n              ([l, n]) =>\n                `<div><strong>${num(n)}</strong><small>${l}</small></div>`,\n            )\n            .join(\"\")}</div></section>`\n        : \"\"\n    }\n    <p class=\"foot\">Counts reflect retained, currently readable records. Removed/deleted work is excluded. *Current streak ends today or yesterday in UTC and is bounded by this window. Reading time, visits, online hours and GitHub-wide activity are not tracked. Refresh to reflect changed permissions. This extension has no external network access.</p>`);\n    document.getElementById(\"days\").onchange = (e) => {\n      days = Number(e.target.value);\n      page = 1;\n      load();\n    };\n    document.getElementById(\"refresh\").onclick = () => load();\n    const privateInput = document.getElementById(\"private\");\n    if (privateInput)\n      privateInput.onchange = (e) => {\n        scope = e.target.checked ? \"all\" : \"public\";\n        page = 1;\n        load();\n      };\n    document.getElementById(\"prev\").onclick = () => {\n      page--;\n      load();\n    };\n    document.getElementById(\"next\").onclick = () => {\n      page++;\n      load();\n    };\n    document\n      .querySelectorAll(\"[data-project]\")\n      .forEach(\n        (b) =>\n          (b.onclick = () =>\n            devdevian.navigate(\"workspace/\" + b.dataset.project)),\n      );\n  }\n  async function load() {\n    const current = ++sequence;\n    if (!person) {\n      shell(\n        '<p class=\"eyebrow\">DEVELOPER INSIGHTS</p><h2>Open a developer profile.</h2><p class=\"muted\">Your statistics appear below its header.</p>',\n      );\n      return;\n    }\n    shell(\n      '<p class=\"eyebrow\">DEVELOPER INSIGHTS</p><h2 role=\"status\">Loading recorded work…</h2>',\n    );\n    try {\n      const data = await devdevian.data.get(\n        `/users/${person}/insights?days=${days}&scope=${scope}&project_page=${page}`,\n      );\n      if (current === sequence) render(data);\n    } catch (e) {\n      if (current === sequence) {\n        shell(\n          `<h2>Could not load insights</h2><p class=\"error\" role=\"alert\">${esc(e.message)}</p><p class=\"muted\">Requires Devdevian Browser/API 1.6 or newer. Your account must still be able to access the profile.</p><button id=\"retry\">Try again</button>`,\n        );\n        document.getElementById(\"retry\").onclick = () => load();\n      }\n    }\n  }\n  function changed(next) {\n    setTheme(next.mode);\n    const id = next.profile?.id || null;\n    if (id === person) return;\n    person = id;\n    scope = \"public\";\n    page = 1;\n    load();\n  }\n  if (devdevian.onContext) devdevian.onContext(changed);\n  await devdevian.commands.register(\n    { id: \"open-insights\", title: \"Open my insights\", shortcut: \"Alt+Shift+I\" },\n    () => devdevian.navigate(\"profile\"),\n  );\n  changed(context);\n  if (!person) await load();\n})();\n"
    },
    "styles": {
      "light": "",
      "dark": ""
    }
  },
  "listing": {
    "full_description": "## Your work, in perspective\n\nInstall Developer Insights and open a developer profile. A large dashboard appears below its header, with an activity calendar, daily chart, contribution-state and activity breakdowns, accepted solutions, code feedback, formal reviews, project attribution and topics. Choose a 30, 90 or 365-day activity window. Summary counters and project attribution describe retained lifetime records; the chart and active-day percentages describe the selected UTC window.\n\n### Public and personal\n\nOther profiles always show public data, even if you belong to the same private community. On your own profile you may explicitly include accessible private work. Hidden profiles and removed records stay hidden; losing access removes those private records from the next response. Personal saved/following/event/installed-extension counts are available only in your own private-inclusive view. An approval to participate in a project alone is not evidence of actual work.\n\n### What these numbers mean\n\nActivity means recorded creation of conversations, replies, code feedback and feedback replies, formal review outcomes, contribution submissions, task-state changes and retained decision revisions. Edits to an ordinary post and reading do not create extra activity. Accepted solutions and contribution states are current-state figures, not a quality score. Project attribution is paginated.\n\nNo visits, time spent reading, online time, GitHub-wide activity or deleted history are guessed or tracked. This dashboard is not an exhaustive historical archive. A streak is bounded by the selected UTC window. Percentages show their denominator and zero-data states are explicit.\n\n### Installation and requirements\n\nRequires Devdevian Browser/API 1.6 or newer. It uses the existing data.read capability and a profile panel; CLI 0.24 can pack and publish its package. The CLI 0.24 standalone playground predates profile panels/insights, so test this extension in Preview in my app or the current repository's local forum sandbox.\n\nNo network or storage permissions, trackers, provider tokens, analytics services or remote fonts. No data leaves Devdevian through this extension. The command Open my insights (Alt+Shift+I) opens your profile. Data is fetched on profile/window/scope changes or Refresh, not by continuous background polling.\n\nSource: examples/extensions/developer-insights.",
    "tags": [
      "statistics",
      "profile",
      "charts",
      "activity",
      "contributions"
    ],
    "links": [
      {
        "label": "Source code",
        "url": "https://github.com/Tenyokj/devdevian/tree/main/examples/extensions/developer-insights"
      }
    ],
    "screenshots": []
  }
}