Compare commits

...

19 Commits

Author SHA1 Message Date
Jeroen De Meerleer c7aaf102f4
BUGFIX: don't call functions twice 2 weeks ago
Jeroen De Meerleer c354f093c1
BUGFIX: updated dependencies 2 weeks ago
Jeroen De Meerleer e874a94fec
BUGFIX: only triggering event listener once 1 month ago
Jeroen De Meerleer cf9d2373c2
Updated dependencies 2 months ago
Jeroen De Meerleer 8e737d80cb
ENHANCEMENT: added visualisation of triggered run 2 months ago
Jeroen De Meerleer 2a7c2a5ca3
NEW FEATURE: added webhooks 2 months ago
Jeroen De Meerleer 12205ad18e
BUGFIX: reboot jobs stopped after triggering initial command 3 months ago
Jeroen De Meerleer c59a84a34f
BUGFIX: deletetempvar without name argument did not remove tempvars 3 months ago
Jeroen De Meerleer ff7044c567
BUGFIX: styling of timepicker to schedule was incorrect 3 months ago
Jeroen De Meerleer a6ce9afa18
Updated dependencies 3 months ago
Jeroen De Meerleer 45ba3c6ce2
BUGFIX: Scheduled run always started 1 second too late 3 months ago
Jeroen De Meerleer 21f65e2480
Added timed scheduled runs 3 months ago
Jeroen De Meerleer f77d487ab2
UPDATED DEPENDENCIES 3 months ago
Jeroen De Meerleer b933b2bad0
BUGFIX: Memory Limit could be too low when running daemon script 3 months ago
Jeroen De Meerleer 83f995c177
UPDATED CHANGELOG 4 months ago
Jeroen De Meerleer 3cec0bd074
UPDATED DEPENDENCIES 4 months ago
Jeroen De Meerleer 8a5eb92897
UPDATED DEPENDENCIES 4 months ago
Jeroen De Meerleer 4e9e9c8d0e
Updated dependencies 5 months ago
Jeroen De Meerleer c57f061805
Added changelog 5 months ago

@ -0,0 +1,28 @@
# Changelog
## Version 1.2
### New
* Added timed scheduled runs
## Version 1.1
### New
* User settings page. Eliminating the need to call the helpline for changing your password or language
* Version tag in footer
### Changed
* Docker images are following the docker philosophy (2 containers providing 1 functionality each. Although you can still use the fat image)
* Translations can be contributed via [crowdin](https://crowdin.com/project/webcron-management)
* Data migrations are done using doctrine migrations
* User command now has update action
* Mail-failed-runs now takes an argument with recipients. Eliminating the need for a user account
* Docker images are build for amd64, arm and arm64
* Symfony framework has been updated to version 6.2
### Fixed
* Some flashes were not translated
* Trusted proxies were not parsed
* When running in a different container the health of the daemon could not be checked
## Version 1.0
(Initial release)

@ -9,4 +9,28 @@ Utils.initTags = () => {
})
}
Utils.timepickerOptions = {
localization:{
locale: 'nl',
format: 'dd/MM/yyyy HH:mm:ss'
},
display: {
icons: {
time: 'icon-clock-o',
date: 'icon-calendar',
up: 'icon-arrow-up',
down: 'icon-arrow-down',
previous: 'icon-chevron-left',
next: 'icon-chevron-right',
today: 'icon-calendar-check-o',
clear: 'icon-delete',
close: 'icon-x',
},
components: {
seconds: true,
useTwentyfourHour: true
}
},
}
export default Utils;

