mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
536 lines
20 KiB
HTML
536 lines
20 KiB
HTML
{% extends 'base.html' %}
|
|
|
|
{% block content %}
|
|
|
|
{% if domains %}
|
|
|
|
<style>
|
|
|
|
@keyframes rotate {
|
|
to {
|
|
--angle: 360deg
|
|
}
|
|
}
|
|
|
|
@property --angle {
|
|
syntax: "<angle>";
|
|
initial-value: 0deg;
|
|
inherits: false
|
|
}
|
|
|
|
.CrawlerStatusCard.active {
|
|
animation: rotate 2s linear infinite;
|
|
background: hsla(0,0%,100%,.5);
|
|
border: 2px solid transparent;
|
|
border-image: conic-gradient(from var(--angle),transparent 0deg 90deg,transparent 90deg 180deg,transparent 180deg 270deg,#0077bc 270deg 1turn) 1 stretch;
|
|
}
|
|
</style>
|
|
|
|
|
|
<div class="modal fade" id="viewModal" tabindex="-1" role="dialog" aria-labelledby="viewModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="viewModalLabel">{{ _('Application Logs') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>
|
|
</div>
|
|
<div class="modal-body bg-dark">
|
|
<div id="fileContent" class="mb-0"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
$("#port").on('input', function() {
|
|
// Get the user-input port value
|
|
var portValue = $(this).val();
|
|
|
|
// Make an AJAX request to check if the port is in use
|
|
$.ajax({
|
|
url: "/json/check_if_port_is_in_use",
|
|
type: "POST",
|
|
data: {port: portValue},
|
|
success: function(data) {
|
|
// Display the appropriate symbol and message based on the response
|
|
var resultDiv = $("#port-check-result");
|
|
if (data.message.includes("not in use")) {
|
|
resultDiv.html('<i class="bi bi-check-lg" style="color:green;"></i> {{ _("available") }}');
|
|
} else {
|
|
resultDiv.html('<i class="bi bi-x-lg" style="color:red;"></i> {{ _("already in use") }}');
|
|
}
|
|
},
|
|
error: function(data) {
|
|
// Handle the error here if needed
|
|
console.log("Error:", data);
|
|
}
|
|
});
|
|
});
|
|
$("#startup_file").on('input', function() {
|
|
// Get the user-input file value
|
|
var fileValue = $(this).val();
|
|
|
|
// Make an AJAX request to check if the file is in use
|
|
$.ajax({
|
|
url: "/json/check_if_file_exists",
|
|
type: "POST",
|
|
data: {file: fileValue},
|
|
success: function(data) {
|
|
// Display the appropriate symbol and message based on the response
|
|
var resultDiv = $("#file-check-result");
|
|
if (data.message.includes("exists")) {
|
|
resultDiv.html('<i class="bi bi-check-lg" style="color:green;"></i> {{ _("file exists") }}');
|
|
} else {
|
|
resultDiv.html('<i class="bi bi-x-lg" style="color:red;"></i> {{ _("file does not exist!") }}');
|
|
}
|
|
},
|
|
error: function(data) {
|
|
// Handle the error here if needed
|
|
console.log("Error:", data);
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
|
|
});
|
|
</script>
|
|
|
|
|
|
<div class="collapse" id="collapseExample">
|
|
<div id="toAddActive" class="card CrawlerStatusCard card-body">
|
|
<!-- Form for adding new applications -->
|
|
<div class="container">
|
|
<a href="#" class="nije-link" id="cancelLink" style="display: none;"><i class="bi bi-x-lg" style="right: 15px;top: 15px;position: absolute;color: black;padding: 6px 10px;border-radius: 50px;"></i></a>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 offset-md-3">
|
|
<h2 class="mb-3"><i class="bi bi-app-indicator"></i> {{ _('Create a new Application') }}</h2>
|
|
<p>{{ _("Run a NodeJS or Python application.") }}</p>
|
|
<form method="post" id="installpm2" action="/pm2/install">
|
|
|
|
<div class="form-group mb-2">
|
|
<label for="domain_id">{{ _('Application URL:') }}</label>
|
|
<div class="input-group">
|
|
<select class="form-control" name="domain_id" id="domain_id">
|
|
{% for domain in domains %}
|
|
<option class="punycode" value="{{ domain.domain_id }}">{{ domain.domain_url }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<div class="input-group-append">
|
|
<input type="text" class="form-control" name="subdirectory" id="subdirectory" placeholder="{{ _('subfolder (optional)') }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="admin_email">{{ _('Application Startup file:') }}</label>
|
|
<div class="input-group mb-0">
|
|
|
|
<div class="input-group">
|
|
<div class="input-group-prepend">
|
|
<span class="input-group-text" id="full-path">/home/{{ current_username }}/</span>
|
|
</div>
|
|
<input type="text" class="form-control" name="startup_file" id="startup_file" required="">
|
|
</div>
|
|
<div id="file-check-result"> </div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="flags">{{ _('Optional flags:') }}</label>
|
|
<div class="input-group mb-0">
|
|
<input type="text" class="form-control" name="flags" id="flags">
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="form-group row mt-2">
|
|
<div class="col-md-6">
|
|
<label for="app_type">{{ _('Type:') }}</label>
|
|
<select class="form-select select2" name="app_type" id="app_type">
|
|
<option value="node">{{ _("NodeJS") }}</option>
|
|
<option value="python3">{{ _("Python") }}</option>
|
|
</select>
|
|
</div>
|
|
<!--div class="col-md-6">
|
|
<label for="version">Version:</label>
|
|
<input type="text" class="form-control" name="version" id="version" value="">
|
|
</div-->
|
|
|
|
<div class="col-md-6">
|
|
<label for="website_name">{{ _("Port:") }}</label>
|
|
<input type="text" class="form-control" name="port" id="port" value="" required>
|
|
<div id="port-check-result"> </div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group row mt-2 mb-2">
|
|
<div class="col-md-6">
|
|
<label for="watch">{{ _('Watch:') }}</label>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked" checked>
|
|
<label class="form-check-label" for="flexSwitchCheckChecked">{{ _('Automatically restart app on file changes') }}</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="logs">{{ _('Enable logs:') }}</label>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked" checked>
|
|
<label class="form-check-label" for="flexSwitchCheckChecked">{{ _('Collect logs') }}</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Check if the URL contains the parameter "install"
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const installParam = urlParams.get('install');
|
|
|
|
if (installParam || window.location.hash === '#install') {
|
|
// Show the Bootstrap collapsible element
|
|
$("#collapseExample").collapse('show');
|
|
}
|
|
|
|
// Add event listener to the "Install website" button to toggle form and jumbotron
|
|
$("#collapseExample").on('shown.bs.collapse', function () {
|
|
$("#jumbotronSection").hide();
|
|
$("#cancelLink").show();
|
|
});
|
|
|
|
// Add event listener to the "Cancel" link to toggle form and jumbotron
|
|
$("#cancelLink").click(function() {
|
|
$("#collapseExample").collapse('hide');
|
|
$("#jumbotronSection").show();
|
|
$("#cancelLink").hide();
|
|
});
|
|
});
|
|
</script>
|
|
<br>
|
|
<button type="submit" class="btn btn-primary" id="installpm2Button">{{ _('Create') }}</button>
|
|
</form>
|
|
<div id="statusMessage"></div>
|
|
<script>
|
|
document.getElementById("installpm2").addEventListener("submit", function(event) {
|
|
event.preventDefault();
|
|
|
|
// Hide the installation form
|
|
const installForm = document.getElementById("installpm2");
|
|
installForm.style.display = "none"; // Hide the form
|
|
|
|
// Show the active state
|
|
document.getElementById("toAddActive").classList.add("active");
|
|
|
|
// Disable the button to prevent multiple clicks
|
|
const button = document.getElementById("installpm2Button");
|
|
button.disabled = true;
|
|
button.innerText = "Installing..."; // Initial message
|
|
|
|
// Clear previous status messages
|
|
const statusMessageDiv = document.getElementById("statusMessage");
|
|
statusMessageDiv.innerText = "";
|
|
|
|
// Start fetching for updates
|
|
const formData = new FormData(this); // Get the form data
|
|
const responseStream = fetch('/pm2/install', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
responseStream
|
|
.then(response => {
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder('utf-8');
|
|
|
|
function read() {
|
|
reader.read().then(({ done, value }) => {
|
|
if (done) {
|
|
showCompletion();
|
|
return;
|
|
}
|
|
const message = decoder.decode(value, { stream: true });
|
|
const jsonMessages = message.split('\n').filter(Boolean); // Split lines and remove empty
|
|
|
|
jsonMessages.forEach(jsonMessage => {
|
|
try {
|
|
const data = JSON.parse(jsonMessage);
|
|
if (data.status) {
|
|
statusMessageDiv.innerText += data.status + "\n"; // append
|
|
} else if (data.error) {
|
|
toaster({
|
|
body: data.error,
|
|
className: 'border-0 text-white bg-danger',
|
|
});
|
|
showCompletion(); // redirect
|
|
}
|
|
} catch (e) {
|
|
//console.error("Error parsing JSON:", e);
|
|
}
|
|
});
|
|
read(); // Continue
|
|
});
|
|
}
|
|
|
|
read(); // Start
|
|
})
|
|
.catch(error => {
|
|
statusMessageDiv.innerText += "Error occurred while processing.\n";
|
|
|
|
console.error("Fetch error:", error);
|
|
toaster({
|
|
body: error,
|
|
className: 'border-0 text-white bg-danger',
|
|
});
|
|
showCompletion(); // redirect
|
|
|
|
});
|
|
|
|
function showCompletion() {
|
|
statusMessageDiv.style.display = "block";
|
|
window.location.href = '/pm2';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div></div>
|
|
{% if pm2_data %}
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<!--th>ID</th-->
|
|
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Application / Domain Name.') }}">{{ _('Application') }}</span></th>
|
|
<!--th><span data-bs-toggle="tooltip" data-bs-placement="top" title="NodeJS/Python version the application is using.">Version</span></th-->
|
|
<th>{{ _('Uptime') }}</th>
|
|
<th>{{ _('Restarts') }}</th>
|
|
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Current status of the application process.') }}">{{ _('Status') }}</span></th>
|
|
<th>{{ _('CPU') }}</th>
|
|
<th>{{ _('Memory') }}</th>
|
|
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('If watching is enabled, the application will automatically restart when its files are edited.') }}">{{ _('Watching') }}</span></th>
|
|
<th>Actions</th>
|
|
<!-- New columns for data from the database -->
|
|
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="NodeJS/Python">{{ _('Type') }}</span></th>
|
|
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Port that the application is using.') }}">Port</span></th>
|
|
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Path to the application startup file (app.js)') }}"> {{ _('Startup file') }}</span></th>
|
|
|
|
|
|
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% if pm2_data %}
|
|
{% for row in pm2_data %}
|
|
<tr>
|
|
<!--td>{{ row[0] }}</td-->
|
|
<td>{{ row[1] }}</td>
|
|
<!--td>{{ row[3] }}</tdVERSION-->
|
|
<td>{{ row[6] }}</td>
|
|
<td>{{ row[7] }}</td>
|
|
<td class="{% if row[8] == 'stopped' %}text-danger{% elif row[8] == 'online' %}text-success{% else %}text-warning{% endif %}">
|
|
{{ row[8] }}
|
|
</td>
|
|
<td>{{ row[9] }}</td>
|
|
<td>{{ row[10] }}</td>
|
|
<td>{{ row[12] }}</td>
|
|
|
|
<td>
|
|
{% if row[8] == 'stopped' %}
|
|
<form action="/pm2/start/{{ row[1] }}" method="post" style="display: inline;">
|
|
<button type="submit" class="btn btn-success btn-sm"><i class="bi bi-play-fill"></i> {{ _('Start') }}</button>
|
|
</form>
|
|
{% else %}
|
|
<form action="/pm2/stop/{{ row[1] }}" method="post" style="display: inline;">
|
|
<button type="submit" class="btn btn-warning btn-sm"><i class="bi bi-stop-fill"></i> {{ _('Stop') }}</button>
|
|
</form>
|
|
{% endif %}
|
|
|
|
<form action="/pm2/restart/{{ row[1] }}" method="post" style="display: inline;">
|
|
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-arrow-clockwise"></i> {{ _('Restart') }}</button>
|
|
</form>
|
|
|
|
<form action="/pm2/delete/{{ row[1] }}" method="post" style="display: inline;">
|
|
<button class="btn btn-danger btn-sm" type="button" style="display: inline;" onclick="confirmAppDelete(this);"><i class="bi bi-trash3"></i> {{ _('Delete') }}</button>
|
|
</form>
|
|
|
|
<button class="btn btn-sm btn-transparent view-button" data-file="{{ row[1] }}" data-type="logs">{{ _('Logs') }}</button>
|
|
|
|
</td>
|
|
|
|
|
|
<!-- Data from the database for the corresponding site -->
|
|
{% for site in all_sites %}
|
|
{% if row[1] == site.site_name %}
|
|
|
|
<td>
|
|
{% if site.type == "NodeJS" %}
|
|
<i class=""><img src="/static/images/icons/nodejs.png" style="height: 25px;"></i>
|
|
{% elif site.type == "Python" %}
|
|
<i class=""><img src="/static/images/icons/python.png" style="height: 25px;"></i>
|
|
{% else %}
|
|
{{ site.type }}
|
|
{% endif %}
|
|
</td>
|
|
|
|
<td>{{ site.ports }}</td>
|
|
<td>{{ site.path }}</td>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
</tr>
|
|
{% endfor %}
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="10">{{ _('No PM2 data available.') }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
|
|
{% else %}
|
|
<!-- Display jumbotron for no installations -->
|
|
<div class="jumbotron text-center mt-5" id="jumbotronSection">
|
|
<h1>{{ _('No existing Applications') }}</h1>
|
|
<p>{{ _('There are no existing applications. You can start a new NodeJS or Python app below.') }}</p>
|
|
<button class="btn btn-lg btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
|
|
<i class="bi bi-app-indicator"></i>{{ _(' Create Application') }}
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/punycode/2.1.1/punycode.min.js"></script>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
var punycodeElements = document.querySelectorAll(".punycode");
|
|
punycodeElements.forEach(function(element) {
|
|
element.textContent = punycode.toUnicode(element.textContent);
|
|
});
|
|
});
|
|
</script>
|
|
|
|
|
|
|
|
</section>
|
|
<footer class="main-footer btn-toolbar" role="toolbar">
|
|
|
|
<div class="btn-group" role="group" aria-label="Status">
|
|
</div>
|
|
|
|
<div class="ms-auto" role="group" aria-label="Actions">
|
|
|
|
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
|
|
<i class="bi bi-plus-lg"></i> {{ _('New') }}<span class="desktop-only"> {{ _('Application') }}</span>
|
|
</button>
|
|
</div>
|
|
</footer>
|
|
|
|
|
|
<script>
|
|
|
|
function confirmAppDelete(button) {
|
|
var countdown = 5;
|
|
var countdownActive = true; // Variable to track countdown status
|
|
|
|
// Change the button style and text
|
|
$(button).removeClass('btn-danger').addClass('btn-dark').html('<i class="bi bi-trash3-fill"></i> Confirm <span class="btn-indicator btn-indicator-mini bg-danger">' + countdown + '</span>');
|
|
|
|
// Interval to update countdown
|
|
var intervalId = setInterval(function () {
|
|
countdown--;
|
|
|
|
// Update the countdown value in the button text
|
|
$(button).find('.btn-indicator-mini').text(countdown);
|
|
|
|
// Remove the onclick event to prevent further changes on subsequent clicks
|
|
$(button).removeAttr('onclick');
|
|
|
|
// If countdown reaches 0, revert the button, clear the interval, and set countdownActive to false
|
|
if (countdown === 0) {
|
|
clearInterval(intervalId);
|
|
revertButton(button);
|
|
countdownActive = false;
|
|
}
|
|
}, 1000);
|
|
|
|
// Add a click event to the confirm button
|
|
$(button).on('click', function () {
|
|
// Check if countdown is active before allowing form submission
|
|
if (countdownActive) {
|
|
// Submit the parent form when the button is clicked during the countdown
|
|
$(button).closest('form').submit();
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// Function to revert the button to its initial state
|
|
function revertButton(button) {
|
|
$(button).removeClass('btn-dark').addClass('btn-danger').html('<i class="bi bi-trash3"></i> Delete');
|
|
$(button).attr('onclick', 'confirmAppDelete(this);');
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<script>
|
|
$('html').on('click', '.view-button', function() {
|
|
const button = $(this);
|
|
const domain = button.data('file');
|
|
|
|
fetch(`/pm2/logs/${encodeURIComponent(domain)}`)
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
const modalTitle = $('#viewModalLabel');
|
|
const modalBody = $('#viewModal').find('.modal-body');
|
|
|
|
modalTitle.text("Error log for application: " + domain);
|
|
modalBody.empty();
|
|
|
|
const textContent = document.createElement('pre');
|
|
textContent.textContent = data;
|
|
modalBody.append(textContent);
|
|
|
|
$('#viewModal').modal('show');
|
|
})
|
|
.catch(error => {
|
|
console.error('{{ _("Error fetching pm2 logs for applicaiton:") }}', error);
|
|
});
|
|
});
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
{% else %}
|
|
<!-- Display jumbotron for no domains -->
|
|
<div class="jumbotron text-center mt-5" id="jumbotronSection">
|
|
<h1>{{ _("No Domains") }}</h1>
|
|
<p>{{ _("Add a domain name first in order to install Python adn NodeJS applications.") }}</p>
|
|
<a href="/domains#add-new" class="btn btn-lg btn-primary">
|
|
<i class="bi bi-plus-lg"></i> {{ _("Add a Domain Name") }}
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
{% endblock %}
|