Add root terminal

This commit is contained in:
Peter Stockings
2025-12-24 11:03:44 +11:00
parent 0c89a0f745
commit 4f34696c72
2 changed files with 91 additions and 1 deletions

22
app.py
View File

@@ -526,3 +526,25 @@ def api_sql_query():
result = run_sql_query(container, query)
return jsonify(result)
# API endpoint for shell commands
@app.post("/api/terminal/exec")
@login_required
def api_terminal_exec():
data = request.json
command = data.get("command")
if not command:
return jsonify({"error": "No command provided"}), 400
try:
# Use shell execution via sh()
# sh() uses subprocess.check_output with shell=False by default (list of strings)
# However, to support pipe/redirection for the user, we should allow shell=True-like behavior
# Let's wrap it in ['sh', '-c', command]
output = sh(["sh", "-c", command])
return jsonify({"output": output, "status": "success"})
except subprocess.CalledProcessError as e:
return jsonify({"output": e.output or str(e), "error": True, "status": "error"})
except Exception as e:
return jsonify({"output": str(e), "error": True, "status": "error"})

View File

@@ -591,6 +591,33 @@
</div>
</div>
</section>
<!-- Root Terminal -->
<section id="root-terminal" class="station" data-label="[ STATION: ROOT_TERMINAL ]">
<div class="station-header">
<h2>Server Shell Access</h2>
<a href="#top" class="back-to-top">[^ BACK_TO_TOP]</a>
</div>
<div class="panel terminal-box" style="margin-top: 0; border-color: #ff555566;">
<div class="terminal-header" style="border-bottom-color: #ff555566;">
<div class="terminal-label" style="color: #ff5555;">ROOT@DOKKU:~$</div>
<button class="copy-btn" onclick="copyLogs('terminal-output')"
style="border-color: #ff555566; color: #ff5555;">COPY BUFFER</button>
</div>
<div id="terminal-output" class="terminal-body"
style="height: 400px; font-size: 13px; background: #000;">
<div class="log-line log-info">Dokku Terminal initialized. Ready for commands.</div>
</div>
<div style="display: flex; background: #000; padding: 10px; border-top: 1px solid #ff555566;">
<span style="color: #00ff88; font-weight: 700; margin-right: 10px;">$</span>
<input type="text" id="terminal-input" placeholder="Enter command..." spellcheck="false"
autocomplete="off"
style="background: transparent; color: #c9d1d9; border: none; outline: none; flex: 1; font-family: inherit; font-size: 13px;"
onkeydown="if(event.key === 'Enter') runCommand()">
</div>
</div>
</section>
</div>
<script>
@@ -598,7 +625,7 @@
const el = document.getElementById(id);
const text = el.innerText;
navigator.clipboard.writeText(text).then(() => {
const btn = event.target;
const btn = event.currentTarget || event.target;
const orig = btn.innerText;
btn.innerText = 'COPIED';
setTimeout(() => { btn.innerText = orig; }, 2000);
@@ -675,6 +702,47 @@
}
}
async function runCommand() {
const input = document.getElementById('terminal-input');
const output = document.getElementById('terminal-output');
const command = input.value.trim();
if (!command) return;
// Add command to terminal
const cmdLine = document.createElement('div');
cmdLine.className = 'log-line';
cmdLine.innerHTML = `<span style="color: #00ff88;">$ ${command}</span>`;
output.appendChild(cmdLine);
input.value = '';
output.scrollTop = output.scrollHeight;
try {
const response = await fetch('/api/terminal/exec', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command })
});
const result = await response.json();
const resultLine = document.createElement('div');
resultLine.className = result.error ? 'log-line log-error' : 'log-line';
resultLine.style.whiteSpace = 'pre-wrap';
resultLine.innerText = result.output;
output.appendChild(resultLine);
output.scrollTop = output.scrollHeight;
} catch (err) {
const errLine = document.createElement('div');
errLine.className = 'log-line log-error';
errLine.innerText = `FATAL ERROR: ${err.message}`;
output.appendChild(errLine);
output.scrollTop = output.scrollHeight;
}
}
// Auto-scroll logic
document.querySelectorAll('.terminal-body').forEach(el => {
el.scrollTop = el.scrollHeight;