@ -1,7 +1,6 @@
import 'bootstrap';
import moment from 'moment';
import * as tempusDominus from '@eonasdan/tempus-dominus/dist/js/tempus-dominus';
import customDateFormat from '@eonasdan/tempus-dominus/dist/plugins/customDateFormat'
import {TempusDominus,extend} from "@eonasdan/tempus-dominus";
import customDateFormat from '@eonasdan/tempus-dominus/dist/plugins/customDateFormat';
import Utils from "./Utils";
document.addEventListener("readystatechange", event => {
@ -18,34 +17,11 @@ document.addEventListener("readystatechange", event => {
}
});
const timepickerOptions = {
localization:{
locale: 'nl',
format: 'dd/MM/yyyy HH:mm:ss'
},
display: {
icons: {
time: 'icon-clock-o',
date: 'icon-calendar',
up: 'icon-arrow-up',
down: 'icon-arrow-down',
previous: 'icon-chevron-left',
next: 'icon-chevron-right',
today: 'icon-calendar-check-o',
clear: 'icon-delete',
close: 'icon-x',
},
components: {
seconds: true,
useTwentyfourHour: true
}
},
}
function initDatePickers()
{
tempusDominus.extend(customDateFormat);
new tempusDominus.TempusDominus(document.querySelector('#nextrunselector'), timepickerOptions);
new tempusDominus.TempusDominus(document.querySelector('#lastrunselector'), timepickerOptions);
extend(customDateFormat);
new TempusDominus(document.querySelector('#nextrunselector'), Utils.timepickerOptions);
new TempusDominus(document.querySelector('#lastrunselector'), Utils.timepickerOptions);
}
function initCronType()

@ -2,11 +2,14 @@ import {Modal} from 'bootstrap';
import image from '/assets/images/ajax-loader.gif'
import '/assets/scss/job/index.scss';
import Utils from "./Utils";
import customDateFormat from '@eonasdan/tempus-dominus/dist/plugins/customDateFormat';
import {DateTime,TempusDominus,extend} from "@eonasdan/tempus-dominus";
document.addEventListener("readystatechange", event => {
if(event.target.readyState === 'complete') {
initDeleteButtons();
initRunNowButtons();
initRunButtons();
initTimepicker();
Utils.initTags();
}
});
@ -27,55 +30,106 @@ function initDeleteButtons() {
}));
}
function initRunNowButtons() {
document.querySelectorAll('.runnow').forEach(elem => elem.addEventListener("click", event => {
var selecttimedatepicker;
function initTimepicker() {
extend(customDateFormat);
let modal = document.querySelector('#run_selecttime');
let datepickeroptions = Utils.timepickerOptions;
datepickeroptions.display.inline = true;
datepickeroptions.display.sideBySide = true;
datepickeroptions.restrictions = {
minDate: new Date()
};
selecttimedatepicker = new TempusDominus(document.querySelector('#selecttime_datepicker'), datepickeroptions);
}
function initRunButtons() {
document.querySelectorAll('.run').forEach(elem => elem.addEventListener("click", event => {
let me = event.currentTarget;
let href = me.dataset.href;
let norun = me.closest('tr').classList.contains('norun')
let maxdate = new DateTime(me.dataset.nextrun)
if (maxdate < new DateTime() ) {
if (norun) {
maxdate = undefined;
} else {
console.error('You cannot have to be run jobs in the past');
return;
}
}
selecttimedatepicker.updateOptions({
restrictions: {
maxDate: maxdate
}
})
selecttimedatepicker.viewDate = new DateTime();
var bsModal = new Modal('#run_selecttime');
bsModal.show();
let runnowCnt = document.querySelector('.runnow-content');
if(runnowCnt.querySelector('img') === null) {
let loaderImg = document.createElement('img');
loaderImg.src = image;
runnowCnt.appendChild(loaderImg);
let schedulefn = event => {
bsModal.hide();
let time = Math.floor(selecttimedatepicker.viewDate / 1000);
run(me, time);
}
let runnowfn = event => {
bsModal.hide();
run(me);
}
document.querySelector('.container-fluid').classList.add('blur');
document.querySelector('.runnow-overlay').classList.add('d-block');
document.querySelector('.runnow-overlay').classList.remove('d-none');
let closebtnfn = event => {
bsModal.hide();
document.querySelectorAll('.schedule').forEach(elem => elem.removeEventListener("click", schedulefn));
document.querySelectorAll('.run-now').forEach(elem => elem.removeEventListener("click",runnowfn));
}
document.querySelectorAll('.schedule').forEach(elem => elem.addEventListener("click", schedulefn, { once: true } ));
document.querySelectorAll('.run-now').forEach(elem => elem.addEventListener("click", runnowfn, { once: true } ));
document.querySelectorAll('.btn-close').forEach(elem => elem.addEventListener("click", closebtnfn ));
} ));
}
function run(elem, time = 0) {
let href = elem.dataset.href;
if (time > 0) href = href + '/' + time.toString();
fetch(href, { method: 'GET' })
.then(response => response.json())
.then(data => {
let modal = document.querySelector('#runnow_result');
modal.querySelector('.modal-title').innerHTML = data.title;
if (data.status == 'deferred') {
modal.querySelector('.modal-body').innerHTML = data.message;
me.classList.add('disabled');
let td = me.closest('td');
td.querySelectorAll('.btn').forEach(btn => {
btn.classList.add('btn-outline-success');
btn.classList.remove('btn-outline-primary');
btn.classList.remove('btn-outline-danger');
})
let runCnt = document.querySelector('.run-content');
if(runCnt.querySelector('img') === null) {
let loaderImg = document.createElement('img');
loaderImg.src = image;
runCnt.appendChild(loaderImg);
}
document.querySelector('.container-fluid').classList.add('blur');
document.querySelector('.run-overlay').classList.add('d-block');
document.querySelector('.run-overlay').classList.remove('d-none');
fetch(href, { method: 'GET' })
.then(response => response.json())
.then(data => {
let modal = document.querySelector('#run_result');
modal.querySelector('.modal-title').innerHTML = data.title;
if (data.status == 'deferred') {
modal.querySelector('.modal-body').innerHTML = data.message;
elem.classList.add('disabled');
let td = elem.closest('td');
td.querySelectorAll('.btn').forEach(btn => {
btn.classList.add('btn-outline-success');
btn.classList.remove('btn-outline-primary');
btn.classList.remove('btn-outline-danger');
})
let tr = me.closest('tr');
tr.classList.add('running');
tr.classList.add('text-success');
tr.classList.remove('norun');
tr.classList.remove('text-danger');
} else if (data.status == 'ran') {
let content = '<p>' + data.message + '</p>'
content += '<pre>' + data.output + '</pre>'
modal.querySelector('.modal-body').innerHTML = content;
}
let tr = elem.closest('tr');
tr.classList.add('running');
tr.classList.add('text-success');
tr.classList.remove('norun');
tr.classList.remove('text-danger');
} else if (data.status == 'ran') {
let content = '<p>' + data.message + '</p>'
content += '<pre>' + data.output + '</pre>'
var bsModal = new Modal('#runnow_result').show();
modal.querySelector('.modal-body').innerHTML = content;
}
document.querySelector('.container-fluid').classList.remove('blur');
document.querySelector('.runnow-overlay').classList.remove('d-block');
document.querySelector('.runnow-overlay').classList.add('d-none');
})
var runModal = new Modal('#run_result');
runModal.show();
document.querySelector('.container-fluid').classList.remove('blur');
document.querySelector('.run-overlay').classList.remove('d-block');
document.querySelector('.run-overlay').classList.add('d-none');
})
)
}

@ -1,5 +1,7 @@
@import "/assets/scss/base";
@import "/assets/scss/icons";
@import "assets/scss/base";
@import "assets/scss/icons";
@import "/node_modules/@eonasdan/tempus-dominus/dist/css/tempus-dominus.css";
@import "assets/scss/tempus-dominus-dark";
tr.norun td {
background-color: #f8d7da;
@ -33,8 +35,8 @@ td.status-col {
filter: blur(3px);
}
.runnow-overlay {
.runnow-blur {
.run-overlay {
.run-blur {
bottom: 0;
left: 0;
position: fixed;
@ -43,7 +45,7 @@ td.status-col {
z-index: 1500;
}
.runnow-content {
.run-content {
font-size: 10px;
height: 50px;
position: absolute;
@ -54,4 +56,25 @@ td.status-col {
margin-left: -50px;
margin-top: -50px;
}
}
#run_selecttime {
.tempus-dominus-widget {
box-shadow: none;
&.timepicker-sbs {
width: 19em;
.td-row {
flex-direction: column;
.td-half {
width: 19em;
}
}
}
&.dark {
background-color: #2f2f2f;
}
}
}

@ -13,23 +13,23 @@
"ext-posix": "*",
"doctrine/doctrine-bundle": "^2.7",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.12",
"guzzlehttp/guzzle": "^7.4",
"doctrine/orm": "^2.13",
"guzzlehttp/guzzle": "^7.5",
"phpseclib/phpseclib": "^3.0",
"scienta/doctrine-json-functions": "^5.1",
"symfony/console": "^6.1",
"symfony/crowdin-translation-provider": "6.1.*",
"symfony/dotenv": "^6.1",
"scienta/doctrine-json-functions": "^5.2",
"symfony/console": "^6.2",
"symfony/crowdin-translation-provider": "^6.2",
"symfony/dotenv": "^6.2",
"symfony/flex": "^2.2",
"symfony/framework-bundle": "^6.1",
"symfony/mailer": "^6.1",
"symfony/proxy-manager-bridge": "^6.1",
"symfony/runtime": "^6.1",
"symfony/security-bundle": "^6.1",
"symfony/translation": "^6.1",
"symfony/twig-bundle": "^6.1",
"symfony/webpack-encore-bundle": "^1.15",
"symfony/yaml": "^6.1"
"symfony/framework-bundle": "^6.2",
"symfony/mailer": "^6.2",
"symfony/proxy-manager-bridge": "^6.2",
"symfony/runtime": "^6.2",
"symfony/security-bundle": "^6.2",
"symfony/translation": "^6.2",
"symfony/twig-bundle": "^6.2",
"symfony/webpack-encore-bundle": "^v1.16",
"symfony/yaml": "^6.2"
},
"config": {
"allow-plugins": {
@ -79,14 +79,14 @@
"extra": {
"symfony": {
"allow-contrib": true,
"require": "6.1.*"
"require": "^6.1"
}
},
"require-dev": {
"symfony/debug-bundle": "^6.1",
"symfony/maker-bundle": "^1.45",
"symfony/debug-bundle": "^6.2",
"symfony/maker-bundle": "^v1.48.0",
"symfony/monolog-bundle": "^3.8",
"symfony/stopwatch": "^6.1",
"symfony/web-profiler-bundle": "^6.1"
"symfony/stopwatch": "^6.2",
"symfony/web-profiler-bundle": "^6.2"
}
}

1262
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -8,7 +8,7 @@ framework:
crowdin:
dsn: '%env(CROWDIN_DSN)%'
domains: ['messages']
locales: ['en', 'nl', 'leet', 'lol']
locales: ['en', 'nl', 'leet']
# loco:
# dsn: '%env(LOCO_DSN)%'
# lokalise:

@ -34,6 +34,13 @@ login:
path: '/{_locale}/login'
controller: App\Controller\UserController::loginAction
webhook:
path: '/hook/{id}/{token}'
controller: App\Controller\JobController::hookAction
requirements:
id: \d+
token: '[A-Za-z0-9]+'
job_index:
path: '/{_locale}/job'
controller: App\Controller\JobController::defaultAction
@ -61,11 +68,14 @@ job_edit:
requirements:
id: \d+
job_runnow:
path: '/{_locale}/job/{id}/runnow'
controller: App\Controller\JobController::runNowAction
job_run:
path: '/{_locale}/job/{id}/run/{timestamp}'
controller: App\Controller\JobController::runAction
requirements:
id: \d+
timestamp: \d+
defaults:
timestamp: 0
job_add:
path: '/{_locale}/job/add'

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use App\Entity\Job;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpKernel\KernelInterface;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version1003 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
$allJobs = $this->connection->executeQuery('SELECT * FROM job')->fetchAllAssociative();
foreach($allJobs as $job) {
$data = json_decode($job['data'], true);
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
$length = 32;
for ($i = 0; $i < $length; $i++) {
$index = rand(0, strlen($characters) - 1);
$randomString .= $characters[$index];
}
$data['hooktoken'] = $randomString;
$this->addSql('UPDATE job SET data = "' . addSlashes(json_encode($data)) . '" WHERE id = ' . $job['id']);
}
}
public function down(Schema $schema): void
{
$allJobs = $this->connection->executeQuery('SELECT * FROM job')->fetchAllAssociative();
foreach($allJobs as $job) {
$data = json_decode($job['data'], true);
unset($data['hooktoken']);
$this->addSql('UPDATE job SET data = "' . addSlashes(json_encode($data)) . '" WHERE id = ' . $job['id']);
}
}
}

3620
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -6,17 +6,17 @@
"license": "AGPL-3.0-or-later",
"private": true,
"dependencies": {
"@eonasdan/tempus-dominus": "6.0",
"@eonasdan/tempus-dominus": "^6.2",
"@popperjs/core": "^2.11",
"bootstrap": "^5.2",
"bootstrap-dark-5": "^1.1",
"moment": "^2.29"
},
"devDependencies": {
"sass": "^1.5",
"sass-loader": "^13.0",
"@symfony/webpack-encore": "^3.0",
"core-js": "^3.24"
"sass": "^1.56",
"sass-loader": "^13.2",
"@symfony/webpack-encore": "^4.1",
"core-js": "^3.26"
},
"scripts": {
"watch" : "encore dev --watch",

@ -37,6 +37,7 @@ class DaemonCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output) : int
{
ini_set('memory_limit', '4G');
$jobRepo = $this->doctrine->getRepository(Job::class);
$timelimit = $input->getOption('time-limit') ?? false;
$async = $input->getOption('async') ?? function_exists('pcntl_fork');
@ -55,14 +56,19 @@ class DaemonCommand extends Command
$jobsToRun = $jobRepo->getJobsDue();
if(!empty($jobsToRun)) {
foreach($jobsToRun as $key=>$job) {
if($job->getData('crontype') == 'reboot') {
$str = @file_get_contents('/proc/uptime');
$num = floatval($str);
if ($job->getData('crontype') == 'reboot') {
$str = @file_get_contents('/proc/uptime');
$num = floatval($str);
$rebootedself = ($num < $job->getData('reboot-duration') * 60);
$consolerun = $jobRepo->getTempVar($job, 'consolerun', false);
if($consolerun && !$rebootedself) continue;
if ($consolerun && !$rebootedself) continue;
}
$manual = ($job->getRunning() == 2);
$manual = '';
if($jobRepo->getTempVar($job, 'webhook', false)) {
$manual = 'Webhook';
} elseif($job->getRunning() > 1) {
$manual = 'Manual';
};
$jobRepo->setJobRunning($job, true);
$output->writeln('Running Job ' . $job->getId());
if($async) {
@ -73,13 +79,10 @@ class DaemonCommand extends Command
$jobRepo = $this->doctrine->getRepository(Job::class);
}
if(!$async || $pid == -1) {
$jobRepo->RunJob($job, $manual);
$jobRepo->setJobRunning($job, false);
} elseif ($pid == 0) {
$jobRepo->RunJob($job, $manual);
$jobRepo->setJobRunning($job, false);
exit;
if((!$async || $pid == -1) || $pid == 0) {
$result = $jobRepo->RunJob($job, $manual);
if ($result['status'] == 'ran') $jobRepo->setJobRunning($job, false);
if ($pid == 0) exit;
}
unset($jobsToRun[$key]);
unset($job);

@ -86,6 +86,7 @@ class DemoInstallCommand extends Command
false,
]
]);
$job1->addToken();
$job2 = $jobRepo->prepareJob([
'name' => '[Website] Update texts to latest version',
@ -111,6 +112,8 @@ class DemoInstallCommand extends Command
true,
]
]);
$job2->addToken();
$job3 = $jobRepo->prepareJob([
'name' => '[Server][Reboot] Monthly reboot',
'interval' => (60*60*24*30),
@ -136,6 +139,8 @@ class DemoInstallCommand extends Command
'var-issecret' => [
]
]);
$job3->addToken();
$em->persist($job1);
$em->persist($job2);
$em->persist($job3);
@ -220,6 +225,15 @@ rtt min/avg/max/mdev = 101.362/101.362/101.362/0.000 ms')
->setFlags(RunRepository::SUCCESS);
$em->persist($run);
$run = new Run();
$run->setExitcode(200)
->setJob($job2)
->setRuntime(rand(0, 10000) / 1000)
->setOutput(json_encode(['success' => true, 'message' => 'Texts are updated succesfully']))
->setTimestamp(7200 * ceil( time() / 7200) - (3542))
->setFlags(RunRepository::SUCCESS . RunRepository::TRIGGERED);
$em->persist($run);
$run = new Run();
$run->setExitcode(200)
->setJob($job2)

@ -47,14 +47,14 @@ class RunCommand extends Command
}
$jobRepo->setJobRunning($job, true);
$jobRepo->setTempVar($job, 'consolerun', true);
$result = $jobRepo->runNow($job, true);
$result = $jobRepo->run($job, true);
if($job->getData('crontype') == 'reboot') {
$sleeping = true;
while($sleeping) {
if(time() >= $job->getRunning()) $sleeping = false;
sleep(1);
}
$result = $jobRepo->runNow($job, true);
$result = $jobRepo->run($job, true);
}
$jobRepo->setJobRunning($job, false);
$jobRepo->setTempVar($job, 'consolerun', false);

@ -81,34 +81,46 @@ class JobController extends AbstractController
}
}
public function runNowAction(Request $request, ManagerRegistry $doctrine, TranslatorInterface $translator, int $id): JsonResponse
public function runAction(Request $request, ManagerRegistry $doctrine, TranslatorInterface $translator, int $id, int $timestamp): JsonResponse
{
if($request->getMethod() == 'GET') {
$jobRepo = $doctrine->getRepository(Job::class);
$job = $jobRepo->find($id);
$runnowResult = $jobRepo->runNow($job);
if ($runnowResult['success'] === NULL) {
$runResult = $jobRepo->run($job, false, $timestamp);
if ($runResult['success'] === NULL) {
$return = [
'status' => 'deferred',
'success' => NULL,
'title' => $translator->trans('job.index.runnow.deferred.title'),
'message' => $translator->trans('job.index.runnow.deferred.message')
'title' => $translator->trans('job.index.run.deferred.title'),
'message' => $translator->trans('job.index.run.deferred.message')
];
} else {
$return = [
'status' => 'ran',
'success' => $runnowResult['success'],
'title' => $runnowResult['success'] ? $translator->trans('job.index.runnow.ran.title.success') : $translator->trans('job.index.runnow.ran.title.failed'),
'message' => $translator->trans('job.index.runnow.ran.message', [
'_runtime_' => number_format($runnowResult['runtime'], 3),
'_exitcode_' => $runnowResult['exitcode']
'success' => $runResult['success'],
'title' => $runResult['success'] ? $translator->trans('job.index.run.ran.title.success') : $translator->trans('job.index.run.ran.title.failed'),
'message' => $translator->trans('job.index.run.ran.message', [
'_runtime_' => number_format($runResult['runtime'], 3),
'_exitcode_' => $runResult['exitcode']
]),
'exitcode' => $runnowResult['exitcode'],
'output' => $runnowResult['output'],
'exitcode' => $runResult['exitcode'],
'output' => $runResult['output'],
];
}
return new JsonResponse($return);
}
return new JsonResponse(['success'=>false, 'message' => 'Your request is invalid'], Response::HTTP_BAD_REQUEST);
}
public function hookAction(Request $request, ManagerRegistry $doctrine, int $id, string $token)
{
$jobRepo = $doctrine->getRepository(Job::class);
$job = $jobRepo->find($id);
if(!empty($job->getToken()) && $job->getToken() == $token && $job->getRunning() != 1) {
$jobRepo->setTempVar($job, 'webhook', true);
return new JsonResponse($jobRepo->run($job, false, time()));
}
return new JsonResponse(['success'=>false, 'message' => 'Your request is invalid'], Response::HTTP_BAD_REQUEST);
}
}

@ -259,4 +259,29 @@ class Job
return $this;
}
public function getToken(): string
{
return $this->getData('hooktoken') ?? '';
}
public function deleteToken(): Job
{
$this->removeData('hooktoken');
return $this;
}
public function addToken(): Job
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
$length = 32;
for ($i = 0; $i < $length; $i++) {
$index = rand(0, strlen($characters) - 1);
$randomString .= $characters[$index];
}
$this->setData('hooktoken', $randomString);
return $this;
}
}

@ -123,7 +123,7 @@ class JobRepository extends EntityRepository
->orWhere(
$qb->expr()->andX(
$qb->expr()->notIn('job.running', [0,1,2]),
$qb->expr()->lt('job.running', ':timestamp')
$qb->expr()->lte('job.running', ':timestamp')
)
)
->orWhere('job.running = 2')
@ -142,7 +142,7 @@ class JobRepository extends EntityRepository
{
$em = $this->getEntityManager();
if(in_array($job->getRunning(), [0,1,2])) $job->setRunning($status ? 1 : 0);
$job->setRunning($status ? 1 : 0);
$em->persist($job);
$em->flush();
@ -167,7 +167,7 @@ class JobRepository extends EntityRepository
*/
public function deleteTempVar(Job &$job, ?string $name = NULL ): void
{
$job->removeData('temp_vars.' . ($name !== NULL ? '.' . $name : ''));
$job->removeData('temp_vars' . ($name !== NULL ? '.' . $name : ''));
}
/**
@ -317,10 +317,10 @@ class JobRepository extends EntityRepository
* @return array|string[]
* @throws \Doctrine\DBAL\Exception
*/
private function runRebootJob(Job &$job, float &$starttime, bool &$manual): array
private function runRebootJob(Job &$job, float &$starttime, string &$manual): array
{
$em = $this->getEntityManager();
if($job->getRunning() == 1) {
if($this->getTempVar($job, 'rebooting', false) === false) {
if(isset($_ENV['DEMO_MODE']) && $_ENV['DEMO_MODE'] == 'true') {
$job->setRunning(time() + $job->getData('reboot-delay-secs') + ($job->getData('reboot-duration') * 60));
$em->persist($job);
@ -341,6 +341,7 @@ class JobRepository extends EntityRepository
}
$job->setRunning(time() + $job->getData('reboot-delay-secs') + ($job->getData('reboot-duration') * 60));
$this->setTempVar($job, 'rebooting', true);
$em->persist($job);
$em->flush();
@ -358,7 +359,7 @@ class JobRepository extends EntityRepository
}
return ['status' => 'deferred'];
} elseif($job->getRunning() != 0) {
} elseif($this->getTempVar($job, 'rebooting', false) === true) {
if($job->getRunning() > time()) {
return ['status' => 'deferred'];
}
@ -411,11 +412,15 @@ class JobRepository extends EntityRepository
* @return array
* @throws \Doctrine\DBAL\Exception
*/
public function runNow(Job &$job, $console = false) {
public function run(Job &$job, $console = false, int $timestamp = 0)
{
$em = $this->getEntityManager();
$runRepo = $this->getEntityManager()->getRepository(Run::class);
if($console == false && ($runRepo->isSlowJob($job)) || count($job->getRuns()) == 0 || $job->getData('crontype') === 'reboot') {
if ($timestamp > 0) {
$job->setRunning($timestamp);
$em->persist($job);
$em->flush();
} elseif($console == false && ($runRepo->isSlowJob($job)) || count($job->getRuns()) == 0 || $job->getData('crontype') === 'reboot') {
if(in_array($job->getRunning(), [0,1,2])) {
$job->setRunning(2);
$em->persist($job);
@ -458,7 +463,7 @@ class JobRepository extends EntityRepository
* @return array|string[]
* @throws \Doctrine\DBAL\Exception
*/
public function runJob(Job &$job, bool $manual): array
public function runJob(Job &$job, string $manual): array
{
$em = $this->getEntityManager();
$starttime = microtime(true);
@ -481,8 +486,10 @@ class JobRepository extends EntityRepository
$flags[] = RunRepository::SUCCESS;
}
if ($manual === true) {
if ($manual == 'Manual') {
$flags[] = RunRepository::MANUAL;
} elseif ($manual == 'Webhook') {
$flags[] = RunRepository::TRIGGERED;
}
// Remove secrets from output
@ -506,13 +513,12 @@ class JobRepository extends EntityRepository
} while ($nextrun < time());
$job->setNextrun($nextrun);
}
$this->deleteTempVar($job);
$em->persist($job);
$em->flush();
return ['job_id' => $job->getId(), 'exitcode' => $result['exitcode'], 'timestamp' =>floor($starttime), 'runtime' => $runtime, 'output' => (string)$result['output'], 'flags' => implode("", $flags)];
return ['job_id' => $job->getId(), 'status' => 'ran', 'exitcode' => $result['exitcode'], 'timestamp' =>floor($starttime), 'runtime' => $runtime, 'output' => (string)$result['output'], 'flags' => implode("", $flags)];
}
/**

@ -14,6 +14,7 @@ class RunRepository extends EntityRepository
const FAILED = 'F';
const SUCCESS = 'S';
const MANUAL = 'M';
const TRIGGERED = 'T';
public function getRunsForJob(Job $job, bool $onlyfailed = false, int $maxage = NULL, bool $ordered = true): array
{

@ -2,252 +2,252 @@
{% block title %}{{ "job.add.title" | trans }}{% endblock %}
{% block content %}
<h2>{{ "job.add.header" | trans }}</h2>
<form method="post" class="form-horizontal" enctype="multipart/form-data" action="{{ path('job_add') }}">
<h3>{{ "job.addedit.generalinfo.header" | trans }}</h3>
<div class="mb-3">
<label for="name">{{ "job.addedit.generalinfo.name.label" | trans }}</label>
<input type="text" name="name" class="form-control" id="name" placeholder="{{ "job.addedit.generalinfo.name.placeholder" | trans }}">
<small id="name-help" class="form-text text-muted">{{ "job.addedit.generalinfo.name.helptext" | trans }}</small>
</div>
<div class="mb-3">
<label for="name">{{ "job.addedit.generalinfo.interval.label" | trans }}</label>
<div class="input-group">
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="intervalButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ "job.addedit.generalinfo.interval.patterns.label" | trans }}
</button>
<ul class="dropdown-menu" aria-labelledby="intervalButton">
<li><a class="dropdown-item intervalpattern-item" href="javascript:void(0);" data-time="60">{{ "job.addedit.generalinfo.interval.patterns.minute" | trans }}</a></li>
<li><a class="dropdown-item intervalpattern-item" href="javascript:void(0);" data-time="3600">{{ "job.addedit.generalinfo.interval.patterns.hour" | trans }}</a></li>
<li><a class="dropdown-item intervalpattern-item" href="javascript:void(0);" data-time="86400">{{ "job.addedit.generalinfo.interval.patterns.day" | trans }}</a></li>
<li><a class="dropdown-item intervalpattern-item" href="javascript:void(0);" data-time="604800">{{ "job.addedit.generalinfo.interval.patterns.week" | trans }}</a></li>
<li><a class="dropdown-item intervalpattern-item" href="javascript:void(0);" data-time="2419200">{{ "job.addedit.generalinfo.interval.patterns.4week" | trans }}</a></li>
</ul>
<input type="number" class="form-control" id="interval" name="interval">
</div>
</div>
<div class="mb-3">
<label for="nextrun">{{ "job.addedit.generalinfo.nextrun.label" | trans }}</label>
<input type="text" autocomplete="off" pattern="[0-9]{2}\/[0-9]{2}\/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}" placeholder="{{ date() | date("d/m/Y H:i:s")}}" step="1" id="nextrunselector" class="form-control datetimepicker-input" data-target="#nextrunselector" data-bs-toggle="datetimepicker" name="nextrun">
</div>
<div class="mb-3">
<label for="lastrun">{{ "job.addedit.generalinfo.lastrun.label" | trans }}</label>
<div class="input-group">
<div class="input-group-text border-end-0">
<input type="checkbox" name="lastrun-eternal" class="lastrun-eternal" placeholder="value" value="true">
</div>
<span class="input-group-text border-start-0">{{ "job.addedit.generalinfo.lastrun.eternal.label" | trans }}</span>
<input type="text" autocomplete="off" pattern="[0-9]{2}\/[0-9]{2}\/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}" placeholder="{{ date() | date("d/m/Y H:i:s")}}" data-placeholder="{{ date() | date("d/m/Y H:i:s")}}" id="lastrunselector" class="form-control datetimepicker-input" data-target="#lastrunselector" data-bs-toggle="datetimepicker" name="lastrun">
</div>
</div>
<div class="mb-3">
<label for="retention">{{ "job.addedit.generalinfo.retention.label" | trans }}</label>
<input type="number" name="retention" class="form-control" id="retention" placeholder="{{ "job.addedit.generalinfo.retention.placeholder" | trans }}">
<small id="retention-help" class="form-text text-muted">{{ "job.addedit.generalinfo.retention.helptext" | trans }}</small>
</div>
<div class="mb-3">
<label for="fail-pct">{{ "job.addedit.generalinfo.failpercentage.label" | trans }}</label>
<div class="input-group d-flex">
<div class="range-value range-value-fail-pct pe-1">50%</div>
<div class="range-input ps-1 flex-grow-1">
<input type="range" name="fail-pct" class="form-range range-input-fail-pct" id="fail-pct" max="100" step="5" value="50">
</div>
</div>
</div>
<div class="mb-3">
<label for="fail-days">{{ "job.addedit.generalinfo.faildays.label" | trans }}</label>
<input type="number" name="fail-days" class="form-control" id="fail-days" placeholder="{{ "job.addedit.generalinfo.faildays.placeholder" | trans }}">
</div>
<div class="mb-3">
<label for="retention">{{ "job.addedit.generalinfo.hostlabel.label" | trans }}</label>
<input type="text" name="hostlabel" class="form-control" id="hostlabel" placeholder="{{ "job.addedit.generalinfo.hostlabel.placeholder" | trans }}">
<small id="hostlabel-help" class="form-text text-muted">{{ "job.addedit.generalinfo.hostlabel.helptext" | trans }}</small>
</div>
<h3>{{ "job.addedit.jobdetails.header" | trans }}</h3>
<div class="mb-3 btn-group croncategory-selector">
<div class="dropdown croncategory-group crontype-group">
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="crontypeButton" data-default-text="" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ "job.addedit.crontype.label" | trans }}
</button>
<ul class="dropdown-menu" aria-labelledby="crontypeButton">
<li><a class="dropdown-item crontype-item" href="javascript:void(0);" data-type="command">{{ "job.addedit.crontype.command.label" | trans }}</a></li>
<li><a class="dropdown-item crontype-item" href="javascript:void(0);" data-type="reboot">{{ "job.addedit.crontype.reboot.label" | trans }}</a></li>
<li><a class="dropdown-item crontype-item" href="javascript:void(0);" data-type="http">{{ "job.addedit.crontype.http.label" | trans }}</a></li>
</ul>
</div>
<div class="dropdown croncategory-group d-none hosttype-group">
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="hosttypeButton" data-default-text="" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ "job.addedit.hosttype.label" | trans }}
</button>
<ul class="dropdown-menu" aria-labelledby="hosttypeButton">
<li><a class="dropdown-item hosttype-item" href="javascript:void(0);" data-type="local">{{ "job.addedit.hosttype.local.label" | trans }}</a></li>
<li><a class="dropdown-item hosttype-item" href="javascript:void(0);" data-type="ssh">{{ "job.addedit.hosttype.ssh.label" | trans }}</a></li>
</ul>
</div>
<form method="post" class="form-horizontal" enctype="multipart/form-data" action="{{ path('job_add') }}">
<div class="dropdown croncategory-group d-none containertype-group">
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="containertypeButton" data-default-text="" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ "job.addedit.containertype.label" | trans }}
</button>
<ul class="dropdown-menu" aria-labelledby="containertypeButton">
<li><a class="dropdown-item containertype-item" href="javascript:void(0);" data-type="none">{{ "job.addedit.containertype.none.label" | trans }}</a></li>
<li><a class="dropdown-item containertype-item" href="javascript:void(0);" data-type="docker">{{ "job.addedit.containertype.docker.label" | trans }}</a></li>
</ul>
</div>
</div>
<h3>{{ "job.addedit.generalinfo.header" | trans }}</h3>
<div class="crontype-command crontype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.crontype.command.label" | trans }}</h4>
<div class="mb-3">
<label for="command">{{ "job.addedit.crontype.command.command.label" | trans }}</label>
<input type="text" name="command" class="form-control" id="command" placeholder="{{ "job.addedit.crontype.command.command.placeholder" | trans }}">
<label for="name">{{ "job.addedit.generalinfo.name.label" | trans }}</label>
<input type="text" name="name" class="form-control" id="name" placeholder="{{ "job.addedit.generalinfo.name.placeholder" | trans }}">
<small id="name-help" class="form-text text-muted">{{ "job.addedit.generalinfo.name.helptext" | trans }}</small>
</div>
<div class="mb-3">
<label for="response">{{ "job.addedit.crontype.command.response.label" | trans }}</label>
<input type="text" name="response" class="form-control" id="response" placeholder="{{ "job.addedit.crontype.command.response.placeholder" | trans }}">
<label for="name">{{ "job.addedit.generalinfo.interval.label" | trans }}</label>
<div class="input-group">
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="intervalButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ "job.addedit.generalinfo.interval.patterns.label" | trans }}
</button>
<ul class="dropdown-menu" aria-labelledby="intervalButton">
<li><a class="dropdown-item intervalpattern-item" href="javascript:void(0);" data-time="60">{{ "job.addedit.generalinfo.interval.patterns.minute" | trans }}</a></li>
<li><a class="dropdown-item intervalpattern-item" href="javascript:void(0);" data-time="3600">{{ "job.addedit.generalinfo.interval.patterns.hour" | trans }}</a></li>
<li><a class="dropdown-item intervalpattern-item" href="javascript:void(0);" data-time="86400">{{ "job.addedit.generalinfo.interval.patterns.day" | trans }}</a></li>
<li><a class="dropdown-item intervalpattern-item" href="javascript:void(0);" data-time="604800">{{ "job.addedit.generalinfo.interval.patterns.week" | trans }}</a></li>
<li><a class="dropdown-item intervalpattern-item" href="javascript:void(0);" data-time="2419200">{{ "job.addedit.generalinfo.interval.patterns.4week" | trans }}</a></li>
</ul>
<input type="number" class="form-control" id="interval" name="interval">
</div>
</div>
</div>
<div class="crontype-reboot crontype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.crontype.reboot.header" | trans }}</h4>
<div class="mb-3">
<label for="reboot-command">{{ "job.addedit.crontype.reboot.reboot.command.label" | trans }}</label>
<input type="text" name="reboot-command" class="form-control" id="reboot-command" placeholder="{{ "job.addedit.crontype.reboot.reboot.command.placeholder" | trans }}">
<small id="reboot-command-help" class="form-text text-muted">{{ "job.addedit.crontype.reboot.reboot.command.placeholder" | trans }}</small>
<label for="nextrun">{{ "job.addedit.generalinfo.nextrun.label" | trans }}</label>
<input type="text" autocomplete="off" pattern="[0-9]{2}\/[0-9]{2}\/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}" placeholder="{{ date() | date("d/m/Y H:i:s")}}" step="1" id="nextrunselector" class="form-control datetimepicker-input" data-target="#nextrunselector" data-bs-toggle="datetimepicker" name="nextrun">
</div>
<div class="mb-3">
<label for="getservices-command">{{ "job.addedit.crontype.reboot.getservices.command.label" | trans }}</label>
<input type="text" name="getservices-command" class="form-control" id="getservices-command" placeholder="{{ "job.addedit.crontype.reboot.getservices.command.placeholder" | trans }}">
<label for="lastrun">{{ "job.addedit.generalinfo.lastrun.label" | trans }}</label>
<div class="input-group">
<div class="input-group-text border-end-0">
<input type="checkbox" name="lastrun-eternal" class="lastrun-eternal" placeholder="value" value="true">
</div>
<span class="input-group-text border-start-0">{{ "job.addedit.generalinfo.lastrun.eternal.label" | trans }}</span>
<input type="text" autocomplete="off" pattern="[0-9]{2}\/[0-9]{2}\/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}" placeholder="{{ date() | date("d/m/Y H:i:s")}}" data-placeholder="{{ date() | date("d/m/Y H:i:s")}}" id="lastrunselector" class="form-control datetimepicker-input" data-target="#lastrunselector" data-bs-toggle="datetimepicker" name="lastrun">
</div>
</div>
<div class="mb-3">
<label for="getservices-response">{{ "job.addedit.crontype.reboot.getservices.response.label" | trans }}</label>
<input type="text" name="getservices-response" class="form-control" id="getservices-response" placeholder="{{ "job.addedit.crontype.reboot.getservices.response.placeholder" | trans }}">
<label for="retention">{{ "job.addedit.generalinfo.retention.label" | trans }}</label>
<input type="number" name="retention" class="form-control" id="retention" placeholder="{{ "job.addedit.generalinfo.retention.placeholder" | trans }}">
<small id="retention-help" class="form-text text-muted">{{ "job.addedit.generalinfo.retention.helptext" | trans }}</small>
</div>
<div class="mb-3">
<label for="reboot-delay">{{ "job.addedit.crontype.reboot.reboot.delay.label" | trans }}</label>
<input type="number" name="reboot-delay" class="form-control" placeholder="{{ "job.addedit.crontype.reboot.reboot.delay.placeholder" | trans }}">
<small id="reboot-delay-help" class="form-text text-muted">{{ "job.addedit.crontype.reboot.reboot.delay.helptext" | trans }}</small>
<label for="fail-pct">{{ "job.addedit.generalinfo.failpercentage.label" | trans }}</label>
<div class="input-group d-flex">
<div class="range-value range-value-fail-pct pe-1">50%</div>
<div class="range-input ps-1 flex-grow-1">
<input type="range" name="fail-pct" class="form-range range-input-fail-pct" id="fail-pct" max="100" step="5" value="50">
</div>
</div>
</div>
<div class="mb-3">
<label for="reboot-duration">{{ "job.addedit.crontype.reboot.reboot.duration.label" | trans }}</label>
<input type="number" name="reboot-duration" class="form-control" placeholder="{{ "job.addedit.crontype.reboot.reboot.duration.placeholder" | trans }}">
<small id="reboot-duration-help" class="form-text text-muted">{{ "job.addedit.crontype.reboot.reboot.duration.helptext" | trans }}</small>
<label for="fail-days">{{ "job.addedit.generalinfo.faildays.label" | trans }}</label>
<input type="number" name="fail-days" class="form-control" id="fail-days" placeholder="{{ "job.addedit.generalinfo.faildays.placeholder" | trans }}">
</div>
</div>
<div class="crontype-http crontype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.crontype.http.header" | trans }}</h4>
<div class="mb-3">
<label for="url">{{ "job.addedit.crontype.http.url.label" | trans }}</label>
<input type="text" name="url" class="form-control" id="url" placeholder="{{ "job.addedit.crontype.http.url.placeholder" | trans }}">
<label for="retention">{{ "job.addedit.generalinfo.hostlabel.label" | trans }}</label>
<input type="text" name="hostlabel" class="form-control" id="hostlabel" placeholder="{{ "job.addedit.generalinfo.hostlabel.placeholder" | trans }}">
<small id="hostlabel-help" class="form-text text-muted">{{ "job.addedit.generalinfo.hostlabel.helptext" | trans }}</small>
</div>
<div class="mb-3">
<label for="basicauth-username">{{ "job.addedit.crontype.http.basic-auth.username.label" | trans }}</label>
<input type="text" name="basicauth-username" class="form-control" id="basicauth-username" placeholder="{{ "job.addedit.crontype.http.basic-auth.username.placeholder" | trans }}">
<h3>{{ "job.addedit.jobdetails.header" | trans }}</h3>
<div class="mb-3 btn-group croncategory-selector">
<div class="dropdown croncategory-group crontype-group">
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="crontypeButton" data-default-text="" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ "job.addedit.crontype.label" | trans }}
</button>
<ul class="dropdown-menu" aria-labelledby="crontypeButton">
<li><a class="dropdown-item crontype-item" href="javascript:void(0);" data-type="command">{{ "job.addedit.crontype.command.label" | trans }}</a></li>
<li><a class="dropdown-item crontype-item" href="javascript:void(0);" data-type="reboot">{{ "job.addedit.crontype.reboot.label" | trans }}</a></li>
<li><a class="dropdown-item crontype-item" href="javascript:void(0);" data-type="http">{{ "job.addedit.crontype.http.label" | trans }}</a></li>
</ul>
</div>
<div class="dropdown croncategory-group d-none hosttype-group">
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="hosttypeButton" data-default-text="" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ "job.addedit.hosttype.label" | trans }}
</button>
<ul class="dropdown-menu" aria-labelledby="hosttypeButton">
<li><a class="dropdown-item hosttype-item" href="javascript:void(0);" data-type="local">{{ "job.addedit.hosttype.local.label" | trans }}</a></li>
<li><a class="dropdown-item hosttype-item" href="javascript:void(0);" data-type="ssh">{{ "job.addedit.hosttype.ssh.label" | trans }}</a></li>
</ul>
</div>
<div class="dropdown croncategory-group d-none containertype-group">
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="containertypeButton" data-default-text="" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ "job.addedit.containertype.label" | trans }}
</button>
<ul class="dropdown-menu" aria-labelledby="containertypeButton">
<li><a class="dropdown-item containertype-item" href="javascript:void(0);" data-type="none">{{ "job.addedit.containertype.none.label" | trans }}</a></li>
<li><a class="dropdown-item containertype-item" href="javascript:void(0);" data-type="docker">{{ "job.addedit.containertype.docker.label" | trans }}</a></li>
</ul>
</div>
</div>
<div class="mb-3">
<label for="basicauth-password">{{ "job.addedit.crontype.http.basic-auth.password.label" | trans }}</label>
<input type="password" name="basicauth-password" class="form-control" placeholder="{{ "job.addedit.crontype.http.basic-auth.password.placeholder" | trans }}">
<small id="basicauth-password-help" class="form-text text-muted">{{ "job.addedit.crontype.http.basic-auth.password.helptext" | trans }}</small>
<div class="crontype-command crontype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.crontype.command.label" | trans }}</h4>
<div class="mb-3">
<label for="command">{{ "job.addedit.crontype.command.command.label" | trans }}</label>
<input type="text" name="command" class="form-control" id="command" placeholder="{{ "job.addedit.crontype.command.command.placeholder" | trans }}">
</div>
<div class="mb-3">
<label for="response">{{ "job.addedit.crontype.command.response.label" | trans }}</label>
<input type="text" name="response" class="form-control" id="response" placeholder="{{ "job.addedit.crontype.command.response.placeholder" | trans }}">
</div>
</div>
<div class="mb-3">
<label for="http-status">{{ "job.addedit.crontype.http.response.label" | trans }}</label>
<input type="text" name="http-status" class="form-control" id="http-status" placeholder="{{ "job.addedit.crontype.http.response.placeholder" | trans }}">
<div class="crontype-reboot crontype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.crontype.reboot.header" | trans }}</h4>
<div class="mb-3">
<label for="reboot-command">{{ "job.addedit.crontype.reboot.reboot.command.label" | trans }}</label>
<input type="text" name="reboot-command" class="form-control" id="reboot-command" placeholder="{{ "job.addedit.crontype.reboot.reboot.command.placeholder" | trans }}">
<small id="reboot-command-help" class="form-text text-muted">{{ "job.addedit.crontype.reboot.reboot.command.placeholder" | trans }}</small>
</div>
<div class="mb-3">
<label for="getservices-command">{{ "job.addedit.crontype.reboot.getservices.command.label" | trans }}</label>
<input type="text" name="getservices-command" class="form-control" id="getservices-command" placeholder="{{ "job.addedit.crontype.reboot.getservices.command.placeholder" | trans }}">
</div>
<div class="mb-3">
<label for="getservices-response">{{ "job.addedit.crontype.reboot.getservices.response.label" | trans }}</label>
<input type="text" name="getservices-response" class="form-control" id="getservices-response" placeholder="{{ "job.addedit.crontype.reboot.getservices.response.placeholder" | trans }}">
</div>
<div class="mb-3">
<label for="reboot-delay">{{ "job.addedit.crontype.reboot.reboot.delay.label" | trans }}</label>
<input type="number" name="reboot-delay" class="form-control" placeholder="{{ "job.addedit.crontype.reboot.reboot.delay.placeholder" | trans }}">
<small id="reboot-delay-help" class="form-text text-muted">{{ "job.addedit.crontype.reboot.reboot.delay.helptext" | trans }}</small>
</div>
<div class="mb-3">
<label for="reboot-duration">{{ "job.addedit.crontype.reboot.reboot.duration.label" | trans }}</label>
<input type="number" name="reboot-duration" class="form-control" placeholder="{{ "job.addedit.crontype.reboot.reboot.duration.placeholder" | trans }}">
<small id="reboot-duration-help" class="form-text text-muted">{{ "job.addedit.crontype.reboot.reboot.duration.helptext" | trans }}</small>
</div>
</div>
</div>
<div class="hosttype-local hosttype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.hosttype.local.header" | trans }}</h4>
<h5>{{ "job.addedit.hosttype.local.nodetails" | trans }}</h5>
</div>
<div class="crontype-http crontype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.crontype.http.header" | trans }}</h4>
<div class="mb-3">
<label for="url">{{ "job.addedit.crontype.http.url.label" | trans }}</label>
<input type="text" name="url" class="form-control" id="url" placeholder="{{ "job.addedit.crontype.http.url.placeholder" | trans }}">
</div>
<div class="mb-3">
<label for="basicauth-username">{{ "job.addedit.crontype.http.basic-auth.username.label" | trans }}</label>
<input type="text" name="basicauth-username" class="form-control" id="basicauth-username" placeholder="{{ "job.addedit.crontype.http.basic-auth.username.placeholder" | trans }}">
</div>
<div class="mb-3">
<label for="basicauth-password">{{ "job.addedit.crontype.http.basic-auth.password.label" | trans }}</label>
<input type="password" name="basicauth-password" class="form-control" placeholder="{{ "job.addedit.crontype.http.basic-auth.password.placeholder" | trans }}">
<small id="basicauth-password-help" class="form-text text-muted">{{ "job.addedit.crontype.http.basic-auth.password.helptext" | trans }}</small>
</div>
<div class="hosttype-ssh hosttype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.hosttype.ssh.header" | trans }}</h4>
<div class="mb-3">
<label for="host">{{ "job.addedit.hosttype.ssh.hostname.label" | trans }}</label>
<input type="text" name="host" class="form-control" id="host" placeholder="{{ "job.addedit.hosttype.ssh.hostname.placeholder" | trans }}">
<div class="mb-3">
<label for="http-status">{{ "job.addedit.crontype.http.response.label" | trans }}</label>
<input type="text" name="http-status" class="form-control" id="http-status" placeholder="{{ "job.addedit.crontype.http.response.placeholder" | trans }}">
</div>
</div>
<div class="mb-3">
<label for="user">{{ "job.addedit.hosttype.ssh.username.label" | trans }}</label>
<input type="text" name="user" class="form-control" id="user" placeholder="{{ "job.addedit.hosttype.ssh.username.placeholder" | trans }}">
<div class="hosttype-local hosttype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.hosttype.local.header" | trans }}</h4>
<h5>{{ "job.addedit.hosttype.local.nodetails" | trans }}</h5>
</div>
<div class="mb-3">
<label for="privkey">{{ "job.addedit.hosttype.ssh.privatekey.label" | trans }}</label>
<div class="input-group">
<input type="file" class="form-control" id="privkey" class="form-control" name="privkey">
<div class="hosttype-ssh hosttype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.hosttype.ssh.header" | trans }}</h4>
<div class="mb-3">
<label for="host">{{ "job.addedit.hosttype.ssh.hostname.label" | trans }}</label>
<input type="text" name="host" class="form-control" id="host" placeholder="{{ "job.addedit.hosttype.ssh.hostname.placeholder" | trans }}">
</div>
<div class="mb-3">
<label for="user">{{ "job.addedit.hosttype.ssh.username.label" | trans }}</label>
<input type="text" name="user" class="form-control" id="user" placeholder="{{ "job.addedit.hosttype.ssh.username.placeholder" | trans }}">
</div>
<div class="mb-3">
<label for="privkey">{{ "job.addedit.hosttype.ssh.privatekey.label" | trans }}</label>
<div class="input-group">
<input type="file" class="form-control" id="privkey" class="form-control" name="privkey">
</div>
<small id="custom-file-help" class="form-text text-muted">{{ "job.addedit.hosttype.ssh.privatekey.helptext" | trans }}</small>
</div>
<div class="mb-3">
<label for="privkey-password">{{ "job.addedit.hosttype.ssh.passphrase.label" | trans }}</label>
<input type="password" name="privkey-password" class="form-control" placeholder="{{ "job.addedit.hosttype.ssh.passphrase.placeholder" | trans }}">
<small id="privkey-password-help" class="form-text text-muted">{{ "job.addedit.hosttype.ssh.passphrase.helptext" | trans }}</small>
</div>
<small id="custom-file-help" class="form-text text-muted">{{ "job.addedit.hosttype.ssh.privatekey.helptext" | trans }}</small>
</div>
<div class="mb-3">
<label for="privkey-password">{{ "job.addedit.hosttype.ssh.passphrase.label" | trans }}</label>
<input type="password" name="privkey-password" class="form-control" placeholder="{{ "job.addedit.hosttype.ssh.passphrase.placeholder" | trans }}">
<small id="privkey-password-help" class="form-text text-muted">{{ "job.addedit.hosttype.ssh.passphrase.helptext" | trans }}</small>
<div class="containertype-none containertype-inputs croncategory-inputs d-none">
</div>
</div>
<div class="containertype-none containertype-inputs croncategory-inputs d-none">
</div>
<div class="containertype-docker containertype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.containertype.docker.header" | trans }}</h4>
<div class="mb-3">
<label for="service">{{ "job.addedit.containertype.docker.service.label" | trans }}</label>
<input type="text" name="service" class="form-control" id="service" placeholder="{{ "job.addedit.containertype.docker.service.placeholder" | trans }}">
</div>
<div class="containertype-docker containertype-inputs croncategory-inputs d-none">
<h4>{{ "job.addedit.containertype.docker.header" | trans }}</h4>
<div class="mb-3">
<label for="service">{{ "job.addedit.containertype.docker.service.label" | trans }}</label>
<input type="text" name="service" class="form-control" id="service" placeholder="{{ "job.addedit.containertype.docker.service.placeholder" | trans }}">
<div class="mb-3">
<label for="user">{{ "job.addedit.containertype.docker.username.label" | trans }}</label>
<input type="text" name="container-user" class="form-control" id="container-user" placeholder="{{ "job.addedit.containertype.docker.username.placeholder" | trans }}">
</div>
</div>
<div class="mb-3">
<label for="user">{{ "job.addedit.containertype.docker.username.label" | trans }}</label>
<input type="text" name="container-user" class="form-control" id="container-user" placeholder="{{ "job.addedit.containertype.docker.username.placeholder" | trans }}">
</div>
</div>
<h3>{{ "job.addedit.variables.header" | trans }}</h3>
<h3>{{ "job.addedit.variables.header" | trans }}</h3>
<div class="vars mb-3">
<div class="input-group var-group d-none">
<span class="input-group-text border-end-0">
<input type="checkbox" name="var-issecret[0]" class="var-issecret" placeholder="value" value="true">
</span>
<span class="input-group-text border-start-0">{{ "job.addedit.variables.secret.label" | trans }}</span>
<input type="text" name="var-id[0]" class="form-control var-id" placeholder="{{ "job.addedit.variables.name.placeholder" | trans }}">
<input type="text" name="var-value[0]" class="form-control var-value" placeholder="{{ "job.addedit.variables.value.placeholder" | trans }}">
<div class="vars mb-3">
<div class="input-group var-group d-none">
<span class="input-group-text border-end-0">
<input type="checkbox" name="var-issecret[0]" class="var-issecret" placeholder="value" value="true">
</span>
<span class="input-group-text border-start-0">{{ "job.addedit.variables.secret.label" | trans }}</span>
<input type="text" name="var-id[0]" class="form-control var-id" placeholder="{{ "job.addedit.variables.name.placeholder" | trans }}">
<input type="text" name="var-value[0]" class="form-control var-value" placeholder="{{ "job.addedit.variables.value.placeholder" | trans }}">
</div>
</div>
<div class="vars-description mb-3 d-none">
<p>
{{ "job.addedit.variables.helptext" | trans }}
</p>
</div>
</div>
<div class="vars-description mb-3 d-none">
<p>
{{ "job.addedit.variables.helptext" | trans }}
</p>
</div>
<div class="mb-3">
<a href="javascript:void(0);" class="btn btn-outline-primary addvar-btn">{{ "job.addedit.variables.add.label" | trans }}</a>
</div>
<input type="hidden" name="crontype" class="crontype" value="">
<input type="hidden" name="hosttype" class="hosttype" value="">
<input type="hidden" name="containertype" class="containertype" value="">
<button type="submit" class="btn btn-outline-primary">{{ "job.addedit.submit.label" | trans }}</button>
</form>
<div class="mb-3">
<a href="javascript:void(0);" class="btn btn-outline-primary addvar-btn">{{ "job.addedit.variables.add.label" | trans }}</a>
</div>
<input type="hidden" name="crontype" class="crontype" value="">
<input type="hidden" name="hosttype" class="hosttype" value="">
<input type="hidden" name="containertype" class="containertype" value="">
<button type="submit" class="btn btn-outline-primary">{{ "job.addedit.submit.label" | trans }}</button>
</form>
{% endblock %}

@ -25,7 +25,7 @@
<td class="d-block d-md-table-cell align-middle">{{ job.interval | interval }}</td>
<td class="d-block d-md-table-cell align-middle">{{ job.nextrun | date("d/m/Y H:i:s") }}</td>
<td class="text-md-end d-block d-md-table-cell align-middle">
<a href="javascript:void(0);" data-href="{{ path('job_runnow', {'id': job.id}) }}" class="runnow btn btn-outline-{% if job.data.running == true %}success{% elseif job.data.norun == true %}danger{% else %}primary{% endif %}{% if job.data.running == true %} disabled{% endif %}"><i class="icon icon-run" aria-hidden="true"></i></a>
<a href="javascript:void(0);" data-nextrun="{{ job.nextrun | date("Y/m/d H:i:s") }}" data-href="{{ path('job_run', {'id': job.id}) }}" class="run btn btn-outline-{% if job.data.running == true %}success{% elseif job.data.norun == true %}danger{% else %}primary{% endif %}{% if job.data.running == true %} disabled{% endif %}"><i class="icon icon-run" aria-hidden="true"></i></a>
<a href="{{ path('job_view', {'id': job.id}) }}" class="btn btn-outline-{% if job.data.running == true %}success{% elseif job.data.norun == true %}danger{% else %}primary{% endif %}"><i class="icon icon-view" aria-hidden="true"></i></a>
<a href="{{ path('job_edit', {'id': job.id}) }}" class="btn btn-outline-{% if job.data.running == true %}success{% elseif job.data.norun == true %}danger{% else %}primary{% endif %}"><i class="icon icon-edit" aria-hidden="true"></i></a>
<a href="javascript:void(0);" data-confirmation="Are you sure you want to delete this job?" data-href="{{ path('job_delete', {'id': job.id}) }}" class="delete-btn btn btn-outline-{% if job.data.running == true %}success{% elseif job.data.norun == true %}danger{% else %}primary{% endif %}"><i class="icon icon-delete" aria-hidden="true"></i></a>
@ -34,7 +34,31 @@
{% endfor %}
</tbody>
</table>
<div class="modal fade" id="runnow_result" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal fade" id="run_selecttime" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="staticBackdropLabel">{{ "job.index.run.selecttime.header" | trans }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"></span>
</button>
</div>
<div class="modal-body">
<p>{{ "job.index.run.selecttime.description" | trans }}</p>
<div class="d-flex flex-column align-items-center selecttimepickers">
<div id="selecttime_datepicker">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary schedule" data-bs-dismiss="modal">{{ "job.index.run.selecttime.btnschedule.label" | trans }}</button>
<button type="button" class="btn btn-outline-success run-now" data-bs-dismiss="modal">{{ "job.index.run.selecttime.btnrunnow.label" | trans }}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="run_result" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
@ -47,7 +71,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-outline-primary" data-bs-dismiss="modal">{{ "job.index.run.ran.btnclose.label" | trans }}</button>
</div>
</div>
</div>
@ -55,10 +79,10 @@
{% endblock %}
{% block extrahtml %}
<div class="runnow-overlay d-none">
<div class="runnow-content">
<div class="run-overlay d-none">
<div class="run-content">
</div>
<div class="runnow-blur"></div>
<div class="run-blur"></div>
</div>
{% endblock %}

@ -1,7 +1,8 @@
{% extends "base.html.twig" %}
{% block title %}{{ 'job.title' | trans({ '_jobname_': job.name }) }}{% endblock %}
{% block title %}{{ 'job.view.title' | trans({ '_jobname_': job.name }) }}{% endblock %}
{% block content %}
<h2>{{ 'job.view.header' | trans({ '_jobname_': (job.name | parsetags) }) | raw }}</h2>
<p class="text-muted small">{{ 'job.view.webhookurl' | trans }}: {{ url('webhook', {id: job.id, token: job.data('hooktoken') }) }}</p>
<p>
<a href="{{ path('job_edit', { id: job.id }) }}">{{ 'job.view.edit' | trans }}</a>
{% if allruns %} | <a href="{{ path('job_view', { id: job.id })}}">{{ 'job.view.show.onlyfailed' | trans }}</a>
@ -18,6 +19,8 @@
<div class="d-md-inline d-block text-left">({{ "job.view.results.exitcode"| trans }}: {{ run.exitcode }} | {{ "job.view.results.runtime"| trans }}: {{ run.runtime | interval }})</div>
{% if 'M' in run.flags %}
<div class="d-md-inline d-block text-left">{{ "job.view.results.manual"| trans }}</div>
{% elseif 'T' in run.flags %}
<div class="d-md-inline d-block text-left">{{ "job.view.results.webhook"| trans }}</div>
{% endif %}
</div>
</button>

@ -161,26 +161,6 @@
<source>job.index.table.headers.nextrun</source>
<target>Next run</target>
</trans-unit>
<trans-unit id="n9XSrh3" resname="job.index.runnow.deferred.title">
<source>job.index.runnow.deferred.title</source>
<target>Cronjob has been scheduled</target>
</trans-unit>
<trans-unit id="2QVNoEx" resname="job.index.runnow.deferred.message">
<source>job.index.runnow.deferred.message</source>
<target>Job was scheduled to be run. You will find the output soon in the job details</target>
</trans-unit>
<trans-unit id="_9vL1xO" resname="job.index.runnow.ran.title.success">
<source>job.index.runnow.ran.title.success</source>
<target>Cronjob succesfully ran</target>
</trans-unit>
<trans-unit id="SH3oarr" resname="job.index.runnow.ran.title.failed">
<source>job.index.runnow.ran.title.failed</source>
<target>Cronjob failed. Please check output below</target>
</trans-unit>
<trans-unit id="az3XJT4" resname="job.index.runnow.ran.message">