{% extends "base.html" %} {% block title %}Pods{% endblock %} {% block breadcrumb %}Pods{% endblock %} {% block extra_head %} {% endblock %} {% block content %} {# ── Stats Overview ── #} {% set running_pods = pods|selectattr('phase', 'equalto', 'Running')|list|length %} {% set pending_pods = pods|selectattr('phase', 'equalto', 'Pending')|list|length %} {% set failed_pods = pods|selectattr('phase', 'equalto', 'Failed')|list|length %} {% set total_restarts = pods|sum(attribute='restart_count') %}
{{ pods|length }}
Total Pods
{{ running_pods }}
Running
{{ pending_pods }}
Pending
{{ failed_pods }}
Failed
{{ deployments|length }}
Deployments
{{ services|length }}
Services
{# ── Resource Charts ── #} {% if pods %}
Pod Phase Distribution
Pod Count (Real-time)
Restarts by Pod
Pods per Namespace
{# ── Health Timeline ── #}
Cluster Health (Last 60 checks)
All Running Partial Failures
{% endif %} {% if not kubectl_available %}
☸️

kubectl Not Available

{{ error or 'kubectl CLI is not installed or not in PATH.' }}

Install kubectl: brew install kubectl or visit kubernetes.io

{% if manifests %}

{{ manifests|length }} K8s manifest(s) found in your workspace. Install kubectl to manage pods.

{% endif %}
{% endif %} {# ── Tabs ── #}
{# ═══════════ TAB: Pods ═══════════ #}
{% if pods %} {# Namespace filter #} {% set ns_list = pods|map(attribute='namespace')|unique|list %} {% if ns_list|length > 1 %}
{% for ns in ns_list %} {% endfor %}
{% endif %}
{% for pod in pods %} {% endfor %}
NameNamespacePhaseReadyRestartsNodeIPAge
{{ pod.name }}
{{ pod.namespace }} {{ pod.phase }} {{ pod.ready_count }}/{{ pod.container_count }} {% set rc = pod.restart_count %} {% if rc > 0 %}{% endif %} {{ rc }} {{ pod.node or '—' }} {{ pod.ip or '—' }} {{ pod.age or '—' }}
{% else %}
☸️
{% if kubectl_available %}

No pods found in the cluster. Deploy with kubectl apply -k k8s/

{% else %}

Install kubectl and connect to a cluster to view pods.

{% endif %}
{% endif %}
{# ═══════════ TAB: Deployments ═══════════ #}
{% if deployments %}
{% for dep in deployments %} {% endfor %}
NameNamespaceReadyAvailableStrategyImageActions
{{ dep.name }} {{ dep.namespace }} {{ dep.ready_replicas }}/{{ dep.replicas }} {% if dep.ready_replicas == dep.replicas and dep.replicas > 0 %}{% elif dep.ready_replicas < dep.replicas %}{% endif %} {{ dep.available_replicas }} {{ dep.strategy or 'RollingUpdate' }} {{ dep.image }}
{% else %}
📦

No deployments found. Generate manifests with aq deploy kubernetes

{% endif %}
{# ═══════════ TAB: Services ═══════════ #}
{% if services %}
{% for svc in services %} {% endfor %}
NameNamespaceTypeCluster IPExternal IPPorts
{{ svc.name }} {{ svc.namespace }} {{ svc.type }} {{ svc.cluster_ip }} {{ svc.external_ip }} {% for p in svc.ports %}{{ p.port }}{% if p.target_port %}:{{ p.target_port }}{% endif %}/{{ p.protocol }}{% endfor %}
{% else %}
🌐

No Kubernetes services found.

{% endif %}
{# ═══════════ TAB: Ingresses ═══════════ #}
{% if ingresses %}
{% for ing in ingresses %} {% endfor %}
NameNamespaceHostsTLSIngress Class
{{ ing.name }} {{ ing.namespace }} {% for h in ing.hosts %}{{ h }}{% endfor %} {% if ing.tls %} Yes{% else %}No{% endif %} {{ ing.class_name or '—' }}
{% else %}
🔀

No ingresses found.

{% endif %}
{# ═══════════ TAB: Manifests ═══════════ #}
{% if manifests %}
Workspace Manifests (k8s/)
{% for m in manifests %}
{{ m.filename }} {% set kind_lower = m.kind|lower %} {{ m.kind }} {% if m.name %}{{ m.name }}{% endif %}
{{ m.size }}
{% if m.content %}
{{ m.content }}
{% endif %}
{% endfor %} {% else %}
📄

No K8s manifests found. Run aq deploy kubernetes to generate production manifests.

{% endif %}
{# ═══════════ TAB: Events ═══════════ #}
{% if events %}
{% for evt in events|reverse %}
{{ evt.type }}
{{ evt.reason }} {{ evt.namespace }}
{{ evt.message }}
{% if evt.object %}
Object: {{ evt.object }}
{% endif %}
{{ evt.timestamp[:19] if evt.timestamp else '' }}
{% endfor %}
{% else %}
🔔
{% if kubectl_available %}

No recent cluster events.

{% else %}

Connect to a cluster to view events.

{% endif %}
{% endif %}
{# ═══════════ TAB: Logs ═══════════ #}
Pod Logs

No pod selected

Select a pod from the dropdown to view its logs.

{# ═══════════ TAB: Topology ═══════════ #}
Cluster Topology
{% if pods or services or deployments %}
Pod (Running) Pod (Failed) Pod (Pending) Deployment Service Ingress
{% else %}
🔗

Deploy some resources to see the cluster topology.

{% endif %}
{# ── Pod detail drawer ── #}

Pod

Pod Information
Containers
Loading...
Recent Events
Loading...
{# ── Cluster info footer ── #} {% if kubectl_available and cluster_info %}
kubectl {{ cluster_info.client_version|default('') }} {% if cluster_info.platform %}Platform: {{ cluster_info.platform }}{% endif %} {% if cluster_info.connected %} Cluster connected {% else %} Cluster not connected {% endif %} Namespaces: {{ namespaces|length }} Updated just now
{% endif %} {% endblock %} {% block extra_js %} /* ═══════════════════════════════════════════════════════════════ */ /* AqChart – lightweight canvas charting (no external deps) */ /* ═══════════════════════════════════════════════════════════════ */ var AqChart=window.AqChart||(function(){'use strict'; var C={green:'#22c55e',greenBg:'rgba(34,197,94,0.15)',blue:'#3b82f6',blueBg:'rgba(59,130,246,0.15)',amber:'#f59e0b',amberBg:'rgba(245,158,11,0.15)',red:'#ef4444',redBg:'rgba(239,68,68,0.15)',purple:'#a855f7',purpleBg:'rgba(168,85,247,0.15)',cyan:'#06b6d4',cyanBg:'rgba(6,182,212,0.15)',rose:'#f43f5e',roseBg:'rgba(244,63,94,0.15)',gray:'#71717a'}; function theme(){var d=document.documentElement.getAttribute('data-theme')!=='light';return{text:d?'rgba(255,255,255,0.5)':'rgba(0,0,0,0.5)',grid:d?'rgba(255,255,255,0.06)':'rgba(0,0,0,0.08)'};} function prep(cv){var ctx=cv.getContext('2d'),dpr=window.devicePixelRatio||1,r=cv.parentElement.getBoundingClientRect();cv.width=r.width*dpr;cv.height=r.height*dpr;ctx.scale(dpr,dpr);return{ctx:ctx,W:r.width,H:r.height};} function line(cv,ds,lbl,o){o=o||{};var s=prep(cv),ctx=s.ctx,W=s.W,H=s.H,t=theme();var p={t:10,r:14,b:28,l:42},pW=W-p.l-p.r,pH=H-p.t-p.b;var all=[];ds.forEach(function(d){all=all.concat(d.data);});var mx=Math.max.apply(null,all.concat([1])),mn=o.minY||0;if(o.maxY!==undefined)mx=o.maxY;ctx.strokeStyle=t.grid;ctx.lineWidth=1;for(var g=0;g<=4;g++){var gy=p.t+(pH/4)*g;ctx.beginPath();ctx.moveTo(p.l,gy);ctx.lineTo(W-p.r,gy);ctx.stroke();ctx.fillStyle=t.text;ctx.font='10px sans-serif';ctx.textAlign='right';ctx.fillText(o.yFmt?o.yFmt(mx-(mx-mn)*(g/4)):(mx-(mx-mn)*(g/4)).toFixed(0),p.l-6,gy+3);} if(lbl&&lbl.length){ctx.fillStyle=t.text;ctx.font='10px sans-serif';ctx.textAlign='center';var st=Math.max(1,Math.floor(lbl.length/6));for(var i=0;i0?'degraded':run===tot?'up':'degraded');} })(); function renderCharts(){ /* Phase doughnut */ var pc=document.getElementById('podPhaseChart'); if(pc){var r=parseInt(document.getElementById('stat-running').textContent)||0,p=parseInt(document.getElementById('stat-pending').textContent)||0,f=parseInt(document.getElementById('stat-failed').textContent)||0,tot=parseInt(document.getElementById('stat-total').textContent)||0,other=Math.max(0,tot-r-p-f); AqChart.doughnut(pc,[r,p,f,other],[AqChart.C.green,AqChart.C.amber,AqChart.C.red,AqChart.C.blue],{ct:String(tot),ir:0.65,legend:['Running','Pending','Failed','Other']});} /* Pod count line */ var pcl=document.getElementById('podCountChart'); if(pcl){AqChart.line(pcl,[{data:podCountH.running,color:AqChart.C.green,fill:true,bg:AqChart.C.greenBg},{data:podCountH.pending,color:AqChart.C.amber,fill:true,bg:AqChart.C.amberBg},{data:podCountH.failed,color:AqChart.C.red,fill:true,bg:AqChart.C.redBg}],pcLabels,{yFmt:function(v){return v.toFixed(0);}});} /* Restarts bar */ var rc=document.getElementById('restartsChart'); if(rc){var pods;try{pods={{ pods|tojson }};}catch(e){pods=[];}var pn=[],rv=[],cl=[],pl=[AqChart.C.green,AqChart.C.amber,AqChart.C.red,AqChart.C.blue,AqChart.C.purple,AqChart.C.cyan]; pods.sort(function(a,b){return(b.restart_count||0)-(a.restart_count||0);}).slice(0,10).forEach(function(p,i){pn.push(p.name.split('-').slice(-2).join('-'));rv.push(p.restart_count||0);cl.push(p.restart_count>5?AqChart.C.red:p.restart_count>0?AqChart.C.amber:AqChart.C.green);}); AqChart.bar(rc,rv,pn,cl);} /* Namespace distribution */ var nsc=document.getElementById('nsChart'); if(nsc){var pods2;try{pods2={{ pods|tojson }};}catch(e){pods2=[];}var nm={};pods2.forEach(function(p){nm[p.namespace]=(nm[p.namespace]||0)+1;});var nn=Object.keys(nm),nv=nn.map(function(k){return nm[k];}),ncl=[AqChart.C.purple,AqChart.C.cyan,AqChart.C.blue,AqChart.C.green,AqChart.C.amber,AqChart.C.rose]; AqChart.doughnut(nsc,nv,ncl,{ct:nn.length+' ns',ir:0.6,legend:nn});} /* Health timeline */ var ht=document.getElementById('healthTimeline'); if(ht){ht.innerHTML='';healthArr.forEach(function(s,i){var b=document.createElement('div');b.className='health-bar '+s;b.style.height=(s==='up'?100:s==='degraded'?60:20)+'%';b.setAttribute('data-tooltip','#'+(i+1)+': '+s);ht.appendChild(b);});} } /* ═══════════════════════════════════════════════════════════════ */ /* Topology */ /* ═══════════════════════════════════════════════════════════════ */ function renderK8sTopology(){ var cv=document.getElementById('k8sTopologyCanvas');if(!cv)return; var wrap=document.getElementById('k8sTopoWrap'); var pods,deps,svcs,ings; try{pods={{ pods|tojson }};}catch(e){pods=[];} try{deps={{ deployments|tojson }};}catch(e){deps=[];} try{svcs={{ services|tojson }};}catch(e){svcs=[];} try{ings={{ ingresses|tojson }};}catch(e){ings=[];} var ctx=cv.getContext('2d'),dpr=window.devicePixelRatio||1,rect=wrap.getBoundingClientRect(); cv.width=rect.width*dpr;cv.height=360*dpr;cv.style.width=rect.width+'px';cv.style.height='360px';ctx.scale(dpr,dpr); var W=rect.width,H=360,dk=document.documentElement.getAttribute('data-theme')!=='light'; var nodes=[],edges=[]; var layers={ingress:[],service:[],deployment:[],pod:[]}; ings.forEach(function(r){layers.ingress.push({name:r.name,type:'ingress',color:'#a855f7',ns:r.namespace});}); svcs.forEach(function(r){layers.service.push({name:r.name,type:'service',color:'#06b6d4',ns:r.namespace});}); deps.forEach(function(r){layers.deployment.push({name:r.name,type:'deployment',color:'#3b82f6',ns:r.namespace,image:r.image});}); pods.forEach(function(r){var c=r.phase_class==='running'?'#22c55e':r.phase_class==='failed'?'#ef4444':r.phase_class==='pending'?'#f59e0b':'#71717a';layers.pod.push({name:r.name,type:'pod',color:c,ns:r.namespace});}); var layerNames=['ingress','service','deployment','pod']; var layerX={}; layerNames.forEach(function(l,i){layerX[l]=60+(i/(layerNames.length-1||1))*(W-120);}); var allNodes={}; layerNames.forEach(function(l){ var items=layers[l],count=items.length||1; items.forEach(function(item,i){ var y=40+((i+1)/(count+1))*(H-80); item.x=layerX[l];item.y=y; allNodes[l+':'+item.name]=item; }); }); // Draw connections ctx.setLineDash([4,4]); ctx.lineWidth=1.5; // svc -> deployment (match by name prefix) layers.service.forEach(function(svc){ layers.deployment.forEach(function(dep){ if(dep.name.indexOf(svc.name)!==-1||svc.name.indexOf(dep.name)!==-1){ ctx.strokeStyle=dk?'rgba(6,182,212,0.2)':'rgba(6,182,212,0.3)'; ctx.beginPath();ctx.moveTo(svc.x,svc.y);ctx.lineTo(dep.x,dep.y);ctx.stroke(); } }); }); // deployment -> pod (match by name prefix) layers.deployment.forEach(function(dep){ pods.forEach(function(pod,i){ var pn=layers.pod[i];if(!pn)return; if(pod.name.indexOf(dep.name)!==-1){ ctx.strokeStyle=dk?'rgba(59,130,246,0.2)':'rgba(59,130,246,0.3)'; ctx.beginPath();ctx.moveTo(dep.x,dep.y);ctx.lineTo(pn.x,pn.y);ctx.stroke(); } }); }); // ingress -> service layers.ingress.forEach(function(ing){ layers.service.forEach(function(svc){ if(svc.ns===ing.ns){ ctx.strokeStyle=dk?'rgba(168,85,247,0.2)':'rgba(168,85,247,0.3)'; ctx.beginPath();ctx.moveTo(ing.x,ing.y);ctx.lineTo(svc.x,svc.y);ctx.stroke(); } }); }); ctx.setLineDash([]); // Draw layer labels ctx.font='bold 10px sans-serif';ctx.textAlign='center'; ctx.fillStyle=dk?'rgba(255,255,255,0.25)':'rgba(0,0,0,0.25)'; layerNames.forEach(function(l){if(layers[l].length)ctx.fillText(l.toUpperCase(),layerX[l],20);}); // Draw nodes for(var key in allNodes){ var n=allNodes[key],isPod=n.type==='pod'; ctx.beginPath(); if(isPod){ctx.arc(n.x,n.y,14,0,Math.PI*2);} else{ctx.moveTo(n.x-12,n.y-12);ctx.lineTo(n.x+12,n.y-12);ctx.lineTo(n.x+12,n.y+12);ctx.lineTo(n.x-12,n.y+12);ctx.closePath();} ctx.fillStyle=n.color.replace(')',',0.15)').replace('#','rgba(').replace(/([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i,function(m,r,g,b){return parseInt(r,16)+','+parseInt(g,16)+','+parseInt(b,16);}); ctx.fillStyle=dk?'rgba(26,26,46,0.8)':'rgba(255,255,255,0.9)'; ctx.fill(); ctx.strokeStyle=n.color;ctx.lineWidth=2;ctx.stroke(); ctx.beginPath();if(isPod){ctx.arc(n.x,n.y,4,0,Math.PI*2);}else{ctx.rect(n.x-4,n.y-4,8,8);} ctx.fillStyle=n.color;ctx.fill(); ctx.fillStyle=dk?'rgba(255,255,255,0.7)':'rgba(0,0,0,0.7)'; ctx.font='9px sans-serif';ctx.textAlign='center'; var lbl=n.name.length>18?n.name.substring(0,16)+'…':n.name; ctx.fillText(lbl,n.x,n.y+(isPod?22:20)); } } /* ═══════════════════════════════════════════════════════════════ */ /* Pod detail drawer */ /* ═══════════════════════════════════════════════════════════════ */ function openPodDrawer(row){ var d=row.dataset,drawer=document.getElementById('podDrawer'),backdrop=document.getElementById('podBackdrop'); document.getElementById('podDrawerTitle').textContent=d.pname; document.getElementById('podDrawerDot').className='phase-dot '+d.pphaseclass; document.getElementById('podDrawerKV').innerHTML= '
Name
'+d.pname+'
'+ '
Namespace
'+d.pns+'
'+ '
Phase
'+d.pphase+'
'+ '
Ready
'+d.pready+'
'+ '
Restarts
'+d.prestarts+'
'+ '
Node
'+(d.pnode||'—')+'
'+ '
IP
'+(d.pip||'—')+'
'+ '
Age
'+(d.page||'—')+'
'; document.getElementById('podDrawerContainers').innerHTML=''+d.pready+' containers ready'; // Show matching events var evts;try{evts={{ events|tojson }};}catch(e){evts=[];} var matched=evts.filter(function(e){return e.object&&e.object.indexOf(d.pname)!==-1;}); if(matched.length){ var html='';matched.slice(-5).forEach(function(e){ html+='
'+e.type+''+e.reason+': '+e.message+'
'; }); document.getElementById('podDrawerEvents').innerHTML=html; }else{document.getElementById('podDrawerEvents').innerHTML='No recent events';} drawer.classList.add('open');backdrop.classList.add('open');document.body.style.overflow='hidden'; } function closePodDrawer(){document.getElementById('podDrawer').classList.remove('open');document.getElementById('podBackdrop').classList.remove('open');document.body.style.overflow='';} document.addEventListener('keydown',function(e){if(e.key==='Escape')closePodDrawer();}); /* ═══════════════════════════════════════════════════════════════ */ /* Actions */ /* ═══════════════════════════════════════════════════════════════ */ function k8sAction(cmd){if(typeof showToast==='function')showToast('kubectl '+cmd+' triggered','info');} function scalePrompt(name,ns){var r=prompt('Scale deployment "'+name+'" to how many replicas?','3');if(r!==null)k8sAction('scale deployment/'+name+' --replicas='+r+' -n '+ns);} /* ═══════════════════════════════════════════════════════════════ */ /* Logs viewer */ /* ═══════════════════════════════════════════════════════════════ */ var _podLogTimer=null; function loadPodLogs(podName){ if(_podLogTimer)clearInterval(_podLogTimer); var body=document.getElementById('podLogsPanelBody'),nm=document.getElementById('logPodName'); if(!podName){body.innerHTML='

Select a pod.

';nm.textContent='No pod selected';return;} nm.textContent=podName;body.innerHTML='Loading...'; function poll(){fetch('{{ url_prefix|default("/admin") }}/pods/api/').then(function(r){return r.json();}).then(function(data){ var p=(data.pods||[]).find(function(pd){return pd.name===podName;});if(!p)return;nm.textContent=p.name; var now=new Date().toLocaleTimeString(); var lines='
['+now+'] Phase: '+p.phase+' | Ready: '+p.ready_count+'/'+p.container_count+' | Restarts: '+p.restart_count; if(p.node)lines+=' | Node: '+p.node; lines+='
'; var prev=body.innerHTML;if(prev.indexOf('Loading')!==-1||prev.indexOf('empty-state')!==-1)prev='';body.innerHTML=prev+lines; if(document.getElementById('logFollow').checked)body.scrollTop=body.scrollHeight; }).catch(function(){});} poll();_podLogTimer=setInterval(poll,5000); } function copyPodLogs(){var t=document.getElementById('podLogsPanelBody').innerText;navigator.clipboard.writeText(t).then(function(){if(typeof showToast==='function')showToast('Copied','success');});} function downloadPodLogs(){var t=document.getElementById('podLogsPanelBody').innerText,b=new Blob([t],{type:'text/plain'}),a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='pod-logs-'+new Date().toISOString().replace(/[:.]/g,'-')+'.txt';a.click();} function clearPodLogs(){document.getElementById('podLogsPanelBody').innerHTML='

Cleared.

';} /* ═══════════════════════════════════════════════════════════════ */ /* Auto-refresh */ /* ═══════════════════════════════════════════════════════════════ */ (function(){ setTimeout(renderCharts,150); setInterval(function(){ fetch('{{ url_prefix|default("/admin") }}/pods/api/').then(function(r){return r.json();}).then(function(data){ var pods=data.pods||[]; var running=pods.filter(function(p){return p.phase==='Running';}).length; var pending=pods.filter(function(p){return p.phase==='Pending';}).length; var failed=pods.filter(function(p){return p.phase==='Failed';}).length; var el; el=document.getElementById('stat-total');if(el)el.textContent=pods.length; el=document.getElementById('stat-running');if(el)el.textContent=running; el=document.getElementById('stat-pending');if(el)el.textContent=pending; el=document.getElementById('stat-failed');if(el)el.textContent=failed; el=document.getElementById('stat-deploys');if(el)el.textContent=(data.deployments||[]).length; el=document.getElementById('stat-services');if(el)el.textContent=(data.services||[]).length; var now=new Date(); pcLabels.push(now.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}));if(pcLabels.length>MAX_PTS)pcLabels.shift(); podCountH.running.push(running);podCountH.pending.push(pending);podCountH.failed.push(failed); if(podCountH.running.length>MAX_PTS){podCountH.running.shift();podCountH.pending.shift();podCountH.failed.shift();} healthArr.push(pods.length===0?'down':failed>0?'degraded':running===pods.length?'up':'degraded'); if(healthArr.length>60)healthArr.shift(); renderCharts(); var ts=document.getElementById('k8sLastUpdated');if(ts)ts.textContent='Updated '+now.toLocaleTimeString(); }).catch(function(){}); },15000); new MutationObserver(function(){setTimeout(renderCharts,50);}).observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']}); window.addEventListener('resize',function(){setTimeout(renderCharts,100);}); })(); {% endblock %}