Add root terminal
This commit is contained in:
22
app.py
22
app.py
@@ -526,3 +526,25 @@ def api_sql_query():
|
|||||||
|
|
||||||
result = run_sql_query(container, query)
|
result = run_sql_query(container, query)
|
||||||
return jsonify(result)
|
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"})
|
||||||
|
|||||||
@@ -591,6 +591,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -598,7 +625,7 @@
|
|||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
const text = el.innerText;
|
const text = el.innerText;
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
const btn = event.target;
|
const btn = event.currentTarget || event.target;
|
||||||
const orig = btn.innerText;
|
const orig = btn.innerText;
|
||||||
btn.innerText = 'COPIED';
|
btn.innerText = 'COPIED';
|
||||||
setTimeout(() => { btn.innerText = orig; }, 2000);
|
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
|
// Auto-scroll logic
|
||||||
document.querySelectorAll('.terminal-body').forEach(el => {
|
document.querySelectorAll('.terminal-body').forEach(el => {
|
||||||
el.scrollTop = el.scrollHeight;
|
el.scrollTop = el.scrollHeight;
|
||||||
|
|||||||
Reference in New Issue
Block a user