diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue
index d694b5ca..94b81267 100644
--- a/frontend/layouts/default.vue
+++ b/frontend/layouts/default.vue
@@ -90,6 +90,11 @@
Log Suppression
+
+
+
+ Backups
+
diff --git a/frontend/pages/backend/[backend]/index.vue b/frontend/pages/backend/[backend]/index.vue
index 843f6293..34bd3901 100644
--- a/frontend/pages/backend/[backend]/index.vue
+++ b/frontend/pages/backend/[backend]/index.vue
@@ -84,8 +84,9 @@
-
- {{ moment.unix(history.updated).fromNow() }}
+
+ {{ moment.unix(history.updated_at ?? history.updated).fromNow() }}
@@ -139,7 +140,7 @@
diff --git a/frontend/pages/console.vue b/frontend/pages/console.vue
index 25919d09..54c409d7 100644
--- a/frontend/pages/console.vue
+++ b/frontend/pages/console.vue
@@ -60,6 +60,13 @@
input. You should use the command directly, For example i.e db:list --output yaml
+
+
+
+ The command contains [...]
which are considered a placeholder, So, please replace
+ [...]
with the intended value if applicable.
+
+
@@ -121,6 +128,7 @@ const command = ref(fromCommand);
const isLoading = ref(false);
const outputConsole = ref();
const hasPrefix = computed(() => command.value.startsWith('console') || command.value.startsWith('docker'));
+const hasPlaceholder = computed(() => command.value && command.value.match(/\[.*\]/));
const show_page_tips = useStorage('show_page_tips', true)
const RunCommand = async () => {
@@ -136,6 +144,13 @@ const RunCommand = async () => {
return
}
+ // use regex to check if command contains [...]
+ if (userCommand.match(/\[.*\]/)) {
+ if (!confirm(`The command contains placeholders '[...]'. Are you sure you want to run as it is?`)) {
+ return
+ }
+ }
+
response.value = []
const searchParams = new URLSearchParams();
@@ -165,6 +180,15 @@ const finished = () => {
}
isLoading.value = false;
+
+ const route = useRoute();
+ const router = useRouter();
+
+ if (route.query?.cmd || route.query?.task) {
+ route.query.cmd = '';
+ route.query.task = '';
+ router.push({path: '/console'});
+ }
}
onUpdated(() => outputConsole.value.scrollTop = outputConsole.value.scrollHeight);
diff --git a/frontend/pages/history/[id]/index.vue b/frontend/pages/history/[id]/index.vue
index 661a348e..2b132009 100644
--- a/frontend/pages/history/[id]/index.vue
+++ b/frontend/pages/history/[id]/index.vue
@@ -138,7 +138,8 @@
Updated:
-
+
{{ moment.unix(data.updated).fromNow() }}
@@ -226,6 +227,32 @@
+
+
+
+
+ Created:
+
+ {{ moment.unix(data.created_at).fromNow() }}
+
+
+
+
+
+
+
+
+
+ Updated:
+
+ {{ moment.unix(data.updated_at).fromNow() }}
+
+
+
+
+
@@ -300,7 +327,7 @@
Updated:
+ v-tooltip="`Backend last activity: ${getMoment(ag(data.extra, `${key}.received_at`, data.updated)).format(TOOLTIP_DATE_FORMAT)}`">
{{ getMoment(ag(data.extra, `${key}.received_at`, data.updated)).fromNow() }}
@@ -435,7 +462,16 @@
diff --git a/frontend/utils/index.js b/frontend/utils/index.js
index 48237e47..dabd1e08 100644
--- a/frontend/utils/index.js
+++ b/frontend/utils/index.js
@@ -4,6 +4,8 @@ const {notify} = useNotification();
const AG_SEPARATOR = '.'
+const TOOLTIP_DATE_FORMAT = 'YYYY-MM-DD h:mm:ss A'
+
const guid_links = {
'episode': {
'imdb': 'https://www.imdb.com/title/{_guid}',
@@ -425,4 +427,5 @@ export {
dEvent,
makeName,
makePagination,
+ TOOLTIP_DATE_FORMAT,
}
diff --git a/src/API/Logs/Index.php b/src/API/Logs/Index.php
index 89fc736b..36eb8ac2 100644
--- a/src/API/Logs/Index.php
+++ b/src/API/Logs/Index.php
@@ -186,7 +186,7 @@ private function download(string $filePath): iResponse
'Content-Type' => false === $mime ? 'application/octet-stream' : $mime,
'Content-Length' => filesize($filePath),
],
- body: stream::make($filePath, 'r')
+ body: Stream::make($filePath, 'r')
);
}
diff --git a/src/API/System/Backup.php b/src/API/System/Backup.php
new file mode 100644
index 00000000..6675b58a
--- /dev/null
+++ b/src/API/System/Backup.php
@@ -0,0 +1,86 @@
+ basename($file),
+ 'type' => $isAuto ? 'automatic' : 'manual',
+ 'size' => filesize($file),
+ 'date' => filemtime($file),
+ ];
+
+ $list[] = $builder;
+ }
+
+ $sorter = array_column($list, 'date');
+ array_multisort($sorter, SORT_DESC, $list);
+
+ foreach ($list as &$item) {
+ $item['date'] = makeDate(ag($item, 'date'));
+ }
+
+ return api_response(HTTP_STATUS::HTTP_OK, $list);
+ }
+
+ #[Route(['GET', 'DELETE'], self::URL . '/{filename}[/]', name: 'system.backup.view')]
+ public function logView(iRequest $request, array $args = []): iResponse
+ {
+ if (null === ($filename = ag($args, 'filename'))) {
+ return api_error('Invalid value for filename path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
+ }
+
+ $path = realpath(fixPath(Config::get('path') . '/backup'));
+
+ $filePath = realpath($path . '/' . $filename);
+
+ if (false === $filePath) {
+ return api_error('File not found.', HTTP_STATUS::HTTP_NOT_FOUND);
+ }
+
+ if (false === str_starts_with($filePath, $path)) {
+ return api_error('Invalid file path.', HTTP_STATUS::HTTP_BAD_REQUEST);
+ }
+
+ if ('DELETE' === $request->getMethod()) {
+ unlink($filePath);
+ return api_response(HTTP_STATUS::HTTP_OK);
+ }
+
+ $mime = (new finfo(FILEINFO_MIME_TYPE))->file($filePath);
+
+ return new Response(
+ status: HTTP_STATUS::HTTP_OK->value,
+ headers: [
+ 'Content-Type' => false === $mime ? 'application/octet-stream' : $mime,
+ 'Content-Length' => filesize($filePath),
+ ],
+ body: Stream::make($filePath, 'r')
+ );
+ }
+}
diff --git a/src/API/System/Command.php b/src/API/System/Command.php
index ba0f3a78..850b697d 100644
--- a/src/API/System/Command.php
+++ b/src/API/System/Command.php
@@ -18,8 +18,13 @@
final class Command
{
public const string URL = '%{api.prefix}/system/command';
+ private const int TIMES_BEFORE_PING = 6;
+ private const int PING_INTERVAL = 200000;
+
private int $counter = 1;
+ private bool $toBackground = false;
+
public function __construct()
{
set_time_limit(0);
@@ -59,10 +64,14 @@ public function __invoke(iRequest $request): iResponse
command: "{$path}/bin/console {$command}",
cwd: $path,
env: $_ENV,
- timeout: $data->get('timeout', 3600),
+ timeout: $data->get('timeout', 7200),
);
$process->start(callback: function ($type, $data) use ($process) {
+ if (true === $this->toBackground) {
+ return;
+ }
+
echo "id: " . hrtime(true) . "\n";
echo "event: data\n";
$data = (string)$data;
@@ -82,26 +91,26 @@ function ($data) {
flush();
- $this->counter = 3;
+ $this->counter = self::TIMES_BEFORE_PING;
if (ob_get_length() > 0) {
ob_end_flush();
}
if (connection_aborted()) {
- $process->stop(1, 9);
+ $this->toBackground = true;
}
});
- while ($process->isRunning()) {
- usleep(500000);
+ while (false === $this->toBackground && $process->isRunning()) {
+ usleep(self::PING_INTERVAL);
$this->counter--;
if ($this->counter > 1) {
continue;
}
- $this->counter = 3;
+ $this->counter = self::TIMES_BEFORE_PING;
echo "id: " . hrtime(true) . "\n";
echo "event: ping\n";
@@ -113,13 +122,13 @@ function ($data) {
}
if (connection_aborted()) {
- $process->stop(1, 9);
+ $this->toBackground = true;
}
}
} catch (ProcessTimedOutException) {
}
- if (!connection_aborted()) {
+ if (false === $this->toBackground && !connection_aborted()) {
echo "id: " . hrtime(true) . "\n";
echo "event: close\n";
echo 'data: ' . makeDate() . "\n\n";
@@ -132,16 +141,12 @@ function ($data) {
exit;
};
- return new Response(
- status: HTTP_STATUS::HTTP_OK->value,
- headers: [
- 'Content-Type' => 'text/event-stream',
- 'Cache-Control' => 'no-cache',
- 'Connection' => 'keep-alive',
- 'X-Accel-Buffering' => 'no',
- 'Last-Event-Id' => time(),
- ],
- body: StreamClosure::create($callable)
- );
+ return new Response(status: HTTP_STATUS::HTTP_OK->value, headers: [
+ 'Content-Type' => 'text/event-stream',
+ 'Cache-Control' => 'no-cache',
+ 'Connection' => 'keep-alive',
+ 'X-Accel-Buffering' => 'no',
+ 'Last-Event-Id' => time(),
+ ], body: StreamClosure::create($callable));
}
}
diff --git a/src/API/Tasks/Index.php b/src/API/Tasks/Index.php
index 0b583efd..844c5542 100644
--- a/src/API/Tasks/Index.php
+++ b/src/API/Tasks/Index.php
@@ -80,6 +80,9 @@ public function taskQueue(iRequest $request, array $args = []): iResponse
]);
}
+ /**
+ * @throws InvalidArgumentException
+ */
#[Get(self::URL . '/{id:[a-zA-Z0-9_-]+}[/]', name: 'tasks.task.view')]
public function taskView(iRequest $request, array $args = []): iResponse
{
@@ -93,7 +96,12 @@ public function taskView(iRequest $request, array $args = []): iResponse
return api_error('Task not found.', HTTP_STATUS::HTTP_NOT_FOUND);
}
- return api_response(HTTP_STATUS::HTTP_OK, Index::formatTask($task));
+ $queuedTasks = $this->cache->get('queued_tasks', []);
+
+ $data = Index::formatTask($task);
+ $data['queued'] = in_array(ag($task, 'name'), $queuedTasks);
+
+ return api_response(HTTP_STATUS::HTTP_OK, $data);
}
private function formatTask(array $task): array
diff --git a/src/Commands/System/PruneCommand.php b/src/Commands/System/PruneCommand.php
index 1fab1928..ee274614 100644
--- a/src/Commands/System/PruneCommand.php
+++ b/src/Commands/System/PruneCommand.php
@@ -77,33 +77,39 @@ protected function runCommand(InputInterface $input, OutputInterface $output): i
$directories = [
[
+ 'name' => 'logs_remover',
'path' => Config::get('tmpDir') . '/logs',
'base' => Config::get('tmpDir'),
'filter' => '*.log',
'time' => strtotime('-7 DAYS', $time)
],
[
+ 'name' => 'webhooks_remover',
'path' => Config::get('tmpDir') . '/webhooks',
'base' => Config::get('tmpDir'),
'filter' => '*.json',
'time' => strtotime('-3 DAYS', $time)
],
[
+ 'name' => 'profiler_remover',
'path' => Config::get('tmpDir') . '/profiler',
'base' => Config::get('tmpDir'),
'filter' => '*.json',
'time' => strtotime('-3 DAYS', $time)
],
[
+ 'name' => 'debug_remover',
'path' => Config::get('tmpDir') . '/debug',
'base' => Config::get('tmpDir'),
'filter' => '*.json',
'time' => strtotime('-3 DAYS', $time)
],
[
+ 'name' => 'backup_remover',
'path' => Config::get('path') . '/backup',
'base' => Config::get('path'),
'filter' => '*.*.json',
+ 'validate' => fn(SplFileInfo $f): bool => 1 === @preg_match('/\w+\.\d{8}\.json/i', $f->getBasename()),
'time' => strtotime('-9 DAYS', $time)
],
];
@@ -111,10 +117,12 @@ protected function runCommand(InputInterface $input, OutputInterface $output): i
$inDryRunMode = $input->getOption('dry-run');
foreach ($directories as $item) {
+ $name = ag($item, 'name');
$path = ag($item, 'path');
if (null === ($expiresAt = ag($item, 'time'))) {
- $this->logger->warning('Error No expected time to live was found for [{path}].', [
+ $this->logger->warning("No expected time to live was found for '{name}' - '{path}'.", [
+ 'name' => $name,
'path' => $path
]);
continue;
@@ -122,34 +130,48 @@ protected function runCommand(InputInterface $input, OutputInterface $output): i
if (null === $path || !is_dir($path)) {
if (true === (bool)ag($item, 'report', true)) {
- $this->logger->warning('Path [{path}] not found or inaccessible.', [
+ $this->logger->warning("{name}: Path '{path}' not found or is inaccessible.", [
+ 'name' => $name,
'path' => $path
]);
}
continue;
}
+ $validate = ag($item, 'validate', null);
+
foreach (glob(ag($item, 'path') . '/' . ag($item, 'filter')) as $file) {
$file = new SplFileInfo($file);
$fileName = $file->getBasename();
if ('.' === $fileName || '..' === $fileName || true === $file->isDir() || false === $file->isFile()) {
- $this->logger->debug('Path [{path}] is not considered valid file.', [
+ $this->logger->debug("{name}: Path '{path}' is not considered valid file.", [
+ 'name' => $name,
'path' => $file->getRealPath(),
]);
continue;
}
+ if (null !== $validate && false === $validate($file)) {
+ $this->logger->debug("{name}: File '{file}' did not pass validation checks.", [
+ 'name' => $name,
+ 'file' => after($file->getRealPath(), ag($item, 'base') . '/'),
+ ]);
+ continue;
+ }
+
if ($file->getMTime() > $expiresAt) {
- $this->logger->debug('File [{file}] Not yet expired. {ttl} left seconds.', [
+ $this->logger->debug("{name}: File '{file}' Not yet expired. '{ttl}' seconds left.", [
+ 'name' => $name,
'file' => after($file->getRealPath(), ag($item, 'base') . '/'),
'ttl' => number_format($file->getMTime() - $expiresAt),
]);
continue;
}
- $this->logger->notice('Removing [{file}].', [
+ $this->logger->notice("{name}: Removing '{file}'. expired TTL.", [
+ 'name' => $name,
'file' => after($file->getRealPath(), ag($item, 'base') . '/')
]);
diff --git a/src/Libs/Entity/StateEntity.php b/src/Libs/Entity/StateEntity.php
index b63cd917..37ae4db9 100644
--- a/src/Libs/Entity/StateEntity.php
+++ b/src/Libs/Entity/StateEntity.php
@@ -143,6 +143,14 @@ public function __construct(array $data)
$this->{$key} = $val;
}
+ if (0 === $this->updated_at && $this->updated > 0) {
+ $this->updated_at = $this->updated;
+ }
+
+ if (0 === $this->created_at && $this->updated > 0) {
+ $this->created_at = $this->updated;
+ }
+
$this->data = $this->getAll();
}