🔒 Repository is read-only – file editing is disabled.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
{% extends "admin/layout.html" %}
{% block admin_title %}Terminal{% endblock %}
{% block admin_h1 %} Terminal{% endblock %}
{% block admin_sub %}Wykonywanie komend na VPS (wyjście na żywo przez SSE){% endblock %}
{% block admin_head %}
<style>
.term { background:#0a0a0a; border:1px solid #1a1a2e; border-radius:var(--radius);
font-family:'JetBrains Mono','Fira Code',monospace; font-size:0.8rem; line-height:1.55;
height:60vh; overflow-y:auto; padding:0.8rem; }
.term.line { white-space:pre-wrap; word-break:break-all; }
.term.prompt { color:#58a6ff; }
.term.cwd { color:#3fb950; }
.term.cmd { color:#e6edf3; }
.term.err { color:#f85149; }
.term-input-row { display:flex; gap:0.4rem; margin-top:0.5rem; align-items:center; }
.term-input-row input { flex:1; font-family:'JetBrains Mono',monospace; font-size:0.82rem;
background:#0d1117; color:var(--fg); border:1px solid var(--border); border-radius:var(--radius-sm);
padding:0.5rem 0.7rem; }
.term-input-row input:focus { outline:none; border-color:var(--accent); }
.term-status { font-size:0.75rem; color:var(--muted); margin-top:0.4rem; }
.term-btns { display:flex; gap:0.4rem; margin-top:0.5rem; }
</style>
{% endblock %}
{% block admin_content %}
<div class="term" id="term" onclick="document.getElementById('term-in').focus()">
<div class="line dim">PaganOS VPS — terminal (komendy przez /bin/bash -c). Ctrl+C przerywa.</div>
</div>
<div class="term-input-row">
<span class="prompt" id="term-prompt">root@vps:~$</span>
<input id="term-in" autocomplete="off" autocapitalize="off" spellcheck="false"
placeholder="wpisz komendę i naciśnij Enter..." onkeydown="onEnter(event)">
</div>
<div class="term-btns">
<button class="btn btn-sm" onclick="stop()">⏹ Przerwij (Ctrl+C)</button>
<button class="btn btn-sm" onclick="clearTerm()"> Wyczyść</button>
<span class="term-status" id="term-status"></span>
</div>
<script>
const term=document.getElementById('term');
const input=document.getElementById('term-in');
let curJob=null, hist=[], hi=0;
function addLine(html){ const d=document.createElement('div'); d.className='line'; d.innerHTML=html;
term.appendChild(d); term.scrollTop=term.scrollHeight; }
function addText(t){
const d=document.createElement('div'); d.className='line';
d.textContent=t; term.appendChild(d); term.scrollTop=term.scrollHeight;
// ogranicz liczbę linii w DOM
while(term.childElementCount>600) term.removeChild(term.firstElementChild);
}
function onEnter(e){
if(e.key==='Enter'){
const cmd=input.value.trim();
if(!cmd) return;
hist.push(cmd); hi=hist.length;
addText(`root@vps:~$ ${cmd}`);
input.value=''; run(cmd);
} else if(e.key==='ArrowUp'){ e.preventDefault(); if(hi>0){hi--; input.value=hist[hi]||'';} }
else if(e.key==='ArrowDown'){ e.preventDefault(); if(hi<hist.length-1){hi++; input.value=hist[hi]||'';} else {hi=hist.length; input.value='';} }
}
async function run(cmd){
document.getElementById('term-status').textContent='⏳ uruchamianie...';
try{
const r=await fetch('/admin/terminal/exec',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':CSRF_TOKEN},
body:JSON.stringify({cmd:cmd})});
const d=await r.json();
if(!d.ok){ addText('❌ '+(d.error||'błąd')); status(''); return; }
curJob=d.job;
const es=new EventSource('/admin/terminal/stream/'+d.job);
es.onmessage=e=>{
let m; try{ m=JSON.parse(e.data); }catch(_){ return; }
if(m.type==='out'){ addText(m.data.replace(/\n$/,'')); }
else if(m.type==='end'){
es.close(); curJob=null;
status('✅ zakończono (rc='+(m.rc===null?'?':m.rc)+')');
addText('');
}
};
es.onerror=()=>{ es.close(); curJob=null; status('⚠ strumień przerwany'); };
}catch(e){ addText('❌ '+e); status(''); }
}
function status(t){ document.getElementById('term-status').textContent=t; }
async function stop(){
if(curJob){ try{ await fetch('/admin/terminal/kill/'+curJob,{method:'POST',headers:{'X-CSRF-Token':CSRF_TOKEN}}); }catch(_){}
addText('⏹ przerwano (SIGTERM)'); curJob=null; status(''); }
}
function clearTerm(){ while(term.childElementCount>1) term.removeChild(term.lastElementChild); }
input.focus();
</script>
{% endblock %}