monitor/web/standalone/deployer.ejs
2025-04-16 22:30:27 +07:00

587 lines
33 KiB
Plaintext

<!DOCTYPE html>
<html lang="en">
<head>
<base href="<%= basePath %>">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="description" content="txAdmin - remotely Manage & Monitor your GTA5 FiveM Server">
<meta name="author" content="André Tabarra">
<title>Server Deployer</title>
<link href="css/simple-line-icons.css?txVer=<%= txAdminVersion %>" rel="stylesheet">
<link href="css/coreui.min.css?txVer=<%= txAdminVersion %>" rel="stylesheet">
<link href="css/dark.css?txVer=<%= txAdminVersion %>" rel="stylesheet">
<link rel="shortcut icon" type="image/png" href="img/favicon_default.png" id="favicon" />
<link rel="stylesheet" href="css/txAdmin.css?txVer=<%= txAdminVersion %>">
<!-- Page CSS -->
<!-- <link rel="stylesheet" href="css/codemirror.css"> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.61.1/codemirror.min.css"
integrity="sha512-xIf9AdJauwKIVtrVRZ0i4nHP61Ogx9fSRAkCLecmE2dL/U8ioWpDvFCAy4dcfecN72HHB9+7FfQj3aiO68aaaw=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="css/codemirror_lucario.css?txVer=<%= txAdminVersion %>">
<style>
.inline-code{
color: #321fdb!important;
}
body.theme--dark .inline-code {
color: #00bf8f !important;
}
.cm-s-lucario{
font-size: 1.05rem !important;
}
.deployer-stepper > .card-header,
.deployer-stepper > .card-footer{
font-size: large;
}
#step3Log {
white-space: break-spaces;
word-break: break-all;
}
</style>
<!-- injected consts -->
<script><%- jsInjection %></script>
</head>
<body class="c-app flex-row <%= uiTheme %>">
<div class="container">
<!-- Stepper Row -->
<div class="row justify-content-center">
<div class="col-12">
<div class="card fade-in deployer-stepper">
<% if (step === 'review') { %>
<style>
.CodeMirror{
max-height: 300px;
}
</style>
<div class="card-header font-weight-bold">Step 1: Review Recipe</div>
<div class="card-body">
<div class="row mb-1 mt-0 text-center">
<div class="col-8 mx-auto card d-block border-primary p-2 mb-1">
Please review the Recipe below and apply any changes you want, <br>
then press the <strong>Run Recipe</strong> button below.
<% if (!recipe.isTrustedSource) { %>
<br><span class="text-danger font-weight-bold">Warning: Only run Recipes from trusted sources!</span>
<% } %>
</div>
</div>
<p class="mb-1">
<strong style="word-wrap: break-word;"><%= recipe.name %></strong>
<% if (recipe.author !== '') { %>
by <%= recipe.author %>
<% } %>
<% if (recipe.description !== '') { %>
<br>
<%= recipe.description %>
<% } %>
</p>
<textarea
id="codeMirrorTarget"
style="width: 100%;"
class="cms-s-lucario"
name="code"
>
<%= recipe.raw %>
</textarea>
<div class="row justify-content-center m-2">
<button class="btn btn-outline-danger" type="button"
onclick="cancelAction()">Cancel and Return to Setup</button>
&nbsp; &nbsp;
<button class="btn btn-info" type="button"
onclick="step1Action();" autofocus>Next</button>
</div>
</div>
<div class="card-footer text-muted">Step 2: Input Parameters</div>
<div class="card-footer text-muted">Step 3: Run Recipe</div>
<div class="card-footer text-muted">Step 4: Configure server.cfg</div>
<% } else if (step === 'input') { %>
<div class="card-header">Step 1: Review Recipe ✔️</div>
<div class="card-header font-weight-bold ">Step 2: Input Parameters</div>
<div class="card-body">
<% if (defaults.autofilled) { %>
<div class="row mb-3 mt-0 text-center">
<div class="col-8 mx-auto card d-block border-warning p-2 mb-1">
<strong>Note:</strong> The following configs were auto-filled by <strong><%= hostConfigSource %></strong>. <br>
You <i>may</i> edit those, but it's strongly disencouraged.
</div>
</div>
<% } %>
<form id="step2-form" style="max-width: 732px; margin-left: auto; margin-right: auto;">
<div class="form-group row">
<label for="step2-svLicense" class="col-md-3 col-form-label">
Server Registration Key
<span class="text-danger">*</span>
</label>
<div class="col-md-9">
<input type="text" class="form-control blur-input" id="step2-svLicense" maxlength="86"
autocomplete="off" placeholder="cfxk_xxxxxxxxxxxxxxxxxxxx_xxxxx"
value="<%= defaults.license %>" required autofocus>
<span class="form-text text-muted">
Formely known as <strong>License Key</strong>, it can be obtained in the <a href="https://portal.cfx.re/servers/registration-keys" target="_blank" rel="noopener noreferrer">Cfx.re Portal</a>. <br>
For more info, check the guide: <a href="https://support.cfx.re/hc/en-us/articles/16539369935900-How-to-create-a-registration-key" target="_blank" rel="noopener noreferrer">How to create a registration key</a>.
</span>
</div>
</div>
<% if (requireDBConfig) { %>
<div class="row justify-content-center m-2">
<a class="btn btn-small btn-outline-secondary" data-toggle="collapse" href="#collapseDbOptions" aria-expanded="true" aria-controls="collapseDbOptions">
Show/Hide Database options (advanced)
</a>
</div>
<div class="collapse" id="collapseDbOptions">
<div class="form-group row">
<label for="step2-dbHost" class="col-md-3 col-form-label">
Database Host
<span class="text-danger">*</span>
</label>
<div class="col-md-9">
<input type="text" class="form-control" id="step2-dbHost"
placeholder="<%= defaults.mysqlHost %>" value="<%= defaults.mysqlHost %>" required>
<span class="form-text text-muted">
The IP/Hostname for the database server (usually <code><%= defaults.mysqlHost %></code>).
</span>
</div>
</div>
<div class="form-group row">
<label for="step2-dbPort" class="col-md-3 col-form-label">
Database Port
<span class="text-danger">*</span>
</label>
<div class="col-md-9">
<input type="number" class="form-control" id="step2-dbPort"
placeholder="<%= defaults.mysqlPort %>" value="<%= defaults.mysqlPort %>" required>
<span class="form-text text-muted">
The port for the database server (usually <code><%= defaults.mysqlPort %></code>).
</span>
</div>
</div>
<div class="form-group row">
<label for="step2-dbUsername" class="col-md-3 col-form-label">
Database Username
<span class="text-danger">*</span>
</label>
<div class="col-md-9">
<input type="text" class="form-control" id="step2-dbUsername"
placeholder="<%= defaults.mysqlUser %>" value="<%= defaults.mysqlUser %>" required>
<span class="form-text text-muted">
The database username (usually <code>root</code>).
</span>
</div>
</div>
<div class="form-group row">
<label for="step2-dbPassword" class="col-md-3 col-form-label">
Database Password
</label>
<div class="col-md-9">
<input type="text" class="form-control blur-input" id="step2-dbPassword"
autocomplete="off" placeholder="leave it blank" value="<%= defaults.mysqlPassword %>">
<span class="form-text text-muted">
The database password (usually blank).
</span>
</div>
</div>
<div class="form-group row">
<label for="step2-dbName" class="col-md-3 col-form-label">
Database Name
</label>
<div class="col-md-9">
<input type="text" class="form-control" id="step2-dbName"
placeholder="<%= defaults.mysqlDatabase %>" value="<%= defaults.mysqlDatabase %>">
<span class="form-text text-muted">
The name of the database to be used or created. <br>
If left empty, the deployment ID (<code><%= deploymentID %></code>) will be used instead.
</span>
</div>
</div>
<div class="form-group row">
<label for="step2-dbDelete" class="col-md-3 col-form-label">
Delete Database
</label>
<div class="col-md-9">
<label class="c-switch c-switch-label c-switch-pill c-switch-success fix-pill-form">
<input class="c-switch-input" type="checkbox" id="step2-dbDelete" checked>
<span class="c-switch-slider" data-checked="On" data-unchecked="Off"></span>
</label>
<span class="form-text text-muted">
If already exists, automatically deletes the database with the name provided above. <br>
<strong>Warning:</strong> all data will be lost.
</span>
</div>
</div>
</div>
<% } %>
<% if (inputVars.length) { %>
<div class="hrsep hrsep-small mb-3">Custom Variables</div>
<% for (const [key, inputVar] of inputVars.entries()) { %>
<div class="form-group row">
<label for="step2-customVar<%= key %>" class="col-md-3 col-form-label">
<%= inputVar.name %>
</label>
<div class="col-md-9">
<input type="text" class="form-control customRecipeVars" id="step2-customVar<%= key %>"
data-varName="<%= inputVar.name %>" placeholder="<%= inputVar.value %>" value="<%= inputVar.value %>">
<% if (inputVar.description) { %>
<span class="form-text text-muted">
<%- inputVar.description %>
</span>
<% } %>
</div>
</div>
<% } %>
<% } %>
</form>
<div class="row justify-content-center m-2 mt-3">
<button class="btn btn-outline-danger" type="button"
onclick="cancelAction()">Cancel and Return to Setup</button>
&nbsp; &nbsp;
<button class="btn btn-success" type="button"
onclick="step2Action();">Run Recipe</button>
</div>
</div>
<div class="card-footer text-muted">Step 3: Run Recipe</div>
<div class="card-footer text-muted">Step 4: Configure server.cfg</div>
<% } else if (step === 'run') { %>
<div class="card-header">Step 1: Review Recipe ✔️</div>
<div class="card-header">Step 2: Input Parameters ✔️</div>
<div class="card-header font-weight-bold ">Step 3: Run Recipe</div>
<div class="card-body">
<div class="row mb-1 mt-0 text-center">
<div class="col-8 mx-auto card d-block border-primary p-2 mb-1">
Your recipe is being executed, the server will be deployed to: <br>
<code class="inline-code"><%= deployPath %></code>
</div>
</div>
<div class="row mb-3">
<div class="col-10 mx-auto card d-block p-2 m-0">
<pre id="step3Log" class="text-body"><h3 class="text-center">🐌🐌🐌</h3></pre>
</div>
</div>
<div class="mx-auto col-8">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"
style="width: 0%" id="step3Progress">0%</div>
</div>
<span class="text-danger font-weight-bold d-none" id="step3Error"></span>
</div>
<div class="row justify-content-center m-2">
<button class="btn btn-outline-danger d-none" type="button"
onclick="cancelAction();" id="step3CancelButton">Cancel and Return to Setup</button>
<button type="button" class="btn btn-info d-none"
onclick="window.location.reload();" id="step3NextButton">Next</button>
</div>
</div>
<div class="card-footer text-muted">Step 3: Configure server.cfg</div>
<% } else if (step === 'configure') { %>
<style>
.CodeMirror{
height: auto;
}
</style>
<div class="card-header">Step 1: Review Recipe ✔️</div>
<div class="card-header">Step 2: Input Parameters ✔️</div>
<div class="card-header">Step 3: Run Recipe ✔️</div>
<div class="card-header font-weight-bold">Step 4: Configure server.cfg</div>
<div class="card-body">
<div class="row mb-1 mt-0 text-center">
<div class="col-8 mx-auto card d-block border-primary p-2 mb-1">
Configure your <code class="inline-code">server.cfg</code> file to your liking, <br>
then press the <strong>Save & Run Server</strong> button below.
</div>
</div>
<textarea id="codeMirrorTarget" style="width: 100%;" class="cms-s-lucario" name="code"><%= serverCFG %></textarea>
<div class="row justify-content-center m-2">
<button class="btn btn-outline-danger" type="button" onclick="cancelAction()">Cancel and Return to Setup</button>
&nbsp; &nbsp;
<button class="btn btn-success" type="button" onclick="step4Action();">Save & Run Server</button>
</div>
</div>
<% } else { %>
<div class="card-body text-center">
Something is wrong 🤔
</div>
<% } %>
</div>
</div>
</div>
</div>
<!-- CoreUI and necessary plugins-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"
integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@4.0.16/lib/marked.umd.min.js"></script>
<script src="js/coreui.bundle.min.js?txVer=<%= txAdminVersion %>"></script>
<script src="js/bootstrap-notify.min.js?txVer=<%= txAdminVersion %>"></script>
<script src="js/txadmin/base.js?txVer=<%= txAdminVersion %>"></script>
<!-- JS -->
<!-- <script src="js/codeEditor/codemirror.js?txVer=<%= txAdminVersion %>"></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.61.1/codemirror.min.js"
integrity="sha512-ZTpbCvmiv7Zt4rK0ltotRJVRaSBKFQHQTrwfs6DoYlBYzO1MA6Oz2WguC+LkV8pGiHraYLEpo7Paa+hoVbCfKw=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="js/codeEditor/mode/simple.js?txVer=<%= txAdminVersion %>"></script>
<script src="js/codeEditor/mode/fivem-cfg.js?txVer=<%= txAdminVersion %>"></script>
<script src="js/codeEditor/mode/yaml.js?txVer=<%= txAdminVersion %>"></script>
<script>
//============================================== CodeMirror & Page Setup
const currentStep = '<%= step %>';
const codeMirrorTarget = document.getElementById("codeMirrorTarget");
window.onload = function () {
if(currentStep == 'review' || currentStep == 'configure'){
window.CMInstance = CodeMirror.fromTextArea(codeMirrorTarget, {
mode: (currentStep == 'review')? 'yaml' : 'fivem-cfg',
lineNumbers: true,
lineWrapping: true,
viewportMargin: Infinity,
theme: "lucario" //NOTE: I modified the theme a bit
});
}
};
//============================================== Step1 action
function step1Action(){
const editor = window.CMInstance;
if(editor.getValue().includes('This is just a placeholder')){
return $.notify({ message: `If you are not trying to write a recipe, click on the cancel button below to return to the setup page, where you can select a recommended template. Otherwise, jut remove the placeholder warning comment.` }, { type: 'warning' });
}
editor.setValue(editor.getValue().replace(/\t/g, ' '.repeat(4)));
const recipe = editor.getValue();
if(recipe.length < 256){
return $.notify({ message: `Your Recipe file is too small, there is a good chance you deleted something you shouldn't.` }, { type: 'warning' });
}
const notify = $.notify({ message: '<p class="text-center">Saving...</p>' }, {});
txAdminAPI({
type: "POST",
url: '/deployer/recipe/confirmRecipe',
timeout: REQ_TIMEOUT_LONG,
data: {recipe},
success: function (data) {
if (checkApiLogoutRefresh(data)) return;
if (data.success == true){
window.location.reload(true);
}else{
notify.update('progress', 0);
notify.update('type', data.type);
notify.update('message', data.message);
}
},
error: function (xmlhttprequest, textstatus, message) {
notify.update('progress', 0);
notify.update('type', 'danger');
notify.update('message', message);
}
});
}
//============================================== Step2 action
const requireDBConfig = ('<%= requireDBConfig %>' == 'true');
const step2elements = {
form: document.getElementById('step2-form'),
svLicense: document.getElementById('step2-svLicense'),
}
if(requireDBConfig){
step2elements.dbHost = document.getElementById('step2-dbHost');
step2elements.dbPort = document.getElementById('step2-dbPort');
step2elements.dbUsername = document.getElementById('step2-dbUsername');
step2elements.dbPassword = document.getElementById('step2-dbPassword');
step2elements.dbName = document.getElementById('step2-dbName');
step2elements.dbDelete = document.getElementById('step2-dbDelete');
step2elements.dbName.addEventListener("keyup", () => {
const dbName = step2elements.dbName.value.trim()
step2elements.dbDelete.checked = (dbName == step2elements.dbName.placeholder || !dbName.length);
});
}
if(step2elements.form){
step2elements.form.addEventListener('submit', (e) => {
e.preventDefault();
step2Action();
});
}
function step2Action(){
if(!step2elements.form.reportValidity()) return;
const postData = {
svLicense: step2elements.svLicense.value.trim()
}
if(requireDBConfig){
postData.dbHost = step2elements.dbHost.value.trim();
postData.dbPort = step2elements.dbPort.value.trim();
postData.dbUsername = step2elements.dbUsername.value.trim();
postData.dbPassword = step2elements.dbPassword.value.trim();
postData.dbName = step2elements.dbName.value.trim();
postData.dbDelete = step2elements.dbDelete.checked;
if(!postData.dbName.length){
postData.dbName = step2elements.dbName.placeholder;
}
if(!postData.dbHost || !postData.dbPort || !postData.dbUsername){
return $.notify({ message: `The database host, port and username are required.` }, { type: 'warning' });
}
}
//Getting custom forms:
const customVarInputs = step2elements.form.getElementsByClassName("customRecipeVars");
for(const input of customVarInputs){
if(!input.dataset.varname) continue;
postData[input.dataset.varname] = input.value.trim();
}
const notify = $.notify({ message: '<p class="text-center">Saving...</p>' }, {});
txAdminAPI({
type: "POST",
url: '/deployer/recipe/setVariables',
timeout: REQ_TIMEOUT_LONG,
data: postData,
success: function (data) {
if (checkApiLogoutRefresh(data)) return;
if (data.success == true){
window.location.reload(true);
}else{
notify.update('progress', 0);
notify.update('type', data.type);
notify.update('message', data.message);
}
},
error: function (xmlhttprequest, textstatus, message) {
notify.update('progress', 0);
notify.update('type', 'danger');
notify.update('message', message);
}
});
}
//============================================== Step3 action
const step3elements = {
log: document.getElementById('step3Log'),
progress: document.getElementById('step3Progress'),
error: document.getElementById('step3Error'),
cancelButton: document.getElementById('step3CancelButton'),
nextButton: document.getElementById('step3NextButton'),
}
const refreshStatus = async () => {
txAdminAPI({
type: "GET",
url: '/deployer/status',
timeout: REQ_TIMEOUT_LONG,
success: function (data) {
if (checkApiLogoutRefresh(data)) return;
step3elements.error.classList.add('d-none');
step3elements.log.innerText = data.log;
if(data.status == 'done'){
step3elements.progress.classList.remove('progress-bar-animated', 'progress-bar-striped');
step3elements.progress.classList.add('bg-success');
step3elements.nextButton.classList.remove('d-none');
step3elements.progress.ariaValueNow = 100;
step3elements.progress.style.width = '100%';
step3elements.progress.innerText = 'DONE';
}else if(data.status == 'failed'){
step3elements.progress.classList.remove('progress-bar-animated', 'progress-bar-striped');
step3elements.progress.classList.add('bg-danger');
step3elements.cancelButton.classList.remove('d-none');
step3elements.progress.ariaValueNow = 100;
step3elements.progress.style.width = '100%';
step3elements.progress.innerText = 'FAILED';
}else{
const ariaMin = (data.progress > 5)? data.progress : 5;
step3elements.progress.ariaValueNow = ariaMin;
step3elements.progress.style.width = ariaMin + '%';
step3elements.progress.innerText = data.progress + '%';
step3elements.progress.scrollIntoView();
}
},
error: function (xmlhttprequest, textstatus, message) {
step3elements.error.innerText = `Error: ${textstatus}`;
step3elements.error.classList.remove('d-none');
}
});
}
if(currentStep == 'run') setInterval(refreshStatus, 1000);
//============================================== Step4 action
function step4Action(){
const serverCFG = window.CMInstance.getValue();
if(serverCFG.length < 256){
return $.notify({ message: `Your settings.cfg file is too small, there is a good chance it is not valid.` }, { type: 'warning' });
}
const notify = $.notify({ message: '<p class="text-center">Saving...</p>' }, {});
if(serverCFG.includes(`sv_licenseKey "changeme"`)){
return $.notify({ message: `Please change the <strong>sv_licenseKey</strong>.` }, { type: 'danger' });
}
txAdminAPI({
type: "POST",
url: '/deployer/recipe/commit',
timeout: REQ_TIMEOUT_LONG,
data: {serverCFG},
success: function (data) {
if (checkApiLogoutRefresh(data)) return;
if (data.success == true){
navigateParentTo('/server/console');
}else{
updateMarkdownNotification(data, notify);
}
},
error: function (xmlhttprequest, textstatus, message) {
notify.update('progress', 0);
notify.update('type', 'danger');
notify.update('message', message);
}
});
}
//============================================== cancel action
function cancelAction(){
const notify = $.notify({ message: '<p class="text-center">Cancelling...</p>' }, {});
txAdminAPI({
type: "POST",
url: '/deployer/recipe/cancel',
timeout: REQ_TIMEOUT_LONG,
success: function (data) {
if (checkApiLogoutRefresh(data)) return;
if (data.success == true){
navigateParentTo('/server/setup');
}else{
notify.update('progress', 0);
notify.update('type', data.type);
notify.update('message', data.message);
}
},
error: function (xmlhttprequest, textstatus, message) {
notify.update('progress', 0);
notify.update('type', 'danger');
notify.update('message', message);
}
});
}
</script>
</body>
</html>