Files
xampp-gui/index.html
T
2026-06-07 14:29:25 +00:00

300 lines
17 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XAMPP Control Panel</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
:root,[data-theme="light"]{
--color-bg:#f7f6f2;--color-surface:#f9f8f5;--color-surface-2:#fbfbf9;
--color-surface-offset:#f0ede8;--color-border:#d4d1ca;--color-divider:#dcd9d5;
--color-text:#28251d;--color-text-muted:#7a7974;--color-text-faint:#bab9b4;
--color-primary:#01696f;--color-primary-highlight:#cedcd8;
--color-success:#437a22;--color-success-highlight:#d4dfcc;
--color-warning:#964219;--color-warning-highlight:#ddcfc6;
--color-error:#a12c7b;--color-error-highlight:#e0ced7;
--color-notification:#a13544;--color-orange:#da7101;
--shadow-sm:0 1px 2px oklch(0.2 0.01 80/.06);--shadow-md:0 4px 12px oklch(0.2 0.01 80/.08);
--radius-sm:.375rem;--radius-md:.5rem;--radius-lg:.75rem;--radius-xl:1rem;--radius-full:9999px;
--transition:180ms cubic-bezier(0.16,1,0.3,1);
--font-body:'Inter',sans-serif;--font-mono:'JetBrains Mono',monospace;
--text-xs:clamp(.75rem,.7rem + .25vw,.875rem);--text-sm:clamp(.875rem,.8rem + .35vw,1rem);
--text-base:clamp(1rem,.95rem + .25vw,1.125rem);--text-lg:clamp(1.125rem,1rem + .75vw,1.5rem);
--sp1:.25rem;--sp2:.5rem;--sp3:.75rem;--sp4:1rem;--sp5:1.25rem;--sp6:1.5rem;--sp8:2rem;
}
[data-theme="dark"]{
--color-bg:#111110;--color-surface:#1a1917;--color-surface-2:#1f1e1c;
--color-surface-offset:#252321;--color-border:#2e2c2a;--color-divider:#242220;
--color-text:#cdccca;--color-text-muted:#797876;--color-text-faint:#5a5957;
--color-primary:#4f98a3;--color-primary-highlight:#1e3032;
--color-success:#6daa45;--color-success-highlight:#1e3018;
--color-warning:#bb653b;--color-warning-highlight:#2e1e10;
--color-error:#d163a7;--color-error-highlight:#2a1420;
--color-notification:#dd6974;--color-orange:#fdab43;
--shadow-sm:0 1px 2px oklch(0 0 0/.3);--shadow-md:0 4px 12px oklch(0 0 0/.4);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{-webkit-font-smoothing:antialiased}
body{min-height:100dvh;font-family:var(--font-body);font-size:var(--text-base);color:var(--color-text);background:var(--color-bg);display:flex;flex-direction:column}
header{position:sticky;top:0;z-index:100;background:var(--color-surface);border-bottom:1px solid var(--color-border);padding:var(--sp4) var(--sp6);display:flex;align-items:center;justify-content:space-between;box-shadow:var(--shadow-sm)}
.logo{display:flex;align-items:center;gap:var(--sp3)}
.logo svg{color:var(--color-primary)}
.logo-text{font-size:var(--text-lg);font-weight:700;letter-spacing:-.02em}
.logo-sub{font-size:var(--text-xs);color:var(--color-text-muted)}
.theme-btn{width:36px;height:36px;border-radius:var(--radius-md);border:1px solid var(--color-border);background:var(--color-surface-2);display:flex;align-items:center;justify-content:center;color:var(--color-text-muted);cursor:pointer;transition:background var(--transition),color var(--transition)}
.theme-btn:hover{background:var(--color-surface-offset);color:var(--color-text)}
main{flex:1;max-width:860px;width:100%;margin:0 auto;padding:var(--sp8) var(--sp6);display:flex;flex-direction:column;gap:var(--sp6)}
.status-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:var(--sp5) var(--sp6);display:flex;align-items:center;justify-content:space-between;gap:var(--sp4);box-shadow:var(--shadow-sm)}
.status-info{display:flex;flex-direction:column;gap:var(--sp1)}
.status-label{font-size:var(--text-xs);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.06em}
.status-value{font-size:var(--text-base);font-weight:600;display:flex;align-items:center;gap:var(--sp2)}
.dot{width:8px;height:8px;border-radius:var(--radius-full);background:var(--color-text-faint);transition:background var(--transition)}
.dot.running{background:var(--color-success);box-shadow:0 0 6px color-mix(in oklch,var(--color-success) 60%,transparent)}
.dot.stopped{background:var(--color-notification)}
.dot.loading{background:var(--color-orange);animation:pulse 1s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.sec-title{font-size:var(--text-xs);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.06em;font-weight:600}
.btn-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(180px,100%),1fr));gap:var(--sp3)}
.btn{display:flex;align-items:center;justify-content:center;gap:var(--sp2);padding:var(--sp4) var(--sp5);border-radius:var(--radius-lg);border:1px solid transparent;font-size:var(--text-sm);font-weight:600;cursor:pointer;transition:all var(--transition)}
.btn:active{transform:scale(.97)}
.btn:disabled{opacity:.45;cursor:not-allowed;transform:none}
.btn-start{background:var(--color-success-highlight);color:var(--color-success);border-color:color-mix(in oklch,var(--color-success) 25%,transparent)}
.btn-start:hover:not(:disabled){background:var(--color-success);color:#fff;border-color:var(--color-success);box-shadow:var(--shadow-md)}
.btn-stop{background:var(--color-error-highlight);color:var(--color-error);border-color:color-mix(in oklch,var(--color-error) 25%,transparent)}
.btn-stop:hover:not(:disabled){background:var(--color-error);color:#fff;border-color:var(--color-error);box-shadow:var(--shadow-md)}
.btn-restart{background:var(--color-warning-highlight);color:var(--color-warning);border-color:color-mix(in oklch,var(--color-warning) 25%,transparent)}
.btn-restart:hover:not(:disabled){background:var(--color-warning);color:#fff;border-color:var(--color-warning);box-shadow:var(--shadow-md)}
.btn-reload{background:var(--color-primary-highlight);color:var(--color-primary);border-color:color-mix(in oklch,var(--color-primary) 25%,transparent)}
.btn-reload:hover:not(:disabled){background:var(--color-primary);color:#fff;border-color:var(--color-primary);box-shadow:var(--shadow-md)}
.btn-stat{background:var(--color-surface-2);color:var(--color-text-muted);border-color:var(--color-border)}
.btn-stat:hover:not(:disabled){background:var(--color-surface-offset);color:var(--color-text)}
.svc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(260px,100%),1fr));gap:var(--sp4)}
.svc-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:var(--sp5);box-shadow:var(--shadow-sm);display:flex;flex-direction:column;gap:var(--sp4)}
.svc-head{display:flex;align-items:center;justify-content:space-between}
.svc-name{font-weight:700;font-size:var(--text-sm)}
.badge{font-size:var(--text-xs);font-weight:600;padding:2px 8px;border-radius:var(--radius-full);background:var(--color-surface-offset);color:var(--color-text-muted);transition:all var(--transition)}
.badge.running{background:var(--color-success-highlight);color:var(--color-success)}
.badge.stopped{background:var(--color-error-highlight);color:var(--color-error)}
.svc-btns{display:flex;gap:var(--sp2)}
.svc-btn{flex:1;padding:var(--sp2) var(--sp3);border-radius:var(--radius-md);border:1px solid var(--color-border);background:var(--color-surface-2);color:var(--color-text-muted);font-size:var(--text-xs);font-weight:600;cursor:pointer;transition:all var(--transition)}
.svc-btn:disabled{opacity:.4;cursor:not-allowed}
.svc-btn.s:hover:not(:disabled){background:var(--color-success);color:#fff;border-color:var(--color-success)}
.svc-btn.x:hover:not(:disabled){background:var(--color-error);color:#fff;border-color:var(--color-error)}
.log-wrap{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);overflow:hidden;box-shadow:var(--shadow-sm)}
.log-head{padding:var(--sp3) var(--sp5);border-bottom:1px solid var(--color-divider);display:flex;align-items:center;justify-content:space-between;background:var(--color-surface-2)}
.log-title{font-size:var(--text-sm);font-weight:600}
.log-clr{font-size:var(--text-xs);color:var(--color-text-muted);background:none;border:none;cursor:pointer;padding:var(--sp1) var(--sp2);border-radius:var(--radius-sm);transition:all var(--transition)}
.log-clr:hover{color:var(--color-text);background:var(--color-surface-offset)}
#log{font-family:var(--font-mono);font-size:.8rem;line-height:1.7;padding:var(--sp4) var(--sp5);height:220px;overflow-y:auto;white-space:pre-wrap;word-break:break-all}
.ll{margin-bottom:2px;color:var(--color-text-faint)}
.ll.ok{color:var(--color-success)}.ll.er{color:var(--color-error)}
.ll.in{color:var(--color-primary)}.ll.wa{color:var(--color-orange)}
footer{padding:var(--sp4) var(--sp6);border-top:1px solid var(--color-divider);text-align:center;font-size:var(--text-xs);color:var(--color-text-faint)}
footer code{font-family:var(--font-mono)}
@media(max-width:480px){main,header{padding-inline:var(--sp4)}}
</style>
</head>
<body>
<header>
<div class="logo">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="currentColor" fill-opacity="0.1"/>
<path d="M8 10L14 16L8 22" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17 10L24 16L17 22" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div><div class="logo-text">XAMPP Control</div><div class="logo-sub">/opt/lampp</div></div>
</div>
<button class="theme-btn" id="themeBtn" aria-label="Toggle theme"></button>
</header>
<main>
<div class="status-card">
<div class="status-info">
<div class="status-label">XAMPP Status</div>
<div class="status-value"><span class="dot" id="mainDot"></span><span id="mainTxt">Unknown</span></div>
</div>
<div class="status-info" style="text-align:right">
<div class="status-label">Backend</div>
<div id="backendTxt" style="font-size:var(--text-xs);font-weight:600;color:var(--color-text-muted)">Connecting...</div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:var(--sp3)">
<div class="sec-title">All Services</div>
<div class="btn-grid">
<button class="btn btn-start" onclick="run('start')">&#9654; Start</button>
<button class="btn btn-stop" onclick="run('stop')">&#9632; Stop</button>
<button class="btn btn-restart" onclick="run('restart')">&#8634; Restart</button>
<button class="btn btn-reload" onclick="run('reload')">&#10227; Reload</button>
<button class="btn btn-stat" onclick="run('status')">&#9432; Status</button>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:var(--sp3)">
<div class="sec-title">Individual Services</div>
<div class="svc-grid">
<div class="svc-card">
<div class="svc-head"><div class="svc-name">&#127760; Apache</div><span class="badge" id="b-apache">&#8212;</span></div>
<div class="svc-btns">
<button class="svc-btn s" onclick="run('startapache')">&#9654; Start</button>
<button class="svc-btn x" onclick="run('stopapache')">&#9632; Stop</button>
</div>
</div>
<div class="svc-card">
<div class="svc-head"><div class="svc-name">&#128044; MySQL</div><span class="badge" id="b-mysql">&#8212;</span></div>
<div class="svc-btns">
<button class="svc-btn s" onclick="run('startmysql')">&#9654; Start</button>
<button class="svc-btn x" onclick="run('stopmysql')">&#9632; Stop</button>
</div>
</div>
<div class="svc-card">
<div class="svc-head"><div class="svc-name">&#128193; FTP</div><span class="badge" id="b-ftp">&#8212;</span></div>
<div class="svc-btns">
<button class="svc-btn s" onclick="run('startftp')">&#9654; Start</button>
<button class="svc-btn x" onclick="run('stopftp')">&#9632; Stop</button>
</div>
</div>
</div>
</div>
<div class="log-wrap">
<div class="log-head">
<div class="log-title">&#128196; Output</div>
<button class="log-clr" onclick="clearLog()">Clear</button>
</div>
<div id="log"></div>
</div>
</main>
<footer>
Backend: <code>python3 ~/xampp-gui/xampp-backend.py</code> &nbsp;&middot;&nbsp;
HTML: <code>cd ~/xampp-gui &amp;&amp; python3 -m http.server 8080</code>
</footer>
<script>
const B='http://localhost:5050';
let ok=false,busy=false;
(()=>{
const btn=document.getElementById('themeBtn'),h=document.documentElement;
let d=h.getAttribute('data-theme')||'dark';
const sun='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>';
const moon='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
btn.innerHTML=d==='dark'?moon:sun;
btn.onclick=()=>{d=d==='dark'?'light':'dark';h.setAttribute('data-theme',d);btn.innerHTML=d==='dark'?moon:sun;};
})();
const logEl=document.getElementById('log');
function log(msg,t=''){
const d=document.createElement('div');
d.className='ll'+(t?' '+t:'');
d.textContent='['+new Date().toLocaleTimeString('en-GB')+'] '+msg;
logEl.appendChild(d);logEl.scrollTop=logEl.scrollHeight;
}
function clearLog(){logEl.innerHTML='';log('Log cleared.');}
function setMain(state){
document.getElementById('mainDot').className='dot '+state;
document.getElementById('mainTxt').textContent=
state==='running'?'Running':state==='stopped'?'Stopped':state==='loading'?'Executing...':'Unknown';
}
function badge(id,state){
const el=document.getElementById('b-'+id);
if(!el)return;
el.className='badge'+(state?' '+state:'');
el.textContent=state==='running'?'Running':state==='stopped'?'Stopped':'—';
}
function parseStatus(output){
const lines=output.split('\n');
const svcs=[
{keys:['apache'],id:'apache'},
{keys:['mysql'],id:'mysql'},
{keys:['proftpd','ftp'],id:'ftp'},
];
const states={};
for(const line of lines){
const low=line.toLowerCase().trim();
if(!low)continue;
for(const {keys,id} of svcs){
if(!keys.some(k=>low.includes(k)))continue;
if(/is running/.test(low)){states[id]='running';break;}
if(/is not running/.test(low)){states[id]='stopped';break;}
if(/starting/.test(low)){states[id]=/ok\.|already running/.test(low)?'running':'stopped';break;}
if(/stopping/.test(low)){states[id]='stopped';break;}
if(/started|ok\./.test(low)&&!/not running/.test(low)){states[id]='running';break;}
if(/not running|stopped/.test(low)){states[id]='stopped';break;}
}
}
for(const [id,state] of Object.entries(states))badge(id,state);
if('apache' in states)setMain(states['apache']);
}
function lockBtns(v){document.querySelectorAll('.btn,.svc-btn').forEach(b=>b.disabled=v);}
async function run(cmd){
if(!ok){log('Backend not reachable! Run: python3 xampp-backend.py','er');return;}
if(busy){log('Please wait, a command is already running...','wa');return;}
busy=true;lockBtns(true);setMain('loading');
log('Running: ./xampp '+cmd+' ...','in');
try{
const res=await fetch(B+'/run',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({cmd}),signal:AbortSignal.timeout(180000)
});
const data=await res.json();
const out=data.output||'';
out.split('\n').filter(l=>l.trim()).forEach(l=>{
const low=l.toLowerCase();
const t=low.includes('error')||low.includes('fail')?'er'
:/ok\.|starting|stopping|running/.test(low)?'ok':'';
log(l,t);
});
parseStatus(out);
if(['start','stop','restart','reload'].includes(cmd))setTimeout(fetchStatus,1500);
}catch(e){
log(e.name==='TimeoutError'||e.name==='AbortError'?'Timeout — command took too long':'Error: '+e.message,'er');
setTimeout(fetchStatus,1000);
}finally{busy=false;lockBtns(false);}
}
async function fetchStatus(){
if(!ok||busy)return;
try{
const res=await fetch(B+'/run',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({cmd:'status'}),signal:AbortSignal.timeout(30000)
});
parseStatus((await res.json()).output||'');
}catch(_){}
}
async function ping(){
if(busy)return;
try{
await fetch(B+'/ping',{signal:AbortSignal.timeout(5000)});
if(!ok){
ok=true;
document.getElementById('backendTxt').textContent='Connected';
document.getElementById('backendTxt').style.color='var(--color-success)';
log('Backend connected (port 5050)','ok');
fetchStatus();
}
}catch(_){
ok=false;
document.getElementById('backendTxt').textContent='Not connected';
document.getElementById('backendTxt').style.color='var(--color-error)';
}
}
log('XAMPP Control Panel ready.','in');
log('Connecting to backend...','');
ping();
setInterval(ping,30000);
</script>
</body>
</html>