Files
server-manager/views/servers/database.php
Agent c3009fbbf7 Add views directory and UI improvements
- Add all view templates (servers, auth, dashboard, admin, layouts, errors, terminal)
- Fix SSH double decryption bug in SSHService
- Fix router parameter type casting for strict_types
- Add missing error views (401, 403, 404)
- Add nginx manager with file editing
- Add SSL certificates management
- Add database manager with phpMyAdmin-like interface
- Add sidebar server accordion
2026-06-06 17:59:13 -04:00

414 lines
25 KiB
PHP

<?php
$server = $server ?? [];
$db = $db ?? [];
$installed = $db['installed'] ?? false;
$dbType = $db['db_type'] ?? '';
$databases = $db['databases'] ?? [];
$totalSize = $db['total_size_mb'] ?? 0;
$status = $db['status'] ?? 'inactive';
?>
<div class="page-header">
<div class="page-header-left">
<div class="back-link">
<a href="/servers/<?= $server['id'] ?>"><i class="fas fa-arrow-left"></i> Back to Server</a>
</div>
<h1 class="page-title"><i class="fas fa-database"></i> <?= htmlspecialchars($dbType) ?> Manager</h1>
<p class="page-subtitle"><?= htmlspecialchars($server['name']) ?> &middot; <?= count($databases) ?> databases &middot; <?= $totalSize ?> MB</p>
</div>
<div class="page-header-right">
<span class="status-badge status-<?= $status === 'active' ? 'online' : 'offline' ?>" style="font-size:0.8rem"><?= ucfirst($status) ?></span>
<button class="btn btn-secondary" onclick="location.reload()"><i class="fas fa-sync-alt"></i></button>
</div>
</div>
<?php if (!$installed): ?>
<div class="card"><div class="card-body"><div class="empty-state">
<i class="fas fa-database empty-icon" style="font-size:3rem;opacity:0.4"></i>
<h3>No database server found</h3>
<p>Neither MySQL nor MariaDB is installed on this server.</p>
<p class="text-muted">Install with: <code>sudo apt install mysql-server</code> or <code>sudo apt install mariadb-server</code></p>
</div></div></div>
<?php else: ?>
<div class="pma-layout">
<aside class="pma-sidebar">
<div class="pma-sidebar-header" style="justify-content:space-between">
<span><i class="fas fa-database"></i> Databases</span>
<span class="badge badge-info"><?= count($databases) ?></span>
</div>
<div class="pma-sidebar-filter"><input type="text" class="form-input" placeholder="Filter..." oninput="filterSidebar(this.value)" style="font-size:0.8rem"></div>
<div class="pma-sidebar-list" id="sidebarList">
<?php foreach ($databases as $d): ?>
<div class="pma-accordion-item" data-db="<?= htmlspecialchars($d['name']) ?>">
<div class="pma-accordion-header" onclick="toggleDb('<?= htmlspecialchars($d['name']) ?>')">
<i class="fas fa-database"></i>
<span class="pma-db-name"><?= htmlspecialchars($d['name']) ?></span>
<span class="pma-db-size"><?= $d['size_mb'] ?> MB</span>
<i class="fas fa-chevron-right pma-chevron"></i>
</div>
<div class="pma-accordion-body" id="tables_<?= htmlspecialchars(md5($d['name'])) ?>">
<div class="pma-loading-tables" style="display:none"><i class="fas fa-spinner fa-spin"></i> Loading...</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div style="padding:0.5rem;border-top:1px solid var(--border-color);display:flex;gap:0.5rem">
<button class="btn btn-xs btn-secondary" style="flex:1" onclick="switchTab('sql')"><i class="fas fa-terminal"></i> SQL</button>
<button class="btn btn-xs btn-secondary" style="flex:1" onclick="switchTab('users');loadUsers()"><i class="fas fa-users"></i> Users</button>
</div>
</aside>
<main class="pma-content">
<div class="pma-tabs">
<button class="pma-tab" id="tabBrowse" onclick="switchTab('browse')" style="display:none"><i class="fas fa-table"></i> Browse</button>
<button class="pma-tab" id="tabStructure" onclick="switchTab('structure')" style="display:none"><i class="fas fa-list"></i> Structure</button>
<button class="pma-tab active" id="tabSql" onclick="switchTab('sql')"><i class="fas fa-terminal"></i> SQL</button>
<button class="pma-tab" id="tabUsers" onclick="switchTab('users')" style="display:none"><i class="fas fa-users"></i> Users</button>
</div>
<div id="pmaWelcome" class="pma-panel active">
<div class="empty-state"><i class="fas fa-database empty-icon" style="opacity:0.2;font-size:5rem"></i><h3>Select a database</h3><p>Click a database in the left panel.</p></div>
</div>
<div id="pmaTables" class="pma-panel"></div>
<div id="pmaStructure" class="pma-panel"></div>
<div id="pmaBrowse" class="pma-panel"></div>
<div id="pmaUsers" class="pma-panel"></div>
<div id="pmaSql" class="pma-panel active">
<div class="pma-sql-editor">
<textarea id="sqlInput" class="form-input" rows="6" style="font-family:var(--font-mono);font-size:0.85rem;border-radius:var(--radius) var(--radius) 0 0" placeholder="SELECT * FROM ...">SHOW DATABASES</textarea>
<div class="pma-sql-toolbar">
<button class="btn btn-primary btn-sm" onclick="runSql()"><i class="fas fa-play"></i> Go</button>
<button class="btn btn-secondary btn-sm" onclick="document.getElementById('sqlInput').value=''"><i class="fas fa-eraser"></i></button>
<span style="flex:1"></span>
<span class="text-muted" style="font-size:0.78rem">Ctrl+Enter</span>
</div>
<div id="sqlResult" class="nginx-config-viewer" style="display:none;border-radius:0 0 var(--radius) var(--radius);border-top:0"><pre><code id="sqlResultText"></code></pre></div>
</div>
</div>
</main>
</div>
<script>
let currentDb = '';
let currentTable = '';
let openedDb = '';
function getCsrfToken() { return document.querySelector('meta[name="csrf-token"]')?.content || ''; }
function filterTables(inp) {
const q = inp.value.toLowerCase();
const body = inp.closest('.pma-accordion-body');
if (body) {
body.querySelectorAll('.pma-table-item').forEach(el => {
el.style.display = !q || (el.dataset.tbl || '').includes(q) ? '' : 'none';
});
}
}
function filterSidebar(q) {
document.querySelectorAll('.pma-accordion-item').forEach(el => {
el.style.display = el.textContent.toLowerCase().includes(q.toLowerCase()) ? '' : 'none';
});
}
function toggleDb(db) {
const item = document.querySelector(`.pma-accordion-item[data-db="${db.replace(/"/g,'\\"')}"]`);
if (!item) return;
const body = item.querySelector('.pma-accordion-body');
const header = item.querySelector('.pma-accordion-header');
const chevron = item.querySelector('.pma-chevron');
if (item.classList.contains('expanded')) {
item.classList.remove('expanded');
body.style.maxHeight = '0';
return;
}
document.querySelectorAll('.pma-accordion-item.expanded').forEach(el => {
el.classList.remove('expanded');
el.querySelector('.pma-accordion-body').style.maxHeight = '0';
});
item.classList.add('expanded');
currentDb = db;
if (body.querySelector('.pma-table-item')) {
body.style.maxHeight = body.scrollHeight + 'px';
return;
}
body.querySelector('.pma-loading-tables').style.display = '';
body.style.maxHeight = '60px';
fetch('/servers/<?= $server['id'] ?>/database', {
method: 'POST',
headers: {'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
body: 'action=tables&db=' + encodeURIComponent(db) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
})
.then(r => r.json())
.then(data => {
body.querySelector('.pma-loading-tables').style.display = 'none';
let html = '<div style="padding:0.3rem 0.5rem"><input type="text" class="form-input tbl-filter" placeholder="Filter tables..." style="font-size:0.75rem;padding:0.25rem 0.4rem;width:100%;box-sizing:border-box" oninput="filterTables(this)"></div>';
(data.tables || []).forEach(t => {
html += '<div class="pma-table-item" data-tbl="' + escHtml(t.name).toLowerCase() + '" onclick="selectTable(\'' + db.replace(/'/g,"\\'") + "','" + t.name.replace(/'/g,"\\'") + '\')">'
+ '<i class="fas fa-table"></i> ' + escHtml(t.name)
+ ' <span class="pma-db-size">' + t.rows + ' rows</span></div>';
});
if (!html) html = '<div class="pma-table-item" style="color:var(--text-muted);cursor:default">No tables</div>';
body.innerHTML = html;
body.style.maxHeight = body.scrollHeight + 'px';
});
}
function selectTable(db, table) {
currentDb = db; currentTable = table;
document.getElementById('tabBrowse').style.display = '';
document.getElementById('tabStructure').style.display = '';
document.querySelectorAll('.pma-tab').forEach(t=>t.classList.remove('active'));
document.getElementById('tabStructure').classList.add('active');
showPanel('structure');
loadStructure(db, table);
}
function loadStructure(db, table) {
const panel = document.getElementById('pmaStructure');
panel.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-muted)"><i class="fas fa-spinner fa-spin"></i> Loading structure...</div>';
fetch('/servers/<?= $server['id'] ?>/database', {
method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
body:'action=structure&db='+encodeURIComponent(db)+'&table='+encodeURIComponent(table)+'&_csrf_token='+encodeURIComponent(getCsrfToken()),
}).then(r=>r.json()).then(d=>{
if (!d.success||!d.columns) return;
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-list"></i> <code>'+escHtml(table)+'</code></h2>'
+'<span class="badge badge-info">'+d.columns.length+' columns</span>'
+'<span style="flex:1"></span>'
+'<button class="btn btn-xs btn-info" onclick="browseTable(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\')"><i class="fas fa-search"></i> Browse</button>'
+'</div>';
html+='<div class="table-responsive"><table class="table table-sm"><thead><tr><th>#</th><th>Field</th><th>Type</th><th>Collation</th><th>Null</th><th>Key</th><th>Default</th><th>Extra</th></tr></thead><tbody>';
d.columns.forEach((c,i)=>{
html+='<tr><td>'+(i+1)+'</td><td><strong>'+escHtml(c.field)+'</strong></td><td><code>'+escHtml(c.type)+'</code></td>'
+'<td>'+escHtml(c.collation)+'</td><td>'+escHtml(c.null)+'</td><td><strong>'+escHtml(c.key)+'</strong></td>'
+'<td>'+(c.default??'NULL')+'</td><td>'+escHtml(c.extra)+'</td></tr>';
});
html+='</tbody></table></div>';
panel.innerHTML=html;
});
}
function browseTable(db, table) {
currentDb=db; currentTable=table;
document.getElementById('tabBrowse').style.display='';
document.getElementById('tabStructure').style.display='';
document.querySelectorAll('.pma-tab').forEach(t=>t.classList.remove('active'));
document.getElementById('tabBrowse').classList.add('active');
showPanel('browse');
loadBrowse(db,table,1);
}
function loadBrowse(db,table,page) {
const panel=document.getElementById('pmaBrowse');
const inputs=document.querySelectorAll('.pma-fi');
let filters='';
if(inputs.length){
const p={};
inputs.forEach(inp=>{if(inp.value.trim())p[inp.dataset.col]=inp.value.trim()});
const s=new URLSearchParams(p).toString();
if(s)filters='&'+s;
}
panel.innerHTML='<div style="text-align:center;padding:2rem;color:var(--text-muted)"><i class="fas fa-spinner fa-spin"></i> Loading...</div>';
let body='action=browse&db='+encodeURIComponent(db)+'&table='+encodeURIComponent(table)+'&page='+page+filters+'&_csrf_token='+encodeURIComponent(getCsrfToken());
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body})
.then(r=>r.json()).then(d=>{
if(!d.success)return;
const tp=Math.ceil(d.total/d.per_page);
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:0.5rem">'
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-search"></i> <code>'+escHtml(table)+'</code></h2>'
+'<span class="badge badge-info">'+d.total+' rows</span></div>';
html+='<form onsubmit="event.preventDefault();loadBrowse(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\',1)">';
html+='<div class="table-responsive" style="max-height:450px;overflow-y:auto"><table class="table table-sm"><thead><tr>';
d.columns.forEach(c=>{html+='<th>'+escHtml(c)+'</th>'});
html+='</tr><tr class="pma-filter-row">';
d.columns.forEach(c=>{
const cv=new URLSearchParams(filters.replace(/^&/,'')).get(c)||'';
html+='<td><input type="text" class="form-input pma-fi" data-col="'+escHtml(c)+'" value="'+escHtml(cv)+'" placeholder="'+escHtml(c)+'" style="font-size:0.75rem;padding:0.2rem 0.35rem;width:100%;min-width:50px"></td>';
});
html+='</tr></thead><tbody>';
d.rows.forEach(row=>{
html+='<tr>';
row.forEach(val=>{html+='<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis"><code>'+escHtml(String(val).substring(0,80))+'</code></td>'});
html+='</tr>';
});
html+='</tbody></table></div>';
html+='<div style="display:flex;gap:0.5rem;margin-top:0.5rem;align-items:center">';
html+='<button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-search"></i> Filter</button>';
html+='<button type="button" class="btn btn-secondary btn-sm" onclick="clearBrowseFilters(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\')"><i class="fas fa-undo"></i> Clear</button>';
if(tp>1){
html+='<span style="flex:1"></span>';
for(let p=1;p<=tp&&p<=10;p++)html+='<button type="button" class="btn btn-xs '+(p===page?'btn-primary':'btn-secondary')+'" onclick="loadBrowse(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\','+p+')">'+p+'</button>';
if(tp>10)html+='<span class="text-muted">...</span>';
}
html+='</div></form>';
panel.innerHTML=html;
});
}
function clearBrowseFilters(db,table){
document.querySelectorAll('.pma-fi').forEach(inp=>inp.value='');
loadBrowse(db,table,1);
}
function loadUsers() {
showPanel('users');
document.getElementById('tabUsers').style.display='';
const panel=document.getElementById('pmaUsers');
panel.innerHTML='<div style="text-align:center;padding:2rem;color:var(--text-muted)"><i class="fas fa-spinner fa-spin"></i> Loading users...</div>';
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body:'action=users&_csrf_token='+encodeURIComponent(getCsrfToken())})
.then(r=>r.json()).then(d=>{
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-users"></i> Database Users</h2>'
+'<span class="badge badge-info">'+(d.users?.length||0)+' users</span>'
+'<span style="flex:1"></span>'
+'<button class="btn btn-sm btn-success" onclick="showCreateUser()"><i class="fas fa-plus"></i> New User</button></div>';
html+='<div class="table-responsive"><table class="table table-sm"><thead><tr><th>User</th><th>Host</th><th>Databases</th><th>Expired</th><th>Actions</th></tr></thead><tbody>';
(d.users||[]).forEach(u=>{
const dbList=(d.grants||[]).filter(g=>g.user===u.user&&g.host===u.host).map(g=>g.db).join(', ');
html+='<tr><td><strong>'+escHtml(u.user)+'</strong></td><td><code>'+escHtml(u.host)+'</code></td><td>'+(dbList||'<span class="text-muted">-</span>')+'</td><td>'+escHtml(u.expired)+'</td>'
+'<td class="actions-cell">'
+'<button class="btn btn-xs btn-secondary" onclick="showEditUser(\''+escHtml(u.user)+"','"+escHtml(u.host)+'\')" title="Change password"><i class="fas fa-key"></i></button> '
+'<button class="btn btn-xs btn-danger" onclick="deleteUser(\''+escHtml(u.user)+"','"+escHtml(u.host)+'\')" title="Delete"><i class="fas fa-trash"></i></button>'
+'</td></tr>';
});
html+='</tbody></table></div>';
panel.innerHTML=html;
});
}
function showCreateUser() {
const panel=document.getElementById('pmaUsers');
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-user-plus"></i> Create User</h2>'
+'<span style="flex:1"></span>'
+'<button class="btn btn-sm btn-secondary" onclick="loadUsers()"><i class="fas fa-arrow-left"></i> Back</button></div>'
+'<div class="dashboard-grid"><div class="dashboard-card"><div class="card-header"><h2><i class="fas fa-user"></i> Account</h2></div><div class="card-body">'
+'<div class="form-row"><div class="form-group"><label>Username</label><input type="text" id="nuUser" class="form-input" placeholder="username"></div>'
+'<div class="form-group"><label>Password</label><input type="password" id="nuPass" class="form-input" placeholder="password"></div>'
+'<div class="form-group"><label>Host</label><input type="text" id="nuHost" class="form-input" value="localhost" placeholder="localhost"></div></div></div></div>'
+'<div class="dashboard-card"><div class="card-header"><h2><i class="fas fa-database"></i> Database Access</h2></div><div class="card-body">'
+'<div style="margin-bottom:0.75rem"><label class="checkbox-label"><input type="checkbox" id="nuAllDb" onchange="document.querySelectorAll(\'.nuDbCheck\').forEach(c=>c.checked=this.checked)"> <strong>Select all</strong></label></div>';
<?php foreach ($databases as $d): ?>
var dbname = '<?= htmlspecialchars($d['name']) ?>';
if (dbname !== 'information_schema' && dbname !== 'performance_schema' && dbname !== 'mysql' && dbname !== 'sys') {
html+='<label class="checkbox-label" style="margin-top:0.25rem"><input type="checkbox" class="nuDbCheck" value="'+dbname+'"> <span>'+dbname+'</span></label>';
}
<?php endforeach; ?>
html+='</div></div></div>'
+'<div class="dashboard-card"><div class="card-header"><h2><i class="fas fa-lock"></i> Privileges</h2></div><div class="card-body">'
+'<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.25rem">';
['ALL PRIVILEGES','SELECT','INSERT','UPDATE','DELETE','CREATE','DROP','ALTER','INDEX','CREATE VIEW','SHOW VIEW','CREATE ROUTINE','ALTER ROUTINE','EXECUTE','LOCK TABLES','REFERENCES','EVENT','TRIGGER'].forEach(function(p){
var checked = p === 'ALL PRIVILEGES' ? ' checked' : '';
html+='<label class="checkbox-label" style="font-size:0.82rem"><input type="checkbox" class="nuPriv" value="'+p+'"'+checked+'> <span>'+p+'</span></label>';
});
html+='</div></div></div>'
+'<div style="margin-top:1rem;display:flex;gap:0.5rem">'
+'<button class="btn btn-primary" onclick="doCreateUser()"><i class="fas fa-save"></i> Create User</button>'
+'<button class="btn btn-secondary" onclick="loadUsers()">Cancel</button></div>';
panel.innerHTML=html;
}
function doCreateUser() {
const user=document.getElementById('nuUser').value;
const pass=document.getElementById('nuPass').value;
const host=document.getElementById('nuHost').value||'localhost';
if(!user||!pass){showToast('error','Username and password required');return;}
const privs=[];
document.querySelectorAll('.nuPriv:checked').forEach(cb=>privs.push(cb.value));
const grantPrivs = privs.includes('ALL PRIVILEGES') ? 'ALL PRIVILEGES' : privs.join(',');
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body: 'action=create_user&new_user='+encodeURIComponent(user)+'&new_pass='+encodeURIComponent(pass)+'&new_host='+encodeURIComponent(host)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
.then(r=>r.json()).then(d=>{
showToast(d.success?'success':'error',d.message);
if(d.success){
const dbs=[];
document.querySelectorAll('.nuDbCheck:checked').forEach(cb=>dbs.push(cb.value));
if(dbs.length>0){
let cnt=0;
dbs.forEach(db=>{
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body:'action=grant&grant_user='+encodeURIComponent(user)+'&grant_db='+encodeURIComponent(db)+'&grant_host='+encodeURIComponent(host)+'&grant_privs='+encodeURIComponent(grantPrivs)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
.then(r2=>r2.json()).then(g=>{
cnt++;
if(cnt===dbs.length)loadUsers();
});
});
}else{loadUsers();}
}
});
}
function showEditUser(user, host) {
const panel = document.getElementById('pmaUsers');
panel.innerHTML='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-key"></i> Change Password</h2>'
+'<code>'+escHtml(user)+'</code> @ <code>'+escHtml(host)+'</code>'
+'<span style="flex:1"></span>'
+'<button class="btn btn-sm btn-secondary" onclick="loadUsers()"><i class="fas fa-arrow-left"></i> Back</button></div>'
+'<div class="card"><div class="card-body">'
+'<div class="form-group"><label>New Password</label><input type="password" id="epPass" class="form-input" placeholder="new password"></div>'
+'<div style="margin-top:1rem;display:flex;gap:0.5rem">'
+'<button class="btn btn-primary" onclick="doChangePass(\''+escHtml(user)+"','"+escHtml(host)+'\')"><i class="fas fa-save"></i> Save</button>'
+'<button class="btn btn-secondary" onclick="loadUsers()">Cancel</button></div></div></div>';
}
function doChangePass(user, host) {
const pass = document.getElementById('epPass').value;
if (!pass) { showToast('error','Password required'); return; }
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
body:'action=change_pass&target_user='+encodeURIComponent(user)+'&target_host='+encodeURIComponent(host)+'&new_pass='+encodeURIComponent(pass)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
.then(r=>r.json()).then(d=>{showToast(d.success?'success':'error',d.message);if(d.success)loadUsers()});
}
function deleteUser(user, host) {
if (!confirm('Delete user \''+user+'\'@\''+host+'\'?\nThis cannot be undone.')) return;
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
body:'action=drop_user&target_user='+encodeURIComponent(user)+'&target_host='+encodeURIComponent(host)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
.then(r=>r.json()).then(d=>{showToast(d.success?'success':'error',d.message);if(d.success)loadUsers()});
}
function runSql() {
const sql=document.getElementById('sqlInput').value; if(!sql)return;
const rd=document.getElementById('sqlResult'); const rt=document.getElementById('sqlResultText');
rd.style.display='none';
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body:'action=query&query='+encodeURIComponent(sql)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
.then(r=>r.json()).then(d=>{if(d.output){rt.textContent=d.output;rd.style.display='block'}else showToast('error',d.message||'No results')});
}
function showPanel(id) {
document.querySelectorAll('.pma-panel').forEach(p=>p.classList.remove('active'));
document.getElementById('pma'+id.charAt(0).toUpperCase()+id.slice(1)).classList.add('active');
}
function switchTab(tab) {
document.querySelectorAll('.pma-tab').forEach(t=>t.classList.remove('active'));
const map={browse:'pmaBrowse',structure:'pmaStructure',sql:'pmaSql',users:'pmaUsers'};
const el=document.getElementById('tab'+tab.charAt(0).toUpperCase()+tab.slice(1));
if(el)el.classList.add('active');
showPanel(tab);
if(tab==='sql'){document.getElementById('pmaWelcome').classList.remove('active');document.getElementById('pmaSql').classList.add('active');}
}
document.getElementById('sqlInput').addEventListener('keydown',function(e){if((e.ctrlKey||e.metaKey)&&e.key==='Enter')runSql()});
function escHtml(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
</script>
<?php endif; ?>