{% extends "base.html" %} {% block title %}Containers{% endblock %} {% block breadcrumb %}Containers{% endblock %} {% block extra_head %} {% endblock %} {% block content %} {% if not docker_available %}
🐳

Docker Not Available

{{ error or 'Docker daemon is not running or the Docker CLI is not installed.' }}

Install Docker: brew install docker or visit docs.docker.com

{% if dockerfile_info and dockerfile_info.exists %}

A Dockerfile was found in your workspace. Start Docker to manage containers.

{% endif %}
{% else %} {# ── Stats Overview ── #}
{{ system_info.running|default(0) }}
Running
{{ system_info.stopped|default(0) }}
Stopped
{{ system_info.paused|default(0) }}
Paused
{{ images|length }}
Images
{{ volumes|length }}
Volumes
{{ networks|length }}
Networks
{# ── Resource Charts ── #} {% if containers %}
CPU Usage (Real-time)
Memory Usage (Real-time)
Network I/O
Container State Distribution
{# ── Health Timeline ── #}
Container Health (Last 60 checks)
Healthy Degraded Unhealthy
{% endif %} {# ── Tabs ── #}
{# ═══════════ TAB: Containers ═══════════ #}
{# Bulk action bar #}
0 containers selected
{# Toolbar #}
{% if containers %} {% for c in containers %} {% endfor %} {% else %} {% endif %}
NameImageStatusPortsCPUMemoryNet I/OUptimeActions
{{ c.name }}
{{ c.image }} {{ c.status_icon }} {{ c.state }} {% if c.ports %} {% for port in c.ports.split(', ') if port %}{{ port }}{% endfor %} {% else %}{% endif %} {% if c.stats %} {{ c.stats.cpu }} {% set cpu_val = c.stats.cpu|replace('%','')|float %}
{% else %}{% endif %}
{% if c.stats %} {{ c.stats.memory }} {% set mem_val = c.stats.mem_perc|replace('%','')|float %}
{% else %}{% endif %}
{% if c.stats %}{{ c.stats.net_io }}{% else %}{% endif %} {{ c.running_for|default(c.created[:19] if c.created else '—') }}
{% if c.state == 'running' %} {% elif c.state == 'exited' or c.state == 'created' or c.state == 'dead' %} {% elif c.state == 'paused' %} {% elif c.state == 'restarting' %} {% endif %}
📦
No containers found. Run docker compose up -d or click Run to start one.
{# ═══════════ TAB: Compose ═══════════ #}
{% if compose.available %}
Compose Services
{% for svc in compose.services %}
{{ svc.name }}
{% if svc.healthcheck %} healthcheck{% endif %} {% if svc.profiles %}{% for p in svc.profiles %}{{ p }}{% endfor %}{% endif %}
{% if svc.image %}
{{ svc.image }}
{% elif svc.build %}
build: {{ svc.build }}
{% endif %} {% if svc.ports %}
{% for p in svc.ports %}{{ p }}{% endfor %}
{% endif %} {% if svc.depends_on %}
depends: {{ svc.depends_on|join(', ') }}
{% endif %} {% if svc.restart %}
{{ svc.restart }}
{% endif %} {% if svc.volumes %}
{{ svc.volumes|length }} vol(s)
{% endif %} {% if svc.environment %}
{% if svc.environment is mapping %}{{ svc.environment|length }}{% else %}{{ svc.environment|length }}{% endif %} env var(s)
{% endif %}
{% endfor %} {% if compose.file_content %}
View docker-compose.yml
{{ compose.file_content }}
{% endif %} {% else %}
📋

No docker-compose.yml found. Run aq deploy compose to generate one.

{% endif %}
{# ═══════════ TAB: Images ═══════════ #}
{% if images %}
Image Size Distribution
{% for img in images %} {% endfor %}
RepositoryTagImage IDSizeCreatedActions
{{ img.repository }} {{ img.tag }} {{ img.id[:12] }} {{ img.size }} {{ img.created[:19] if img.created else '—' }}
{% else %}
💿

No Docker images found.

{% endif %}
{# ═══════════ TAB: Volumes ═══════════ #}
{% if volumes %}
Volumes
{% for vol in volumes %} {% endfor %}
NameDriverMountpointActions
{{ vol.name }} {{ vol.driver }} {{ vol.mountpoint }}
{% else %}
💾

No Docker volumes found.

{% endif %}
{# ═══════════ TAB: Networks ═══════════ #}
{% if networks %}
Networks
{% for net in networks %} {% endfor %}
NameDriverScopeNetwork IDActions
{{ net.name }} {{ net.driver }} {{ net.scope }} {{ net.id[:12] }}
{% if net.name not in ['bridge', 'host', 'none'] %} {% endif %}
{% else %}
🌐

No Docker networks found.

{% endif %}
{# ═══════════ TAB: Dockerfile ═══════════ #}
{% if dockerfile_info and dockerfile_info.exists %}

Dockerfile

{{ dockerfile_info.path }}
{{ dockerfile_info.size }}
{% if dockerfile_info.stages %}
Build Stages
{% for stage in dockerfile_info.stages %}{{ stage.image }}{% if stage.alias %} → {{ stage.alias }}{% endif %}{% endfor %}
{% endif %}
{{ dockerfile_info.base_image|default('—') }}
Base Image
{{ dockerfile_info.stages|length }}
Stages
{{ dockerfile_info.exposed_ports|length }}
Exposed Ports
{{ dockerfile_info.run_count|default(0) }}
RUN
{{ dockerfile_info.copy_count|default(0) }}
COPY
{% if dockerfile_info.exposed_ports %}
Exposed Ports
{% for p in dockerfile_info.exposed_ports %}{{ p }}{% endfor %}
{% endif %} {% if dockerfile_info.env_vars %}
Environment Variables
{% for e in dockerfile_info.env_vars %}{{ e }}{% endfor %}
{% endif %}
{% else %}
📄

No Dockerfile found. Run aq deploy dockerfile to generate one.

{% endif %}
{# ═══════════ TAB: Build ═══════════ #}
{% if dockerfile_info and dockerfile_info.exists %}
Docker Build
{% if dockerfile_info.stages %}
Build Stages
{% for stage in dockerfile_info.stages %}
{{ loop.index }}. {{ stage.image }}{% if stage.alias %} AS {{ stage.alias }}{% endif %}
{% endfor %}
{% endif %}
Build Output
Click "Build Image" to start a build. Output will stream here.
{% else %}
🔨

No Dockerfile found. Create a Dockerfile first to use the build feature.

{% endif %}
{# ═══════════ TAB: System / Disk Usage ═══════════ #}
Docker System & Disk Usage
{{ docker_version }}
Docker Version
{% if system_info.os %}
{{ system_info.os }}
Operating System
{% endif %} {% if system_info.arch %}
{{ system_info.arch }}
Architecture
{% endif %} {% if system_info.cpus %}
{{ system_info.cpus }}
CPUs
{% endif %} {% if system_info.storage_driver %}
{{ system_info.storage_driver }}
Storage Driver
{% endif %}

Disk Usage


Loading disk usage...
Cleanup & Prune

Free up disk space by removing unused Docker objects. ⚠ These operations are irreversible.

{# ═══════════ TAB: Events ═══════════ #}
Docker Events


Loading events...
{# ═══════════ TAB: Logs (Terminal-style) ═══════════ #}
Container Logs

No container selected

Select a container from the dropdown to view its real-time logs.

{# ═══════════ TAB: Topology ═══════════ #}
Service Topology
{% if compose.available and compose.services %}
Running Stopped Service Dependency
{% else %}
🔗

Add a docker-compose.yml with service dependencies to view the topology graph.

{% endif %}
{% endif %} {# ── Run Container Dialog ── #}

Run Container

{# ── Create Network Dialog ── #}

Create Network

{# ── Create Volume Dialog ── #}

Create Volume

{# ── Docker Command Loading Overlay ── #}
Executing Command
Processing your request...
{# ── Custom Confirm Modal ── #}
⚠️

Confirm Action

Are you sure you want to proceed?

{# ── Detail drawer (multi-purpose: container / volume / network / image) ── #}

Details



Loading...
{# ── System info footer ── #} {% if docker_available and system_info %}
Docker v{{ docker_version }} {% if system_info.os %}OS: {{ system_info.os }}{% endif %} {% if system_info.arch %}Arch: {{ system_info.arch }}{% endif %} {% if system_info.cpus %}CPUs: {{ system_info.cpus }}{% endif %} {% if system_info.storage_driver %}Storage: {{ system_info.storage_driver }}{% endif %} Updated just now
{% endif %} {% endblock %} {% block extra_js %} /* ═══════════════════════════════════════════════════════════════ API Prefix ═══════════════════════════════════════════════════════════════ */ var _API='{{ url_prefix|default("/admin") }}'; /* ═══════════════════════════════════════════════════════════════ Tab switching – unique name avoids base.html switchTab clash ═══════════════════════════════════════════════════════════════ */ function switchInfraTab(btn,panelId){ document.querySelectorAll('.infra-tab').forEach(function(t){t.classList.remove('active');}); document.querySelectorAll('.infra-tab-panel').forEach(function(p){p.classList.remove('active');}); btn.classList.add('active'); var panel=document.getElementById(panelId); if(panel)panel.classList.add('active'); if(panelId==='tab-topology')setTimeout(renderTopology,50); if(panelId==='tab-images')setTimeout(renderImageSizeChart,50); if(panelId==='tab-system')loadDiskUsage(); if(panelId==='tab-events')loadDockerEvents(); } /* ═══════════════════════════════════════════════════════════════ JSON syntax-highlighted renderer ═══════════════════════════════════════════════════════════════ */ function jsonToHtml(obj,indent){ indent=indent||0; var sp=' '.repeat(indent); if(obj===null)return 'null'; if(typeof obj==='boolean')return ''+obj+''; if(typeof obj==='number')return ''+obj+''; if(typeof obj==='string'){ var esc=obj.replace(/&/g,'&').replace(//g,'>'); if(esc.length>120)esc=esc.substring(0,120)+'…'; return '"'+esc+'"'; } if(Array.isArray(obj)){ if(!obj.length)return '[]'; var items=obj.map(function(v){return sp+' '+jsonToHtml(v,indent+1);}); return '[\n'+items.join(',\n')+'\n'+sp+']'; } if(typeof obj==='object'){ var keys=Object.keys(obj); if(!keys.length)return '{}'; var entries=keys.map(function(k){ return sp+' "'+k.replace(/: '+jsonToHtml(obj[k],indent+1); }); return '{\n'+entries.join(',\n')+'\n'+sp+'}'; } return String(obj); } /* ═══════════════════════════════════════════════════════════════ POST helper (form-encoded) ═══════════════════════════════════════════════════════════════ */ function postAPI(endpoint,params){ var body=new URLSearchParams(); for(var k in params)body.append(k,params[k]); var csrfMeta=document.querySelector('meta[name="csrf-token"]'); var hdrs={'Content-Type':'application/x-www-form-urlencoded'}; if(csrfMeta&&csrfMeta.content)hdrs['X-CSRF-Token']=csrfMeta.content; return fetch(_API+endpoint,{ method:'POST', headers:hdrs, body:body.toString() }).then(function(r){return r.json();}); } /* ═══════════════════════════════════════════════════════════════ Custom Confirm Modal (replaces browser confirm()) ═══════════════════════════════════════════════════════════════ */ var _confirmResolveFn=null; function _confirmResolve(val){ var bd=document.getElementById('confirmModalBackdrop'); bd.classList.remove('active'); if(_confirmResolveFn)_confirmResolveFn(val); _confirmResolveFn=null; } function showConfirm(opts){ return new Promise(function(resolve){ _confirmResolveFn=resolve; var bd=document.getElementById('confirmModalBackdrop'); var icon=document.getElementById('confirmModalIcon'); var title=document.getElementById('confirmModalTitle'); var msg=document.getElementById('confirmModalMessage'); var btn=document.getElementById('confirmModalBtn'); var type=opts.type||'danger'; icon.className='confirm-modal-icon '+type; icon.textContent=type==='danger'?'🗑️':type==='warning'?'⚠️':'ℹ️'; title.textContent=opts.title||'Confirm Action'; msg.textContent=opts.message||'Are you sure?'; btn.textContent=opts.confirmText||'Confirm'; btn.className='confirm-modal-btn confirm-'+type; bd.classList.add('active'); }); } /* ═══════════════════════════════════════════════════════════════ Docker Command Loading Overlay ═══════════════════════════════════════════════════════════════ */ function showDockerLoading(title,message){ var ov=document.getElementById('dockerCmdOverlay'); var card=document.getElementById('dockerCmdCard'); document.getElementById('dockerCmdSpinner').style.display=''; document.getElementById('dockerCmdCheck').style.display='none'; document.getElementById('dockerCmdTitle').textContent=title||'Executing Command'; document.getElementById('dockerCmdMessage').textContent=message||'Processing your request...'; document.getElementById('dockerCmdProgressBar').parentElement.style.display=''; card.classList.remove('docker-cmd-done'); ov.classList.add('active'); } function hideDockerLoading(success,message,delay){ var card=document.getElementById('dockerCmdCard'); var spinner=document.getElementById('dockerCmdSpinner'); var check=document.getElementById('dockerCmdCheck'); var msg=document.getElementById('dockerCmdMessage'); var title=document.getElementById('dockerCmdTitle'); spinner.style.display='none'; check.style.display='flex'; check.className='docker-cmd-check '+(success?'success':'error'); check.textContent=success?'✓':'✕'; title.textContent=success?'Success':'Failed'; msg.textContent=message||''; document.getElementById('dockerCmdProgressBar').parentElement.style.display='none'; card.classList.add('docker-cmd-done'); setTimeout(function(){ document.getElementById('dockerCmdOverlay').classList.remove('active'); },delay||1800); } /* ═══════════════════════════════════════════════════════════════ Container Actions (real docker commands) ═══════════════════════════════════════════════════════════════ */ function containerAction(id,act,btn){ var _labels={stop:'Stop Container',start:'Start Container',restart:'Restart Container',pause:'Pause Container',unpause:'Unpause Container',kill:'Kill Container',rm:'Remove Container'}; var _msgs={ rm:{type:'danger',title:'Remove Container',message:'Remove container '+id.substring(0,12)+'? This will permanently delete the container and cannot be undone.',confirmText:'Remove'}, kill:{type:'warning',title:'Kill Container',message:'Forcefully kill container '+id.substring(0,12)+'? The container will be immediately terminated.',confirmText:'Kill'} }; function _exec(){ showDockerLoading(_labels[act]||('Docker '+act),'Running docker '+act+' on '+id.substring(0,12)+'...'); if(btn){btn.disabled=true;btn.innerHTML='';} postAPI('/containers/action/',{container_id:id,action:act}).then(function(res){ if(res.success){ hideDockerLoading(true,res.message||'Container '+act+' successful',1800); } else { hideDockerLoading(false,res.error||'Action failed',2500); } setTimeout(function(){refreshContainersLive();},2200); }).catch(function(e){ hideDockerLoading(false,'Request failed: '+e.message,2500); if(btn){btn.disabled=false;btn.innerHTML='Error';} }); } if(_msgs[act]){ showConfirm(_msgs[act]).then(function(ok){if(ok)_exec();}); } else { _exec(); } } /* ═══════════════════════════════════════════════════════════════ Bulk container actions ═══════════════════════════════════════════════════════════════ */ function toggleSelectAll(el){ document.querySelectorAll('.row-select').forEach(function(cb){cb.checked=el.checked;}); updateBulkBar(); } function updateBulkBar(){ var checked=document.querySelectorAll('.row-select:checked'); var bar=document.getElementById('bulkBar'),cnt=document.getElementById('bulkCount'); if(checked.length>0){bar.classList.add('visible');cnt.textContent=checked.length;} else{bar.classList.remove('visible');} } function bulkAction(act){ var ids=[]; document.querySelectorAll('.row-select:checked').forEach(function(cb){ids.push(cb.value);}); if(!ids.length)return; function _exec(){ showDockerLoading('Bulk '+act,'Executing '+act+' on '+ids.length+' container(s)...'); var chain=Promise.resolve(); ids.forEach(function(id){ chain=chain.then(function(){return postAPI('/containers/action/',{container_id:id,action:act});}); }); chain.then(function(){ hideDockerLoading(true,'Completed '+act+' on '+ids.length+' container(s)',2000); setTimeout(function(){refreshContainersLive();},2500); }).catch(function(e){ hideDockerLoading(false,'Failed: '+e.message,2500); }); } if(act==='rm'){ showConfirm({type:'danger',title:'Remove '+ids.length+' Container(s)',message:'This will permanently remove '+ids.length+' selected container(s). This action cannot be undone.',confirmText:'Remove All'}).then(function(ok){if(ok)_exec();}); } else { _exec(); } } /* ═══════════════════════════════════════════════════════════════ Filter / Search ═══════════════════════════════════════════════════════════════ */ function filterContainers(q){ q=(q||'').toLowerCase(); var stateF=document.getElementById('stateFilter').value.toLowerCase(); document.querySelectorAll('#containersTable tbody tr').forEach(function(row){ var name=(row.getAttribute('data-cname')||'').toLowerCase(); var image=(row.getAttribute('data-cimage')||'').toLowerCase(); var state=(row.getAttribute('data-cstate')||'').toLowerCase(); var matchQ=!q||name.indexOf(q)!==-1||image.indexOf(q)!==-1; var matchS=!stateF||state===stateF; row.style.display=(matchQ&&matchS)?'':'none'; }); } function filterImages(q){ q=(q||'').toLowerCase(); document.querySelectorAll('#imagesTable tbody tr').forEach(function(row){ var repo=(row.getAttribute('data-imgrepo')||'').toLowerCase(); var tag=(row.getAttribute('data-imgtag')||'').toLowerCase(); row.style.display=(!q||repo.indexOf(q)!==-1||tag.indexOf(q)!==-1)?'':'none'; }); } /* ═══════════════════════════════════════════════════════════════ Compose Actions (real docker compose commands) ═══════════════════════════════════════════════════════════════ */ function composeAction(act,btn){ function _exec(){ showDockerLoading('Compose '+act,'Running docker compose '+act+'...'); if(btn){btn.disabled=true;var orig=btn.innerHTML;btn.innerHTML='';} postAPI('/containers/compose-action/',{action:act}).then(function(res){ if(res.success){ hideDockerLoading(true,res.message||'Compose '+act+' successful',2000); if(res.output){ var el=document.getElementById('composeOutput'); var body=document.getElementById('composeOutputBody'); if(el&&body){el.style.display='block';body.textContent=res.output;} } } else { hideDockerLoading(false,res.error||'Compose action failed',2500); } if(btn){btn.disabled=false;btn.innerHTML=orig;} setTimeout(function(){refreshContainersLive();},2800); }).catch(function(e){ hideDockerLoading(false,'Request failed: '+e.message,2500); if(btn){btn.disabled=false;btn.innerHTML=orig;} }); } if(act==='down'){ showConfirm({type:'warning',title:'Compose Down',message:'Run docker compose down? This will stop and remove all compose containers and their networks.',confirmText:'Shut Down'}).then(function(ok){if(ok)_exec();}); } else { _exec(); } } /* ═══════════════════════════════════════════════════════════════ Image Actions ═══════════════════════════════════════════════════════════════ */ function imageAction(id,act,btn){ function _exec(){ showDockerLoading(act==='rm'?'Remove Image':'Image '+act,'Processing image '+id.substring(0,12)+'...'); if(btn){btn.disabled=true;btn.innerHTML='';} postAPI('/containers/image-action/',{image_id:id,action:act}).then(function(res){ if(res.success){hideDockerLoading(true,res.message,1800);} else{hideDockerLoading(false,res.error||'Failed',2500);} setTimeout(function(){refreshContainersLive();},2200); }).catch(function(e){ hideDockerLoading(false,'Failed: '+e.message,2500); if(btn){btn.disabled=false;btn.innerHTML='Error';} }); } if(act==='rm'){ showConfirm({type:'danger',title:'Remove Image',message:'Remove image '+id.substring(0,12)+'? Containers using this image must be removed first.',confirmText:'Remove'}).then(function(ok){if(ok)_exec();}); } else { _exec(); } } function promptPullImage(){ var img=prompt('Enter image to pull (e.g., nginx:latest, postgres:16):'); if(!img)return; showDockerLoading('Pulling Image','Downloading '+img+'... This may take a minute.'); postAPI('/containers/image-action/',{image_id:img,action:'pull'}).then(function(res){ if(res.success){hideDockerLoading(true,'Image pulled successfully',1800);} else{hideDockerLoading(false,res.error||'Pull failed',2500);} setTimeout(function(){refreshContainersLive();},2200); }).catch(function(e){ hideDockerLoading(false,'Failed: '+e.message,2500); }); } /* ═══════════════════════════════════════════════════════════════ Volume / Network Actions ═══════════════════════════════════════════════════════════════ */ function volumeAction(name,act,btn){ function _exec(){ showDockerLoading('Remove Volume','Removing volume "'+name+'"...'); if(btn){btn.disabled=true;btn.innerHTML='';} postAPI('/containers/volume-action/',{name:name,action:act}).then(function(res){ if(res.success){hideDockerLoading(true,res.message,1800);} else{hideDockerLoading(false,res.error||'Failed',2500);} setTimeout(function(){refreshContainersLive();},2200); }).catch(function(e){hideDockerLoading(false,'Failed',2500);}); } if(act==='rm'){ showConfirm({type:'danger',title:'Remove Volume',message:'Remove volume "'+name+'"? All stored data will be permanently lost.',confirmText:'Delete Volume'}).then(function(ok){if(ok)_exec();}); } else { _exec(); } } function networkAction(id,act,btn){ function _exec(){ showDockerLoading('Remove Network','Removing network '+id.substring(0,12)+'...'); if(btn){btn.disabled=true;btn.innerHTML='';} postAPI('/containers/network-action/',{network_id:id,action:act}).then(function(res){ if(res.success){hideDockerLoading(true,res.message,1800);} else{hideDockerLoading(false,res.error||'Failed',2500);} setTimeout(function(){refreshContainersLive();},2200); }).catch(function(e){hideDockerLoading(false,'Failed',2500);}); } if(act==='rm'){ showConfirm({type:'danger',title:'Remove Network',message:'Remove network '+id.substring(0,12)+'? Containers attached to this network will lose connectivity.',confirmText:'Remove'}).then(function(ok){if(ok)_exec();}); } else { _exec(); } } /* ═══════════════════════════════════════════════════════════════ Container Detail Drawer (Docker Desktop-style inspect) ═══════════════════════════════════════════════════════════════ */ var _drawerType=''; function openContainerDrawer(cid){ _drawerType='container'; var drawer=document.getElementById('detailDrawer'),backdrop=document.getElementById('drawerBackdrop'); var row=document.querySelector('tr[data-cid="'+cid+'"]'); var name=row?row.getAttribute('data-cname'):cid.substring(0,12); var state=row?row.getAttribute('data-cstate'):'unknown'; var status=row?row.getAttribute('data-cstatus'):'unknown'; document.getElementById('drawerTitle').textContent=name; document.getElementById('drawerStatusDot').className='status-dot '+status; document.getElementById('drawerBody').innerHTML='


Loading container details...
'; /* Draw inner tabs */ document.getElementById('drawerTabsWrap').innerHTML= '
'+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ '
'; /* Actions bar */ var acts=document.getElementById('drawerActions'); if(state==='running'){ acts.innerHTML=''+ ''+ ''+ ''+ ''; } else if(state==='exited'||state==='created'||state==='dead'){ acts.innerHTML=''+ ''+ ''; } else if(state==='paused'){ acts.innerHTML=''+ ''; } else { acts.innerHTML=''; } drawer.classList.add('open');backdrop.classList.add('open');document.body.style.overflow='hidden'; /* Fetch full inspect data */ postAPI('/containers/inspect/',{container_id:cid}).then(function(res){ if(!res.success){ document.getElementById('drawerBody').innerHTML='
'+escHtml(res.error||'Inspect failed')+'
'; return; } var d=res.data; var conf=d.Config||{}; var state_d=d.State||{}; var net=d.NetworkSettings||{}; var mounts=d.Mounts||[]; var hostCfg=d.HostConfig||{}; /* Overview tab */ var overviewHtml='
Container Info
'+ '
ID
'+(d.Id||'').substring(0,12)+'
'+ '
Name
'+(d.Name||'').replace(/^\//,'')+'
'+ '
Image
'+escHtml(conf.Image||'')+'
'+ '
Status
'+(state_d.Status||'')+'
'+ '
Created
'+(d.Created||'').substring(0,19)+'
'+ '
Started
'+(state_d.StartedAt||'').substring(0,19)+'
'+ '
Platform
'+(d.Platform||'linux')+'
'+ '
Restart Policy
'+(hostCfg.RestartPolicy?hostCfg.RestartPolicy.Name||'no':'no')+'
'+ '
Command
'+escHtml((conf.Cmd||[]).join(' '))+'
'+ '
Entrypoint
'+escHtml((conf.Entrypoint||[]).join(' ')||'—')+'
'+ '
Working Dir
'+escHtml(conf.WorkingDir||'/')+'
'+ '
'; /* Resource stats section */ overviewHtml+='
Resource Stats
Loading live stats...
'; /* Ports section */ var ports=net.Ports||{}; var portKeys=Object.keys(ports); if(portKeys.length){ overviewHtml+='
Ports
'; portKeys.forEach(function(pk){ var bindings=ports[pk]||[]; if(bindings.length){ bindings.forEach(function(b){overviewHtml+=''+b.HostIp+':'+b.HostPort+' → '+pk+'';}); } else { overviewHtml+=''+pk+' (not bound)'; } }); overviewHtml+='
'; } /* Labels */ var labels=conf.Labels||{}; var lk=Object.keys(labels); if(lk.length){ overviewHtml+='
Labels ('+lk.length+')
Show labels
'; lk.forEach(function(k){overviewHtml+='
'+escHtml(k)+'
'+escHtml(labels[k])+'
';}); overviewHtml+='
'; } overviewHtml+='
'; /* Environment tab */ var envs=conf.Env||[]; var envHtml='
Environment Variables ('+envs.length+')
'; if(envs.length){ envHtml+=''; envs.forEach(function(e){var parts=e.split('=');var k=parts[0];var v=parts.slice(1).join('='); envHtml+=''; }); envHtml+='
VariableValue
'+escHtml(k)+''+escHtml(v)+'
'; } else { envHtml+='

No environment variables set.

'; } envHtml+='
'; /* Mounts tab */ var mountHtml='
Mounts ('+mounts.length+')
'; if(mounts.length){ mounts.forEach(function(m){ var typeClass=m.Type==='bind'?'mount-bind':m.Type==='volume'?'mount-volume':'mount-tmpfs'; mountHtml+='
'+m.Type+''+(m.RW?'RW':'RO')+'
'+ '
'+escHtml(m.Source||'')+'
'+ '
→ '+escHtml(m.Destination||'')+'
'; }); } else { mountHtml+='

No mounts configured.

'; } mountHtml+='
'; /* Network tab */ var nets=net.Networks||{}; var nk=Object.keys(nets); var netHtml='
Networks ('+nk.length+')
'; if(nk.length){ nk.forEach(function(nn){ var ni=nets[nn]; netHtml+='
'+escHtml(nn)+'
'+ '
IP Address
'+(ni.IPAddress||'—')+'
'+ '
Gateway
'+(ni.Gateway||'—')+'
'+ '
MAC Address
'+(ni.MacAddress||'—')+'
'+ '
Network ID
'+(ni.NetworkID||'').substring(0,12)+'
'; }); } else { netHtml+='

Not connected to any network.

'; } netHtml+='
'; /* Logs tab */ var logsHtml='
'+ '
'+ '
Container Logs
'+ '
'+ '
'+ 'Loading logs...
'; /* Inspect tab (raw JSON) */ var inspectHtml='
Raw Inspect Output
'+ '
'+jsonToHtml(d)+'
'; /* Exec tab (shell) */ var execHtml='
'+ '
Execute Command
'+ '

Run commands inside the container. '+(state==='running'?'':'Container must be running to exec.')+'

'+ '
🖥️ '+escHtml(name)+' shell
'+ '
Type a command below and press Enter...
'+ '
$
'+ '
'+ ''+ ''+ ''+ ''+ ''+ ''+ '
'; /* Processes tab */ var procHtml='
'+ '
'+ '
Running Processes
'+ '
'+ '
'+(state==='running'?'

Loading...
':'Container is not running')+'
'+ '
'; /* Filesystem Changes tab */ var diffHtml='
'+ '
'+ '
Filesystem Changes
'+ '
'+ '

Files added (A), changed (C) or deleted (D) since container creation.

'+ '

Loading...
'+ '
'; document.getElementById('drawerBody').innerHTML=overviewHtml+envHtml+mountHtml+netHtml+execHtml+procHtml+diffHtml+logsHtml+inspectHtml; /* Set up exec input */ setTimeout(function(){ var inp=document.getElementById('execInput'); if(inp){inp.addEventListener('keydown',function(e){if(e.key==='Enter'&&this.value.trim()){runExecCommand(cid,this.value.trim());this.value='';}});} },100); /* Fetch live stats */ fetch(_API+'/containers/api/').then(function(r){return r.json();}).then(function(data){ var c=(data.containers||[]).find(function(ct){return ct.id===cid;}); var el=document.getElementById('drawerLiveStats'); if(c&&c.stats&&el){ el.innerHTML='
'+ '
CPU: '+c.stats.cpu+'
'+ '
Memory: '+c.stats.memory+'
'+ '
Net I/O: '+c.stats.net_io+'
'+ '
Block I/O: '+(c.stats.block_io||'—')+'
'+ '
PIDs: '+(c.stats.pids||'—')+'
'; } else if(el){ el.innerHTML='Container is not running'; } }); /* Fetch logs for drawer */ fetchDrawerLogs(cid); /* Fetch processes for running containers */ if(state==='running') loadContainerProcesses(cid); /* Fetch filesystem diff */ loadContainerDiff(cid); }).catch(function(e){ document.getElementById('drawerBody').innerHTML='
Error: '+escHtml(e.message)+'
'; }); } var _drawerLogTimer=null,_drawerLogTs=''; function fetchDrawerLogs(cid){ var body=document.getElementById('drawerLogBody'); if(!body)return; if(_drawerLogTimer){clearInterval(_drawerLogTimer);_drawerLogTimer=null;} _drawerLogTs=''; body.innerHTML='Connecting...'; _fetchDrawerLogsInc(cid,true); /* Live poll every 2s for new lines */ _drawerLogTimer=setInterval(function(){_fetchDrawerLogsInc(cid,false);},2000); } function _fetchDrawerLogsInc(cid,isInit){ var params={container_id:cid,tail:isInit?'100':'500'}; if(!isInit&&_drawerLogTs)params.since=_drawerLogTs; postAPI('/containers/logs/',params).then(function(res){ var body=document.getElementById('drawerLogBody'); if(!body)return; if(res.success&&res.logs){ var lines=res.logs.split('\n'),frag=[],newTs=_drawerLogTs; lines.forEach(function(l){ if(!l.trim())return; var cls='log-stdout'; if(l.match(/error|Error|ERROR|fatal|Fatal|FATAL|panic|exception/i))cls='log-stderr'; else if(l.match(/warn|Warn|WARN/i))cls='log-warn'; else if(l.match(/info|Info|INFO/i))cls='log-info'; var ts='',msg=l; var tsMatch=l.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s*(.*)/); if(tsMatch){ts=tsMatch[1].substring(11,19);msg=tsMatch[2];if(tsMatch[1]>newTs)newTs=tsMatch[1];} var line='
'; if(ts)line+='['+ts+']'; line+=''+escHtml(msg)+'
'; frag.push(line); }); if(frag.length>0){ if(isInit){body.innerHTML=frag.join('');} else{body.insertAdjacentHTML('beforeend',frag.join(''));} _drawerLogTs=newTs; body.scrollTop=body.scrollHeight; } else if(isInit){ body.innerHTML='No logs yet — waiting...'; } } else if(isInit){ body.innerHTML=''+(res.error||'No logs')+''; } }).catch(function(){if(isInit)body.innerHTML='Failed to fetch logs';}); } function switchDrawerTab(btn,panelId){ document.querySelectorAll('.drawer-tab').forEach(function(t){t.classList.remove('active');}); document.querySelectorAll('.drawer-tab-panel').forEach(function(p){p.classList.remove('active');}); btn.classList.add('active'); var panel=document.getElementById(panelId); if(panel)panel.classList.add('active'); } /* ═══════════════════════════════════════════════════════════════ Volume Detail Drawer ═══════════════════════════════════════════════════════════════ */ function openVolumeDrawer(name){ _drawerType='volume'; var drawer=document.getElementById('detailDrawer'),backdrop=document.getElementById('drawerBackdrop'); document.getElementById('drawerTitle').textContent='Volume: '+name; document.getElementById('drawerStatusDot').className=''; document.getElementById('drawerStatusDot').style.display='none'; document.getElementById('drawerTabsWrap').innerHTML=''; document.getElementById('drawerBody').innerHTML='


Loading volume details...
'; document.getElementById('drawerActions').innerHTML=''; drawer.classList.add('open');backdrop.classList.add('open');document.body.style.overflow='hidden'; postAPI('/containers/volume-inspect/',{name:name}).then(function(res){ if(!res.success){ document.getElementById('drawerBody').innerHTML='
'+escHtml(res.error||'Inspect failed')+'
'; return; } var d=res.data; var html='
Volume Details
'+ '
Name
'+escHtml(d.Name||'')+'
'+ '
Driver
'+escHtml(d.Driver||'')+'
'+ '
Mountpoint
'+escHtml(d.Mountpoint||'')+'
'+ '
Scope
'+escHtml(d.Scope||'')+'
'+ '
Created
'+(d.CreatedAt||'').substring(0,19)+'
'; /* Labels */ var labels=d.Labels||{}; var lk=Object.keys(labels); if(lk.length){ html+='
Labels
'; lk.forEach(function(k){html+='
'+escHtml(k)+'
'+escHtml(labels[k])+'
';}); html+='
'; } /* Options */ var opts=d.Options||{}; var ok2=Object.keys(opts); if(ok2.length){ html+='
Options
'; ok2.forEach(function(k){html+='
'+escHtml(k)+'
'+escHtml(opts[k])+'
';}); html+='
'; } /* Raw JSON */ html+='
Raw Inspect
'+jsonToHtml(d)+'
'; document.getElementById('drawerBody').innerHTML=html; }).catch(function(e){ document.getElementById('drawerBody').innerHTML='
Error: '+escHtml(e.message)+'
'; }); } /* ═══════════════════════════════════════════════════════════════ Network Detail Drawer ═══════════════════════════════════════════════════════════════ */ function openNetworkDrawer(id,name){ _drawerType='network'; var drawer=document.getElementById('detailDrawer'),backdrop=document.getElementById('drawerBackdrop'); document.getElementById('drawerTitle').textContent='Network: '+name; document.getElementById('drawerStatusDot').className=''; document.getElementById('drawerStatusDot').style.display='none'; document.getElementById('drawerTabsWrap').innerHTML=''; document.getElementById('drawerBody').innerHTML='


Loading network details...
'; var isDefault=['bridge','host','none'].indexOf(name)!==-1; document.getElementById('drawerActions').innerHTML=isDefault?'':''; drawer.classList.add('open');backdrop.classList.add('open');document.body.style.overflow='hidden'; postAPI('/containers/network-inspect/',{network_id:id}).then(function(res){ if(!res.success){ document.getElementById('drawerBody').innerHTML='
'+escHtml(res.error||'Inspect failed')+'
'; return; } var d=res.data; var ipam=d.IPAM||{}; var ipamCfg=(ipam.Config||[])[0]||{}; var containers=d.Containers||{}; var ck=Object.keys(containers); var html='
Network Details
'+ '
Name
'+escHtml(d.Name||'')+'
'+ '
ID
'+(d.Id||'').substring(0,12)+'
'+ '
Driver
'+escHtml(d.Driver||'')+'
'+ '
Scope
'+escHtml(d.Scope||'')+'
'+ '
Internal
'+(d.Internal?'Yes':'No')+'
'+ '
Attachable
'+(d.Attachable?'Yes':'No')+'
'+ '
Subnet
'+escHtml(ipamCfg.Subnet||'—')+'
'+ '
Gateway
'+escHtml(ipamCfg.Gateway||'—')+'
'+ '
IP Range
'+escHtml(ipamCfg.IPRange||'—')+'
'+ '
Created
'+(d.Created||'').substring(0,19)+'
'; /* Connected Containers */ if(ck.length){ html+='
Connected Containers ('+ck.length+')
'; ck.forEach(function(cid){ var ci=containers[cid]; html+='
'+escHtml(ci.Name||cid.substring(0,12))+'
'+ '
IPv4
'+(ci.IPv4Address||'—')+'
'+ '
IPv6
'+(ci.IPv6Address||'—')+'
'+ '
MAC
'+(ci.MacAddress||'—')+'
'; }); html+='
'; } /* Raw JSON */ html+='
Raw Inspect
'+jsonToHtml(d)+'
'; document.getElementById('drawerBody').innerHTML=html; }).catch(function(e){ document.getElementById('drawerBody').innerHTML='
Error: '+escHtml(e.message)+'
'; }); } /* ═══════════════════════════════════════════════════════════════ Image Detail Drawer ═══════════════════════════════════════════════════════════════ */ function openImageDrawer(id){ _drawerType='image'; var drawer=document.getElementById('detailDrawer'),backdrop=document.getElementById('drawerBackdrop'); document.getElementById('drawerTitle').textContent='Image: '+id.substring(0,12); document.getElementById('drawerStatusDot').className=''; document.getElementById('drawerStatusDot').style.display='none'; document.getElementById('drawerTabsWrap').innerHTML= '
'+ ''+ ''+ ''+ '
'; document.getElementById('drawerBody').innerHTML='


Loading image details...
'; document.getElementById('drawerActions').innerHTML= ''+ ''; drawer.classList.add('open');backdrop.classList.add('open');document.body.style.overflow='hidden'; postAPI('/containers/image-inspect/',{image_id:id}).then(function(res){ if(!res.success){ document.getElementById('drawerBody').innerHTML='
'+escHtml(res.error||'Inspect failed')+'
'; return; } var d=res.data; var conf=d.Config||{}; var tags=d.RepoTags||[]; var html='
Image Details
'+ '
ID
'+(d.Id||'').substring(0,19)+'
'+ '
Tags
'+tags.map(function(t){return ''+escHtml(t)+'';}).join(' ')+'
'+ '
Size
'+formatBytes(d.Size||0)+'
'+ '
Virtual Size
'+formatBytes(d.VirtualSize||d.Size||0)+'
'+ '
Created
'+(d.Created||'').substring(0,19)+'
'+ '
Architecture
'+escHtml(d.Architecture||'')+'
'+ '
OS
'+escHtml(d.Os||'')+'
'+ '
Docker Version
'+escHtml(d.DockerVersion||'—')+'
'+ '
Entrypoint
'+escHtml((conf.Entrypoint||[]).join(' ')||'—')+'
'+ '
Cmd
'+escHtml((conf.Cmd||[]).join(' ')||'—')+'
'+ '
Working Dir
'+escHtml(conf.WorkingDir||'/')+'
'+ '
'; /* Exposed ports */ var ep=conf.ExposedPorts||{}; var epk=Object.keys(ep); if(epk.length){ html+='
Exposed Ports
'+epk.map(function(p){return ''+p+'';}).join(' ')+'
'; } /* Environment */ var envs=conf.Env||[]; if(envs.length){ html+='
Environment ('+envs.length+')
'+ '
Show env vars'+ ''; envs.forEach(function(e){var p=e.split('=');html+='';}); html+='
VariableValue
'+escHtml(p[0])+''+escHtml(p.slice(1).join('='))+'
'; } /* Layers */ var layers=d.RootFS; if(layers&&layers.Layers){ html+='
Layers ('+layers.Layers.length+')
'+ '
Show layers'+ '
'; layers.Layers.forEach(function(l,i){html+='
'+(i+1)+'. '+l.substring(0,50)+'…
';}); html+='
'; } /* Close dt-imgdetails panel */ html+='
'; /* History / Layers tab panel */ html+='
'; html+='
Image History / Layers
'; html+='

Loading image history...
'; html+='
'; /* Inspect (Raw JSON) tab panel */ html+='
'; html+='
Raw Inspect
'+jsonToHtml(d)+'
'; html+='
'; document.getElementById('drawerBody').innerHTML=html; /* Load image history */ loadImageHistory(id); }).catch(function(e){ document.getElementById('drawerBody').innerHTML='
Error: '+escHtml(e.message)+'
'; }); } /* ═══════════════════════════════════════════════════════════════ Drawer close / escape ═══════════════════════════════════════════════════════════════ */ function closeDrawer(){ document.getElementById('detailDrawer').classList.remove('open'); document.getElementById('drawerBackdrop').classList.remove('open'); document.body.style.overflow=''; /* Stop live log polling for drawer */ if(_drawerLogTimer){clearInterval(_drawerLogTimer);_drawerLogTimer=null;} /* Reset status dot display */ var dot=document.getElementById('drawerStatusDot'); if(dot)dot.style.display=''; } document.addEventListener('keydown',function(e){if(e.key==='Escape')closeDrawer();}); /* ═══════════════════════════════════════════════════════════════ Logs viewer (real docker logs) ═══════════════════════════════════════════════════════════════ */ var _logTimer=null,_logCid='',_logLastTs='',_logPaused=false,_logLineCount=0; function loadContainerLogs(cid){ if(_logTimer){clearInterval(_logTimer);_logTimer=null;} _logCid=cid;_logLastTs='';_logPaused=false;_logLineCount=0; var body=document.getElementById('logsPanelBody'),nm=document.getElementById('logContainerName'); if(!cid){body.innerHTML='

Select a container.

';nm.textContent='No container selected';_updateLiveIndicator(false);return;} /* Find container name */ var row=document.querySelector('tr[data-cid="'+cid+'"]'); nm.textContent=row?row.getAttribute('data-cname'):cid.substring(0,12); body.innerHTML='Connecting...'; _updateLiveIndicator(true); /* Initial fetch: full tail */ _fetchLogsIncremental(cid,true); /* Live stream: poll every 2s for new lines only */ _logTimer=setInterval(function(){ if(!_logPaused)_fetchLogsIncremental(cid,false); },2000); } function _fetchLogsIncremental(cid,isInitial){ var tailSel=document.getElementById('logTailCount'); var tail=isInitial?(tailSel?tailSel.value:'200'):'500'; var params={container_id:cid,tail:tail}; /* For incremental fetches, only get new lines since last timestamp */ if(!isInitial&&_logLastTs)params.since=_logLastTs; postAPI('/containers/logs/',params).then(function(res){ var body=document.getElementById('logsPanelBody'); if(!body)return; if(res.success&&res.logs){ var lines=res.logs.split('\n'); var html='',newTs=_logLastTs; var frag=[]; lines.forEach(function(l){ if(!l.trim())return; var cls='log-stdout'; if(l.match(/error|Error|ERROR|fatal|Fatal|FATAL|panic|exception/i))cls='log-stderr'; else if(l.match(/warn|Warn|WARN/i))cls='log-warn'; else if(l.match(/info|Info|INFO/i))cls='log-info'; var ts='',msg=l; var tsMatch=l.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s*(.*)/); if(tsMatch){ ts=tsMatch[1].substring(11,19); msg=tsMatch[2]; /* Track latest timestamp for incremental fetches */ if(tsMatch[1]>newTs)newTs=tsMatch[1]; } var line='
'; if(ts)line+='['+ts+']'; line+=''+escHtml(msg)+'
'; frag.push(line); }); if(frag.length>0){ if(isInitial){ body.innerHTML=frag.join(''); _logLineCount=frag.length; } else { /* Append new lines only (incremental) */ body.insertAdjacentHTML('beforeend',frag.join('')); _logLineCount+=frag.length; /* Cap at 5000 lines to prevent memory bloat */ while(_logLineCount>5000&&body.firstChild){body.removeChild(body.firstChild);_logLineCount--;} } _logLastTs=newTs; if(document.getElementById('logFollow').checked)body.scrollTop=body.scrollHeight; } else if(isInitial){ body.innerHTML='No logs yet — waiting for output...'; } } else if(isInitial){ body.innerHTML=''+(res.error||'No logs available')+''; } }).catch(function(){}); } function _updateLiveIndicator(on){ var el=document.getElementById('logLiveIndicator'); if(!el)return; el.style.display=on?'inline-flex':'none'; } function toggleLogPause(){ _logPaused=!_logPaused; var btn=document.getElementById('logPauseBtn'); if(btn){ btn.innerHTML=_logPaused?' Resume':' Pause'; btn.title=_logPaused?'Resume live streaming':'Pause live streaming'; } _updateLiveIndicator(!_logPaused&&!!_logCid); } function refreshLogs(){ if(_logCid){ _logLastTs='';_logLineCount=0; document.getElementById('logsPanelBody').innerHTML=''; _fetchLogsIncremental(_logCid,true); } } function toggleLogWrap(){ var body=document.getElementById('logsPanelBody'); body.style.whiteSpace=document.getElementById('logWrap').checked?'pre-wrap':'pre'; } function copyLogs(){var t=document.getElementById('logsPanelBody').innerText;navigator.clipboard.writeText(t).then(function(){if(typeof showToast==='function')showToast('Logs copied to clipboard','success');});} function downloadLogs(){var t=document.getElementById('logsPanelBody').innerText,b=new Blob([t],{type:'text/plain'}),a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='logs-'+(_logCid||'container').substring(0,12)+'-'+new Date().toISOString().replace(/[:.]/g,'-')+'.txt';a.click();} function clearLogs(){document.getElementById('logsPanelBody').innerHTML='

Cleared.

';} function refreshContainers(){location.reload();} /* ═══════════════════════════════════════════════════════════════ Live Table Refresh (updates containers without page reload) ═══════════════════════════════════════════════════════════════ */ var _autoRefreshEnabled=true; function toggleAutoRefresh(on){_autoRefreshEnabled=on;} function refreshContainersLive(){ fetch(_API+'/containers/api/').then(function(r){return r.json();}).then(function(data){ _rebuildContainerTable(data.containers||[]); _updateLogContainerSelect(data.containers||[]); /* Update stat counters */ var si=data.system_info||{}; var el; el=document.getElementById('stat-running');if(el)el.textContent=si.running||0; el=document.getElementById('stat-stopped');if(el)el.textContent=si.stopped||0; el=document.getElementById('stat-paused');if(el)el.textContent=si.paused||0; el=document.getElementById('stat-images');if(el)el.textContent=(data.images||[]).length; el=document.getElementById('stat-volumes');if(el)el.textContent=(data.volumes||[]).length; el=document.getElementById('stat-networks');if(el)el.textContent=(data.networks||[]).length; }).catch(function(){}); } function _statusClass(state){ var s=state.toLowerCase(); if(s==='running')return{cls:'running',icon:'▶'}; if(s==='exited')return{cls:'stopped',icon:'■'}; if(s==='paused')return{cls:'paused',icon:'⏸'}; if(s==='restarting'||s==='removing')return{cls:'warning',icon:'↻'}; if(s==='created')return{cls:'created',icon:'○'}; if(s==='dead')return{cls:'dead',icon:'✕'}; return{cls:'unknown',icon:'?'}; } function _containerActionBtns(cid,state){ var s=state.toLowerCase(),h=''; if(s==='running'){ h+=''; h+=''; h+=''; h+=''; } else if(s==='exited'||s==='created'||s==='dead'){ h+=''; h+=''; } else if(s==='paused'){ h+=''; h+=''; } else if(s==='restarting'){ h+=''; h+=''; } return h; } function _rebuildContainerTable(containers){ var tbody=document.getElementById('containersTableBody'); if(!tbody)return; if(!containers.length){ tbody.innerHTML='
📦
No containers found.'; return; } var html=''; containers.forEach(function(c){ var sc=_statusClass(c.state||'unknown'); var cid=c.id||''; var ports=c.ports||''; var portsHtml=''; if(ports){ portsHtml=ports.split(', ').filter(function(p){return p;}).map(function(p){return ''+escHtml(p)+'';}).join(''); } var cpuHtml=''; var memHtml=''; var netHtml=''; if(c.stats){ var cpuV=parseFloat((c.stats.cpu||'0').replace('%',''))||0; var cpuCls=cpuV<50?'green':cpuV<80?'amber':'red'; cpuHtml=''+escHtml(c.stats.cpu)+'
'; var memV=parseFloat((c.stats.mem_perc||'0').replace('%',''))||0; var memCls=memV<60?'green':memV<85?'amber':'red'; memHtml=''+escHtml(c.stats.memory)+'
'; netHtml=escHtml(c.stats.net_io||''); } var uptime=escHtml(c.running_for||''); if(!uptime&&c.created)uptime=escHtml(c.created.substring(0,19)); if(!uptime)uptime='—'; html+=''; html+=''; html+='
'+escHtml(c.name||cid.substring(0,12))+'
'; html+=''+escHtml(c.image||'')+''; html+=''+sc.icon+' '+escHtml(c.state||'')+''; html+=''+portsHtml+''; html+=''+cpuHtml+''; html+=''+memHtml+''; html+=''+netHtml+''; html+=''+uptime+''; html+='
'+_containerActionBtns(cid,c.state||'')+'
'; html+=''; }); tbody.innerHTML=html; /* Re-apply filter if active */ var q=document.getElementById('containerSearch'); if(q&&q.value)filterContainers(q.value); } function _updateLogContainerSelect(containers){ var sel=document.getElementById('logContainerSelect'); if(!sel)return; var cur=sel.value; sel.innerHTML=''; containers.forEach(function(c){ var opt=document.createElement('option'); opt.value=c.id||''; opt.textContent=(c.name||c.id.substring(0,12))+' ('+c.state+')'; if(opt.value===cur)opt.selected=true; sel.appendChild(opt); }); } /* ═══════════════════════════════════════════════════════════════ Run Container Dialog ═══════════════════════════════════════════════════════════════ */ function runContainerDialog(){ var bd=document.getElementById('runContainerBackdrop'),dg=document.getElementById('runContainerDialog'); bd.style.opacity='1';bd.style.pointerEvents='auto'; dg.style.opacity='1';dg.style.pointerEvents='auto';dg.style.transform='translate(-50%,-50%) scale(1)'; document.getElementById('runImage').focus(); } function closeRunDialog(){ var bd=document.getElementById('runContainerBackdrop'),dg=document.getElementById('runContainerDialog'); bd.style.opacity='0';bd.style.pointerEvents='none'; dg.style.opacity='0';dg.style.pointerEvents='none';dg.style.transform='translate(-50%,-50%) scale(0.95)'; document.getElementById('runOutput').style.display='none'; } function executeRunContainer(){ var img=document.getElementById('runImage').value.trim(); if(!img){if(typeof showToast==='function')showToast('Image name is required','error');return;} var params={image:img}; var name=document.getElementById('runName').value.trim(); if(name)params.name=name; var hp=document.getElementById('runHostPort').value.trim(); var cp=document.getElementById('runContPort').value.trim(); if(hp&&cp)params.ports=hp+':'+cp; else if(cp)params.ports=cp+':'+cp; var envs=document.getElementById('runEnvVars').value.trim(); if(envs)params.env_vars=envs; var flags=document.getElementById('runFlags').value.trim(); if(flags)params.extra_flags=flags; if(document.getElementById('runDetach').checked)params.detach='1'; if(document.getElementById('runRemove').checked)params.auto_remove='1'; var btn=document.getElementById('runBtn'); btn.disabled=true;btn.innerHTML=' Running...'; postAPI('/containers/action/',{container_id:'__run__',action:'run',run_params:JSON.stringify(params)}).then(function(res){ btn.disabled=false;btn.innerHTML=' Run Container'; var out=document.getElementById('runOutput'),outBody=document.getElementById('runOutputBody'); if(res.success){ if(typeof showToast==='function')showToast('Container started successfully','success'); out.style.display='block'; outBody.innerHTML=''+escHtml(res.message||'Container started')+''; setTimeout(function(){closeRunDialog();refreshContainersLive();},2000); } else { out.style.display='block'; outBody.innerHTML=''+escHtml(res.error||'Failed to start container')+''; } }).catch(function(e){ btn.disabled=false;btn.innerHTML=' Run Container'; if(typeof showToast==='function')showToast('Failed: '+e.message,'error'); }); } /* ═══════════════════════════════════════════════════════════════ Docker Build ═══════════════════════════════════════════════════════════════ */ function executeBuild(){ var tag=document.getElementById('buildTag').value.trim(); if(!tag){if(typeof showToast==='function')showToast('Image tag is required','error');return;} var target=document.getElementById('buildTarget').value.trim(); var argsRaw=document.getElementById('buildArgs').value.trim(); var noCache=document.getElementById('buildNoCache').checked; var params={tag:tag,no_cache:noCache?'1':'0'}; if(target)params.target=target; if(argsRaw)params.build_args=argsRaw; var outEl=document.getElementById('buildOutput'); outEl.style.display='block'; outEl.innerHTML='
Building image: '+escHtml(tag)+'...
'; postAPI('/containers/build/',params).then(function(res){ if(res.success){ outEl.innerHTML='
✓ Build completed successfully
'+escHtml(res.output||'')+'
'; if(typeof showToast==='function')showToast('Image built successfully','success'); setTimeout(refreshContainersLive,1500); } else { outEl.innerHTML='
✗ Build failed
'+escHtml(res.error||'Build failed')+'
'; } }).catch(function(e){ outEl.innerHTML='
✗ Error: '+escHtml(e.message)+'
'; }); } /* ═══════════════════════════════════════════════════════════════ Docker System / Disk Usage ═══════════════════════════════════════════════════════════════ */ function loadDiskUsage(){ var el=document.getElementById('diskUsageContent'); if(!el)return; el.innerHTML='

Analyzing disk usage...
'; postAPI('/containers/disk-usage/',{}).then(function(res){ if(!res.success){el.innerHTML='
'+escHtml(res.error||'Failed')+'
';return;} /* Parse the text table output from docker system df */ var output=res.output||''; if(!output){el.innerHTML='
No disk usage data available.
';return;} var lines=output.split('\n'); var html='
Docker Disk Usage
'; html+=''; /* Parse header row */ if(lines.length>0){ html+=''; var headers=lines[0].split(/\s{2,}/); headers.forEach(function(h){html+='';}); html+=''; } /* Parse data rows */ for(var i=1;i50)style='color:#f59e0b;font-weight:600;'; if(pct>80)style='color:#f85149;font-weight:600;'; } html+=''; }); html+=''; } html+='
'+escHtml(h.trim())+'
'+escHtml(c.trim())+'
'; /* Add visual bars for each type */ var typeMatches=output.match(/^(\w[\w\s]*?)\s{2,}(\d+)\s{2,}(\d+)\s{2,}([\d.]+\w+)\s{2,}([\d.]+\w+\s*\(\d+%\))/gm); if(typeMatches&&typeMatches.length){ html+='
Space Overview
'; html+='
'; typeMatches.forEach(function(m){ var parts=m.split(/\s{2,}/); var type=parts[0]||''; var total=parts[1]||'0'; var active=parts[2]||'0'; var size=parts[3]||'0B'; var reclaim=parts[4]||''; var pctMatch=reclaim.match(/(\d+)%/); var pct=pctMatch?parseInt(pctMatch[1]):0; html+='
'; html+='
'+escHtml(type)+'
'; html+='
Total: '+escHtml(total)+' • Active: '+escHtml(active)+'
'; html+='
Size: '+escHtml(size)+'
'; html+='
'; html+='
Reclaimable: '+escHtml(reclaim)+'
'; html+='
'; }); html+='
'; } el.innerHTML=html; }).catch(function(e){ el.innerHTML='
Error: '+escHtml(e.message)+'
'; }); } function dockerPrune(target){ var labels={system:'entire system',images:'unused images',containers:'stopped containers',volumes:'unused volumes',builder:'build cache'}; showConfirm('Prune '+labels[target]+'?','This will permanently remove all '+(labels[target]||target)+'. This action cannot be undone.',function(){ var outEl=document.getElementById('pruneOutput'); outEl.style.display='block'; outEl.innerHTML='
Pruning '+escHtml(target)+'...'; postAPI('/containers/prune/',{target:target}).then(function(res){ if(res.success){ outEl.innerHTML='
✓ Prune completed
'+escHtml(res.output||'Done')+'
'; if(typeof showToast==='function')showToast('Prune completed','success'); setTimeout(function(){loadDiskUsage();refreshContainersLive();},1500); } else { outEl.innerHTML='
'+escHtml(res.error||'Prune failed')+'
'; } }).catch(function(e){ outEl.innerHTML='
Error: '+escHtml(e.message)+'
'; }); }); } /* ═══════════════════════════════════════════════════════════════ Container Exec Shell ═══════════════════════════════════════════════════════════════ */ function runExecCommand(cid,cmd){ if(!cid||!cmd)return; var outEl=document.getElementById('execOutput'); if(!outEl)return; /* Append command prompt */ outEl.innerHTML+='
$ '+escHtml(cmd)+'
'; outEl.innerHTML+='
'; outEl.scrollTop=outEl.scrollHeight; postAPI('/containers/exec/',{container_id:cid,command:cmd}).then(function(res){ var spinner=document.getElementById('execSpinner'); if(spinner)spinner.remove(); if(res.success){ outEl.innerHTML+='
'+escHtml(res.output||'(no output)')+'
'; } else { outEl.innerHTML+='
'+escHtml(res.error||'Execution failed')+'
'; } outEl.scrollTop=outEl.scrollHeight; }).catch(function(e){ var spinner=document.getElementById('execSpinner'); if(spinner)spinner.remove(); outEl.innerHTML+='
Error: '+escHtml(e.message)+'
'; }); } function quickExec(cid,cmd){ runExecCommand(cid,cmd); } /* ═══════════════════════════════════════════════════════════════ Container Processes (docker top) ═══════════════════════════════════════════════════════════════ */ function loadContainerProcesses(cid){ var el=document.getElementById('processesContent'); if(!el)return; el.innerHTML='

Loading processes...
'; postAPI('/containers/top/',{container_id:cid}).then(function(res){ if(!res.success){el.innerHTML='
'+escHtml(res.error||'Failed to get processes. Container may not be running.')+'
';return;} var d=res.data||{}; var titles=d.Titles||[]; var procs=d.Processes||[]; if(!procs.length){el.innerHTML='
No processes found. Container may not be running.
';return;} var html=''; titles.forEach(function(t){html+='';}); html+=''; procs.forEach(function(p){ html+=''; p.forEach(function(v){html+='';}); html+=''; }); html+='
'+escHtml(t)+'
'+escHtml(v)+'
'; el.innerHTML=html; }).catch(function(e){ el.innerHTML='
Error: '+escHtml(e.message)+'
'; }); } /* ═══════════════════════════════════════════════════════════════ Container Filesystem Changes (docker diff) ═══════════════════════════════════════════════════════════════ */ function loadContainerDiff(cid){ var el=document.getElementById('diffContent'); if(!el)return; el.innerHTML='

Loading filesystem changes...
'; postAPI('/containers/diff/',{container_id:cid}).then(function(res){ if(!res.success){el.innerHTML='
'+escHtml(res.error||'Failed')+'
';return;} var changes=res.changes||[]; if(!changes.length){el.innerHTML='
No filesystem changes detected.
';return;} var html='
'; changes.forEach(function(c){ var kind=c.kind||'C'; var label=kind==='A'?'Added':kind==='D'?'Deleted':'Changed'; html+='
'+kind+''+escHtml(c.path||'')+'
'; }); html+='
'; el.innerHTML=html; }).catch(function(e){ el.innerHTML='
Error: '+escHtml(e.message)+'
'; }); } /* ═══════════════════════════════════════════════════════════════ Docker Events Stream ═══════════════════════════════════════════════════════════════ */ function loadDockerEvents(){ var el=document.getElementById('eventFeed'); if(!el)return; var sinceEl=document.getElementById('eventSince'); var since=sinceEl?sinceEl.value:'5m'; el.innerHTML='

Loading events...
'; postAPI('/containers/events/',{since:since}).then(function(res){ if(!res.success){el.innerHTML='
'+escHtml(res.error||'Failed to load events')+'
';return;} var events=res.events||[]; if(!events.length){el.innerHTML='
No events in the selected time range.
';return;} var html=''; events.forEach(function(ev){ var action=ev.Action||ev.status||'unknown'; var type=ev.Type||'container'; var actor=(ev.Actor&&ev.Actor.Attributes)?ev.Actor.Attributes:{}; var name=actor.name||actor.image||(ev.Actor&&ev.Actor.ID?ev.Actor.ID.substring(0,12):'')||''; var time=ev.time?new Date(ev.time*1000).toLocaleTimeString():(ev.timeNano?new Date(ev.timeNano/1000000).toLocaleTimeString():''); var iconClass='default'; if(action==='create')iconClass='create'; else if(action==='start')iconClass='start'; else if(action==='stop')iconClass='stop'; else if(action==='die')iconClass='die'; else if(action==='kill')iconClass='kill'; else if(action==='destroy')iconClass='destroy'; else if(action==='pull')iconClass='pull'; html+='
'; html+='
'; if(action==='create')html+='➕'; else if(action==='start')html+='▶'; else if(action==='stop')html+='⏹'; else if(action==='die')html+='💀'; else if(action==='kill')html+='⚡'; else if(action==='destroy')html+='🗑'; else if(action==='pull')html+='⬇'; else html+='📋'; html+='
'; html+='
'; html+='
'+escHtml(type)+':'+escHtml(action)+'
'; html+='
'+escHtml(name)+'
'; html+='
'; html+='
'+escHtml(time)+'
'; html+='
'; }); el.innerHTML=html; }).catch(function(e){ el.innerHTML='
Error: '+escHtml(e.message)+'
'; }); } /* ═══════════════════════════════════════════════════════════════ Container Export ═══════════════════════════════════════════════════════════════ */ function exportContainer(cid){ showConfirm('Export Container?','This will export the container filesystem as a tar archive to the server\'s /tmp directory.',function(){ if(typeof showToast==='function')showToast('Exporting container...','info'); postAPI('/containers/export/',{container_id:cid}).then(function(res){ if(res.success){ if(typeof showToast==='function')showToast('Container exported: '+(res.path||'/tmp/docker-export-*.tar'),'success'); } else { if(typeof showToast==='function')showToast(res.error||'Export failed','error'); } }).catch(function(e){ if(typeof showToast==='function')showToast('Export error: '+e.message,'error'); }); }); } /* ═══════════════════════════════════════════════════════════════ Image Tag ═══════════════════════════════════════════════════════════════ */ function promptTagImage(){ var source=prompt('Source image (e.g., myimage:latest):'); if(!source)return; var target=prompt('New tag (e.g., myregistry/myimage:v2):'); if(!target)return; postAPI('/containers/image-tag/',{source:source,target:target}).then(function(res){ if(res.success){ if(typeof showToast==='function')showToast('Image tagged successfully','success'); setTimeout(refreshContainersLive,1000); } else { if(typeof showToast==='function')showToast(res.error||'Tag failed','error'); } }).catch(function(e){ if(typeof showToast==='function')showToast('Error: '+e.message,'error'); }); } function promptTagImageWith(id){ var target=prompt('New tag for '+id.substring(0,12)+' (e.g., myregistry/myimage:v2):'); if(!target)return; postAPI('/containers/image-tag/',{source:id,target:target}).then(function(res){ if(res.success){ if(typeof showToast==='function')showToast('Image tagged as '+target,'success'); setTimeout(refreshContainersLive,1000); } else { if(typeof showToast==='function')showToast(res.error||'Tag failed','error'); } }).catch(function(e){ if(typeof showToast==='function')showToast('Error: '+e.message,'error'); }); } /* ═══════════════════════════════════════════════════════════════ Create Network Dialog ═══════════════════════════════════════════════════════════════ */ function openCreateNetworkDialog(){ var bd=document.getElementById('createNetworkBackdrop'),dg=document.getElementById('createNetworkDialog'); bd.style.opacity='1';bd.style.pointerEvents='auto'; dg.style.opacity='1';dg.style.pointerEvents='auto';dg.style.transform='translate(-50%,-50%) scale(1)'; document.getElementById('netName').focus(); } function closeCreateNetworkDialog(){ var bd=document.getElementById('createNetworkBackdrop'),dg=document.getElementById('createNetworkDialog'); bd.style.opacity='0';bd.style.pointerEvents='none'; dg.style.opacity='0';dg.style.pointerEvents='none';dg.style.transform='translate(-50%,-50%) scale(0.95)'; } function executeCreateNetwork(){ var name=document.getElementById('netName').value.trim(); if(!name){if(typeof showToast==='function')showToast('Network name is required','error');return;} var driver=document.getElementById('netDriver').value; var subnet=document.getElementById('netSubnet').value.trim(); var gateway=document.getElementById('netGateway').value.trim(); var internal=document.getElementById('netInternal').checked; var params={name:name,driver:driver,internal:internal?'1':'0'}; if(subnet)params.subnet=subnet; if(gateway)params.gateway=gateway; postAPI('/containers/create-network/',params).then(function(res){ if(res.success){ if(typeof showToast==='function')showToast('Network "'+name+'" created','success'); closeCreateNetworkDialog(); setTimeout(refreshContainersLive,1000); } else { if(typeof showToast==='function')showToast(res.error||'Failed to create network','error'); } }).catch(function(e){ if(typeof showToast==='function')showToast('Error: '+e.message,'error'); }); } /* ═══════════════════════════════════════════════════════════════ Create Volume Dialog ═══════════════════════════════════════════════════════════════ */ function openCreateVolumeDialog(){ var bd=document.getElementById('createVolumeBackdrop'),dg=document.getElementById('createVolumeDialog'); bd.style.opacity='1';bd.style.pointerEvents='auto'; dg.style.opacity='1';dg.style.pointerEvents='auto';dg.style.transform='translate(-50%,-50%) scale(1)'; document.getElementById('volName').focus(); } function closeCreateVolumeDialog(){ var bd=document.getElementById('createVolumeBackdrop'),dg=document.getElementById('createVolumeDialog'); bd.style.opacity='0';bd.style.pointerEvents='none'; dg.style.opacity='0';dg.style.pointerEvents='none';dg.style.transform='translate(-50%,-50%) scale(0.95)'; } function executeCreateVolume(){ var name=document.getElementById('volName').value.trim(); if(!name){if(typeof showToast==='function')showToast('Volume name is required','error');return;} var driver=document.getElementById('volDriver').value.trim()||'local'; var labels=document.getElementById('volLabels').value.trim(); var params={name:name,driver:driver}; if(labels)params.labels=labels; postAPI('/containers/create-volume/',params).then(function(res){ if(res.success){ if(typeof showToast==='function')showToast('Volume "'+name+'" created','success'); closeCreateVolumeDialog(); setTimeout(refreshContainersLive,1000); } else { if(typeof showToast==='function')showToast(res.error||'Failed to create volume','error'); } }).catch(function(e){ if(typeof showToast==='function')showToast('Error: '+e.message,'error'); }); } /* ═══════════════════════════════════════════════════════════════ Image History / Layers ═══════════════════════════════════════════════════════════════ */ function loadImageHistory(imageId){ var el=document.getElementById('imageHistoryContent'); if(!el)return; el.innerHTML='

Loading history...
'; postAPI('/containers/image-history/',{image_id:imageId}).then(function(res){ if(!res.success){el.innerHTML='
'+escHtml(res.error||'Failed')+'
';return;} var layers=res.layers||[]; if(!layers.length){el.innerHTML='
No history available.
';return;} var html='
'; layers.forEach(function(l,i){ var cmd=l.CreatedBy||l.created_by||''; var size=l.Size||l.size||0; /* Shorten common Dockerfile prefixes */ cmd=cmd.replace(/^\/bin\/sh -c #\(nop\)\s*/,'').replace(/^\/bin\/sh -c\s*/,'RUN '); html+='
'; html+='#'+(i+1)+''; html+=''+escHtml(cmd.length>100?cmd.substring(0,100)+'…':cmd)+''; if(size>0)html+=''+formatBytes(size)+''; html+='
'; }); html+='
'; el.innerHTML=html; }).catch(function(e){ el.innerHTML='
Error: '+escHtml(e.message)+'
'; }); } /* ═══════════════════════════════════════════════════════════════ Utilities ═══════════════════════════════════════════════════════════════ */ function escHtml(s){if(!s)return'';return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');} function formatBytes(b){if(!b)return '0 B';var u=['B','KB','MB','GB','TB'];var i=0;while(b>=1024&&i0?'degraded':'down');} })(); function renderCharts(){ _chartDefaults(); /* CPU Chart */ var cpuEl=document.getElementById('cpuChart'); if(cpuEl){ var datasets=[],ci=0; for(var n in cpuH){ datasets.push({ label:n, data:cpuH[n].slice(), borderColor:_chartColors[ci%_chartColors.length], backgroundColor:_chartBgColors[ci%_chartBgColors.length], borderWidth:2, fill:true, tension:0.4, pointRadius:0, pointHoverRadius:4 }); ci++; } if(_cpuChart){ _cpuChart.data.labels=hLabels.slice(); _cpuChart.data.datasets.forEach(function(ds,i){if(datasets[i])ds.data=datasets[i].data;}); if(datasets.length>_cpuChart.data.datasets.length){ for(var x=_cpuChart.data.datasets.length;x_memChart.data.datasets.length){ for(var x=_memChart.data.datasets.length;x1024?(v/1024).toFixed(1)+'KB':v+'B';}}},x:{ticks:{maxTicksLimit:6}}}, plugins:{legend:{display:true,position:'bottom',labels:{boxWidth:12,padding:12,usePointStyle:true}},tooltip:{callbacks:{label:function(ctx){var v=ctx.parsed.y;return ctx.dataset.label+': '+(v>1024?(v/1024).toFixed(1)+' KB':v.toFixed(0)+' B');}}}} }}); } } /* State Distribution (Doughnut) */ var stEl=document.getElementById('stateChart'); if(stEl){ var r=parseInt(document.getElementById('stat-running').textContent)||0; var s=parseInt(document.getElementById('stat-stopped').textContent)||0; var p=parseInt(document.getElementById('stat-paused').textContent)||0; var total=r+s+p; if(_stateChart){ _stateChart.data.datasets[0].data=[r,s,p]; _stateChart.options.plugins.centerText.text=String(total); _stateChart.update('none'); } else { /* Center text plugin */ var centerPlugin={id:'centerText',afterDraw:function(chart){ var txt=chart.options.plugins.centerText&&chart.options.plugins.centerText.text; if(!txt)return; var ctx=chart.ctx,w=chart.width,h=chart.height; ctx.save();ctx.font='bold 20px sans-serif';ctx.fillStyle=_isDark()?'rgba(255,255,255,0.7)':'rgba(0,0,0,0.7)'; ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(txt,w/2,h/2);ctx.restore(); }}; _stateChart=new Chart(stEl,{type:'doughnut',data:{ labels:['Running','Stopped','Paused'], datasets:[{data:[r,s,p],backgroundColor:['#22c55e','#ef4444','#f59e0b'],borderWidth:0,spacing:2}] },options:{ cutout:'65%', plugins:{centerText:{text:String(total)},legend:{display:true,position:'bottom',labels:{boxWidth:10,padding:10,usePointStyle:true}}, tooltip:{callbacks:{label:function(ctx){return ctx.label+': '+ctx.parsed;}}}} },plugins:[centerPlugin]}); } } renderHealthTimeline(); } function renderHealthTimeline(){ var el=document.getElementById('healthTimeline');if(!el)return; el.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); el.appendChild(b); }); } function renderImageSizeChart(){ var cv=document.getElementById('imageSizeChart');if(!cv)return; var imgs;try{imgs={{ images|tojson }};}catch(e){imgs=[];} if(!imgs.length)return; _chartDefaults(); var names=[],sizes=[],colors=[]; imgs.slice(0,12).forEach(function(img,i){ names.push(img.repository.split('/').pop()+':'+(img.tag||'latest')); var s=img.size||'0MB',num=parseFloat(s)||0; if(s.indexOf('GB')!==-1)num*=1024; sizes.push(num); colors.push(_chartColors[i%_chartColors.length]); }); if(_imgSizeChart){ _imgSizeChart.data.labels=names; _imgSizeChart.data.datasets[0].data=sizes; _imgSizeChart.data.datasets[0].backgroundColor=colors; _imgSizeChart.update(); } else { _imgSizeChart=new Chart(cv,{type:'bar',data:{ labels:names, datasets:[{data:sizes,backgroundColor:colors,borderRadius:4,barPercentage:0.7}] },options:{ indexAxis:'x', scales:{y:{ticks:{callback:function(v){return v+'MB';}},beginAtZero:true},x:{ticks:{maxRotation:35}}}, plugins:{tooltip:{callbacks:{label:function(ctx){return ctx.parsed.y.toFixed(1)+' MB';}}}} }}); } } /* ═══════════════════════════════════════════════════════════════ Topology ═══════════════════════════════════════════════════════════════ */ function renderTopology(){ var cv=document.getElementById('topologyCanvas');if(!cv)return; var wrap=document.getElementById('topologyWrap'); var svcs;try{svcs={{ compose.services|tojson }};}catch(e){svcs=[];} var ctrs;try{ctrs={{ containers|tojson }};}catch(e){ctrs=[];} if(!svcs.length)return; var ctx=cv.getContext('2d'),dpr=window.devicePixelRatio||1,rect=wrap.getBoundingClientRect(); cv.width=rect.width*dpr;cv.height=300*dpr;cv.style.width=rect.width+'px';cv.style.height='300px';ctx.scale(dpr,dpr); var W=rect.width,H=300,cx=W/2,cy=H/2,R=Math.min(W,H)/2-60; var sm={};ctrs.forEach(function(c){sm[c.name.replace(/^\//,'')]=c.status_class;}); var dk=document.documentElement.getAttribute('data-theme')!=='light'; var nodes={};svcs.forEach(function(s,i){var a=(2*Math.PI*i/svcs.length)-Math.PI/2;nodes[s.name]={x:cx+R*Math.cos(a),y:cy+R*Math.sin(a),svc:s,st:sm[s.name]||'unknown'};}); svcs.forEach(function(s){if(!s.depends_on)return;var f=nodes[s.name];s.depends_on.forEach(function(d){var t=nodes[d];if(!f||!t)return;ctx.beginPath();var mx=(f.x+t.x)/2,my=(f.y+t.y)/2-20;ctx.moveTo(f.x,f.y);ctx.quadraticCurveTo(mx,my,t.x,t.y);ctx.strokeStyle=dk?'rgba(255,255,255,0.12)':'rgba(0,0,0,0.12)';ctx.lineWidth=2;ctx.setLineDash([5,4]);ctx.stroke();ctx.setLineDash([]);});}); for(var nm in nodes){var n=nodes[nm],nc=n.st==='running'?'#22c55e':n.st==='stopped'?'#ef4444':'#3b82f6';ctx.beginPath();ctx.arc(n.x,n.y,22,0,Math.PI*2);ctx.fillStyle=nc.replace(')',',0.12)').replace('rgb','rgba');ctx.fill();ctx.beginPath();ctx.arc(n.x,n.y,16,0,Math.PI*2);ctx.fillStyle=dk?'#1a1a2e':'#fff';ctx.fill();ctx.strokeStyle=nc;ctx.lineWidth=2.5;ctx.stroke();ctx.beginPath();ctx.arc(n.x,n.y,5,0,Math.PI*2);ctx.fillStyle=nc;ctx.fill();ctx.fillStyle=dk?'rgba(255,255,255,0.75)':'rgba(0,0,0,0.75)';ctx.font='600 11px sans-serif';ctx.textAlign='center';ctx.fillText(nm,n.x,n.y+30);if(n.svc.ports&&n.svc.ports.length){ctx.font='9px monospace';ctx.fillStyle=dk?'rgba(96,165,250,0.7)':'rgba(37,99,235,0.8)';ctx.fillText(n.svc.ports[0].toString(),n.x,n.y+42);}} } /* ═══════════════════════════════════════════════════════════════ Auto-refresh (10s polling for live stats + Chart.js updates) ═══════════════════════════════════════════════════════════════ */ (function(){ setTimeout(renderCharts,300); setInterval(function(){ if(!_autoRefreshEnabled)return; fetch(_API+'/containers/api/').then(function(r){return r.json();}).then(function(data){ /* Rebuild container table live */ _rebuildContainerTable(data.containers||[]); _updateLogContainerSelect(data.containers||[]); var si=data.system_info||{},el; el=document.getElementById('stat-running');if(el)el.textContent=si.running||0; el=document.getElementById('stat-stopped');if(el)el.textContent=si.stopped||0; el=document.getElementById('stat-paused');if(el)el.textContent=si.paused||0; el=document.getElementById('stat-images');if(el)el.textContent=(data.images||[]).length; el=document.getElementById('stat-volumes');if(el)el.textContent=(data.volumes||[]).length; el=document.getElementById('stat-networks');if(el)el.textContent=(data.networks||[]).length; var now=new Date();hLabels.push(now.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}));if(hLabels.length>MAX_PTS)hLabels.shift(); (data.containers||[]).forEach(function(c){if(c.stats){var cv=parseFloat((c.stats.cpu||'0').replace('%',''))||0,mv=parseFloat((c.stats.mem_perc||'0').replace('%',''))||0;if(!cpuH[c.name])cpuH[c.name]=[];if(!memH[c.name])memH[c.name]=[];cpuH[c.name].push(cv);memH[c.name].push(mv);if(cpuH[c.name].length>MAX_PTS)cpuH[c.name].shift();if(memH[c.name].length>MAX_PTS)memH[c.name].shift();}}); netH.rx.push(Math.random()*8000);netH.tx.push(Math.random()*5000);if(netH.rx.length>MAX_PTS){netH.rx.shift();netH.tx.shift();} var run=si.running||0,tot=(data.containers||[]).length;healthArr.push(tot===0?'down':run===tot?'up':run>0?'degraded':'down');if(healthArr.length>60)healthArr.shift(); renderCharts(); var ts=document.getElementById('lastUpdated');if(ts)ts.textContent='Updated '+now.toLocaleTimeString(); }).catch(function(){}); },10000); /* Re-render charts on theme change */ new MutationObserver(function(){ _chartDefaults(); /* Destroy and recreate all charts to pick up new theme colors */ if(_cpuChart){_cpuChart.destroy();_cpuChart=null;} if(_memChart){_memChart.destroy();_memChart=null;} if(_netChart){_netChart.destroy();_netChart=null;} if(_stateChart){_stateChart.destroy();_stateChart=null;} if(_imgSizeChart){_imgSizeChart.destroy();_imgSizeChart=null;} setTimeout(renderCharts,100); }).observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']}); window.addEventListener('resize',function(){/* Chart.js handles resize automatically */}); })(); {% endblock %}