From 819d69d5bf621cee6d965407d975975231c909a3 Mon Sep 17 00:00:00 2001 From: Austin Best Date: Wed, 21 Aug 2024 23:36:00 -0400 Subject: [PATCH] WIP... Closes #43 Implement sqlite3 to replace settings and servers json files Add nightly branch Refactor notifications so multiples can be sent per platform Add a trait/interface function loader Add some defines for common platforms Fix the dialogClose function Some rewording --- .github/workflows/docker-publish.yml | 4 +- Dockerfile | 5 + README.md | 10 +- root/app/www/public/ajax/commands.php | 15 +- root/app/www/public/ajax/compose.php | 6 +- root/app/www/public/ajax/containers.php | 301 ++++++++------ root/app/www/public/ajax/login.php | 5 + root/app/www/public/ajax/notification.php | 385 +++++++++++------- root/app/www/public/ajax/overview.php | 10 +- root/app/www/public/ajax/settings.php | 148 ++++--- root/app/www/public/ajax/tasks.php | 28 +- root/app/www/public/api/index.php | 28 +- root/app/www/public/classes/Database.php | 101 +++++ root/app/www/public/classes/Docker.php | 57 +-- root/app/www/public/classes/Maintenance.php | 24 +- root/app/www/public/classes/Notifications.php | 114 ++++-- .../www/public/classes/interfaces/Docker.php | 50 +++ .../classes/interfaces/Notifications.php | 15 + .../traits/Database/ContainerGroupLink.php | 70 ++++ .../traits/Database/ContainerGroups.php | 80 ++++ .../traits/Database/ContainerSettings.php | 85 ++++ .../traits/Database/NotificationLink.php | 81 ++++ .../traits/Database/NotificationPlatform.php | 30 ++ .../traits/Database/NotificationTrigger.php | 61 +++ .../classes/traits/Database/Servers.php | 60 +++ .../classes/traits/Database/Settings.php | 73 ++++ .../classes/traits/Docker/Container.php | 13 +- .../traits/Notifications/Templates.php | 64 +++ .../traits/Notifications/notifiarr.php | 4 +- root/app/www/public/crons/health.php | 24 +- root/app/www/public/crons/housekeeper.php | 10 +- root/app/www/public/crons/prune.php | 17 +- root/app/www/public/crons/pulls.php | 24 +- root/app/www/public/crons/sse.php | 2 +- root/app/www/public/crons/state.php | 60 +-- root/app/www/public/crons/stats.php | 4 +- root/app/www/public/functions/api.php | 13 +- root/app/www/public/functions/common.php | 35 +- root/app/www/public/functions/containers.php | 28 +- root/app/www/public/functions/files.php | 4 +- .../www/public/functions/helpers/array.php | 5 + .../www/public/functions/notifications.php | 15 +- root/app/www/public/includes/constants.php | 20 +- root/app/www/public/includes/footer.php | 2 +- root/app/www/public/includes/header.php | 55 ++- root/app/www/public/index.php | 8 +- root/app/www/public/js/common.js | 37 +- root/app/www/public/js/containers.js | 13 +- root/app/www/public/js/notification.js | 122 +++++- root/app/www/public/js/settings.js | 2 +- root/app/www/public/loader.php | 57 +-- .../public/migrations/001_initial_setup.php | 274 +++++++++++++ root/app/www/public/sse.php | 2 +- root/app/www/public/startup.php | 18 +- root/etc/php82/conf.d/dockwatch.ini | 2 +- 55 files changed, 2076 insertions(+), 704 deletions(-) create mode 100644 root/app/www/public/classes/Database.php create mode 100644 root/app/www/public/classes/interfaces/Docker.php create mode 100644 root/app/www/public/classes/interfaces/Notifications.php create mode 100644 root/app/www/public/classes/traits/Database/ContainerGroupLink.php create mode 100644 root/app/www/public/classes/traits/Database/ContainerGroups.php create mode 100644 root/app/www/public/classes/traits/Database/ContainerSettings.php create mode 100644 root/app/www/public/classes/traits/Database/NotificationLink.php create mode 100644 root/app/www/public/classes/traits/Database/NotificationPlatform.php create mode 100644 root/app/www/public/classes/traits/Database/NotificationTrigger.php create mode 100644 root/app/www/public/classes/traits/Database/Servers.php create mode 100644 root/app/www/public/classes/traits/Database/Settings.php create mode 100644 root/app/www/public/classes/traits/Notifications/Templates.php create mode 100644 root/app/www/public/migrations/001_initial_setup.php diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4b42365..ce38013 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -4,11 +4,11 @@ name: Docker on: push: - branches: [ "main", "develop" ] + branches: [ "main", "develop", "nightly" ] # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main", "develop" ] + branches: [ "main", "develop", "nightly" ] env: # Use docker.io for Docker Hub if empty diff --git a/Dockerfile b/Dockerfile index 5ea2348..3f1befc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,11 @@ RUN \ # install sockets RUN apk add --no-cache \ php82-sockets + +# install sqlite3 +RUN apk add --no-cache \ + sqlite \ + php82-sqlite3 # add regctl for container digest checks ARG TARGETARCH diff --git a/README.md b/README.md index 9cfd62d..b31be72 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,12 @@ ## Purpose Simple UI driven way to manage updates & notifications for Docker containers. -No database required. All settings are stored locally in a volume mount. +No database container required, all settings are stored locally with sqlite3 + +## Branches +- `:main` Stable releases after testing on develop +- `:develop` Usually pretty stable +- `:nightly` Use at your own risk, suggest joining Discord to keep up to date with what is going on. Might not be able to hop back to develop or main!! ## Notification triggers @@ -21,12 +26,13 @@ No database required. All settings are stored locally in a volume mount. ## Notification platforms - Notifiarr +- Telegram (*coming soon*) ## Update options - Ignore -- Auto update - Check for updates +- Auto update ## Additional features diff --git a/root/app/www/public/ajax/commands.php b/root/app/www/public/ajax/commands.php index b5c9d4a..317c1be 100644 --- a/root/app/www/public/ajax/commands.php +++ b/root/app/www/public/ajax/commands.php @@ -42,10 +42,10 @@ $serverData) { + foreach ($serversTable as $serverId => $serverData) { ?> - + $serverData) { - if (in_array($serverIndex, $servers)) { + foreach ($serversTable as $serverId => $serverData) { + if (in_array($serverId, $servers)) { $serverOverride = $serverData; $apiResponse = apiRequest($_POST['command'], ['name' => $_POST['container'], 'params' => $_POST['parameters']]); - - if ($apiResponse['code'] == 200) { - $apiResponse = $apiResponse['response']['docker']; - } else { - $apiResponse = $apiResponse['code'] .': '. $apiResponse['error']; - } + $apiResponse = $apiResponse['code'] == 200 ? $apiResponse['response']['docker'] : $apiResponse['code'] .': '. $apiResponse['error']; ?>

diff --git a/root/app/www/public/ajax/compose.php b/root/app/www/public/ajax/compose.php index c03e65a..a33fde7 100644 --- a/root/app/www/public/ajax/compose.php +++ b/root/app/www/public/ajax/compose.php @@ -15,7 +15,7 @@ container_name: dockwatch image: ghcr.io/notifiarr/dockwatch:main ports: - - 9999:80/tcp + - ' . APP_PORT . ':80/tcp environment: - PGID=999 - TZ=America/New_York @@ -35,7 +35,7 @@ closedir($dir); if ($_POST['m'] == 'init') { - if ($_SESSION['serverIndex'] != 0) { + if ($_SESSION['serverId'] != APP_SERVER_ID) { echo 'Remote compose management is not supported. Please do that on the Dockwatch instance directly.'; } else { ?> @@ -176,4 +176,4 @@ } else { echo $up; } -} \ No newline at end of file +} diff --git a/root/app/www/public/ajax/containers.php b/root/app/www/public/ajax/containers.php index ad97f51..821f111 100644 --- a/root/app/www/public/ajax/containers.php +++ b/root/app/www/public/ajax/containers.php @@ -10,10 +10,13 @@ require 'shared.php'; if ($_POST['m'] == 'init') { - $dependencyFile = $docker->setContainerDependencies($processList); - $pulls = is_array($pullsFile) ? $pullsFile : json_decode($pullsFile, true); + $containersTable = $database->getContainers(); + $containerGroupsTable = $database->getContainerGroups(); + $containerLinksTable = $database->getContainerGroupLinks(); + $dependencyFile = $docker->setContainerDependencies($processList); + $pulls = is_array($pullsFile) ? $pullsFile : json_decode($pullsFile, true); + $pullsNotice = empty($pullsFile) ? true : false; array_sort_by_key($processList, 'Names'); - $pullsNotice = empty($pullsFile) ? true : false; ?>
@@ -26,7 +29,7 @@
- Real time updates: 60)' : 'disabled') ?> + Real time updates: 60)' : 'disabled' ?>
@@ -47,31 +50,36 @@ $containerGroup) { - $groupCPU = $groupMemory = 0; + $groupContainerHashes = []; + if ($containerLinksTable) { + foreach ($containerGroupsTable as $containerGroup) { + $groupHash = $containerGroup['hash']; + $groupContainers = $database->getGroupLinkContainersFromGroupId($containerLinksTable, $containersTable, $containerGroup['id']); + $groupCPU = $groupMemory = 0; foreach ($processList as $process) { $nameHash = md5($process['Names']); - if (in_array($nameHash, $containerGroup['containers'])) { - $memUsage = floatval(str_replace('%', '', $process['stats']['MemPerc'])); - $groupMemory += $memUsage; - - $cpuUsage = floatval(str_replace('%', '', $process['stats']['CPUPerc'])); - if (intval($settingsFile['global']['cpuAmount']) > 0) { - $cpuUsage = number_format(($cpuUsage / intval($settingsFile['global']['cpuAmount'])), 2); + foreach ($groupContainers as $groupContainer) { + if ($nameHash == $groupContainer['hash']) { + $memUsage = floatval(str_replace('%', '', $process['stats']['MemPerc'])); + $groupMemory += $memUsage; + + $cpuUsage = floatval(str_replace('%', '', $process['stats']['CPUPerc'])); + if (intval($settingsTable['cpuAmount']) > 0) { + $cpuUsage = number_format(($cpuUsage / intval($settingsTable['cpuAmount'])), 2); + } + $groupCPU += $cpuUsage; } - $groupCPU += $cpuUsage; } } ?> - + @@ -83,11 +91,12 @@ findContainer(['hash' => $_POST['hash'], 'data' => $stateFile]); - if ($_POST['action'] == 'stop' || $_POST['action'] == 'restart') { + if (str_equals_any($_POST['action'], ['stop', 'restart'])) { apiRequest('dockerStopContainer', [], ['name' => $container['Names']]); } - if ($_POST['action'] == 'start' || $_POST['action'] == 'restart') { + if (str_equals_any($_POST['action'], ['start', 'restart'])) { apiRequest('dockerStartContainer', [], ['name' => $container['Names']]); } $return = renderContainerRow($_POST['hash'], 'json'); - if ($_POST['action'] == 'start' || $_POST['action'] == 'restart') { + if (str_equals_any($_POST['action'], ['start', 'restart'])) { $return['length'] = 'Up 1 second'; } @@ -499,6 +501,10 @@ $processList = json_decode($processList['response']['docker'], true); array_sort_by_key($processList, 'Names'); + $containersTable = $database->getContainers(); + $containerGroupTable = $database->getContainerGroups(); + $containerGroupLinksTable = $database->getContainerGroupLinks(); + ?>
@@ -507,11 +513,11 @@
getContainerFromHash($nameHash, $containersTable); + $inGroup = ''; + + if ($containerGroupTable) { + foreach ($containerGroupTable as $containerGroup) { + $containersInGroup = $database->getGroupLinkContainersFromGroupId($containerGroupLinksTable, $containersTable, $containerGroup['id']); + + foreach ($containersInGroup as $containerInGroup) { + if ($containerInGroup['hash'] == $nameHash) { + $inGroup = $containerGroup['name']; + break; + } } } } ?> - + - + getContainers(); + $containerGroupTable = $database->getContainerGroups(); + $containerGroupLinksTable = $database->getContainerGroupLinks(); + foreach ($processList as $process) { $nameHash = md5($process['Names']); + $container = $database->getContainerFromHash($nameHash, $containersTable); $inGroup = ''; $inThisGroup = false; - if ($settingsFile['containerGroups']) { - foreach ($settingsFile['containerGroups'] as $groupHash => $groupContainers) { - if (in_array($nameHash, $groupContainers['containers'])) { - $inGroup = $groupContainers['name']; - if ($groupHash == $_POST['groupHash']) { - $inThisGroup = true; + if ($containerGroupTable) { + foreach ($containerGroupTable as $containerGroup) { + $containersInGroup = $database->getGroupLinkContainersFromGroupId($containerGroupLinksTable, $containersTable, $containerGroup['id']); + + foreach ($containersInGroup as $containerInGroup) { + if ($containerInGroup['hash'] == $nameHash) { + $inGroup = $containerGroup['name']; + + if ($containerGroup['id'] == $_POST['groupId']) { + $inGroup = '' . $containerGroup['name'] . ''; + $inThisGroup = true; + } + + break; } } } } + ?> - + - + getContainers(); + $containerGroupTable = $database->getContainerGroups(); + $containerGroupLinksTable = $database->getContainerGroupLinks(); + if ($_POST['delete']) { - unset($settingsFile['containerGroups'][$groupHash]); + $database->deleteContainerGroup($groupId); } else { - if ($_POST['selection'] == '1' && is_array($settingsFile['containerGroups'])) { - foreach ($settingsFile['containerGroups'] as $groupDetails) { - if (strtolower($groupDetails['name']) == strtolower($groupName)) { + if (!$groupId) { + foreach ($containerGroupTable as $containerGroup) { + if (str_compare('nocase', $containerGroup['name'], $groupName)) { $error = 'A group with that name already exists'; break; } } + + if (!$error) { + $groupId = $database->addContainerGroup($groupName); + + if (!$groupId) { + $error = 'Error creating the new \'' . $groupName . '\' group: ' . $database->error(); + } + } + } else { + foreach ($containerGroupTable as $containerGroup) { + if ($containerGroup['id'] == $groupId) { + if ($containerGroup['name'] != $groupName) { + $database->updateContainerGroup($groupId, ['name' => $database->prepare($groupName)]); + } + break; + } + } } if (!$error) { - $containers = []; - foreach ($_POST as $key => $val) { if (!str_contains($key, 'groupContainer')) { continue; } - list($junk, $containerHash) = explode('-', $key); - $containers[] = $containerHash; - } + list($junk, $containerId) = explode('-', $key); - $settingsFile['containerGroups'][$groupHash] = ['name' => $groupName, 'containers' => $containers]; - } - } + $linkExists = false; + foreach ($containerGroupLinksTable as $groupLink) { + if ($groupLink['group_id'] != $groupId) { + continue; + } + + if ($groupLink['container_id'] == $containerId) { + $linkExists = true; + break; + } + } - if (!$error) { - setServerFile('settings', $settingsFile); + if ($linkExists) { + if (!$val) { + $database->removeContainerGroupLink($groupId, $containerId); + } + } else { + if ($val) { + $database->addContainerGroupLink($groupId, $containerId); + } + } + } + } } echo $error; } if ($_POST['m'] == 'updateOptions') { - $processList = apiRequest('dockerProcessList', ['format' => true]); - $processList = json_decode($processList['response']['docker'], true); + $containersTable = $database->getContainers(); + $processList = apiRequest('dockerProcessList', ['format' => true]); + $processList = json_decode($processList['response']['docker'], true); array_sort_by_key($processList, 'Names'); ?> @@ -648,25 +709,24 @@ getContainerFromHash($nameHash, $containersTable); ?> - +

- Containers: + Containers:
   
Group
') ?>' ?> Not assigned' ?>
' : '') : '') ?>' : '') : '' ?> Not assigned' ?>
- - - +
- - -
-
- - - -
-
- - - -
+
+
+
+ + + +
+
+ + +
-
getContainers(); foreach ($_POST as $key => $val) { - preg_match('/container-update-([^-\n]+)|container-frequency-([^-\n]+)/', $key, $matches, PREG_OFFSET_CAPTURE); - if (!$matches) { + if (!str_contains($key, 'container-frequency-')) { continue; } - - $hash = $matches[1][0]; - if (!$hash || $hash == "all") { + + $hash = str_replace('container-frequency-', '', $key); + if (!$hash || $hash == 'all') { continue; } - list($minute, $hour, $dom, $month, $dow) = explode(' ', $_POST['container-frequency-' . $hash]); - $frequency = $minute . ' ' . $hour . ' ' . $dom . ' ' . $month . ' ' . $dow; + list($minute, $hour, $dom, $month, $dow) = explode(' ', $key); + $frequency = $minute . ' ' . $hour . ' ' . $dom . ' ' . $month . ' ' . $dow; + $updates = intval($_POST['container-update-' . $hash]); try { $cron = Cron\CronExpression::factory($frequency); @@ -722,16 +779,12 @@ $frequency = DEFAULT_CRON; } - $newSettings[$hash]['updates'] = $val; - $newSettings[$hash]['frequency'] = $frequency; - $newSettings[$hash]['restartUnhealthy'] = $settingsFile['containers'][$hash]['restartUnhealthy']; - $newSettings[$hash]['disableNotifications'] = $settingsFile['containers'][$hash]['disableNotifications']; - $newSettings[$hash]['shutdownDelay'] = $settingsFile['containers'][$hash]['shutdownDelay']; - $newSettings[$hash]['shutdownDelaySeconds'] = $settingsFile['containers'][$hash]['shutdownDelaySeconds']; + //-- ONLY UPDATE WHAT HAS CHANGED + $container = $database->getContainerFromHash($hash, $containersTable); + if ($container['updates'] != $updates || $container['frequency'] != $frequency) { + $database->updateContainer($hash, ['updates' => $updates, 'frequency' => $database->prepare($frequency)]); + } } - - $settingsFile['containers'] = $newSettings; - setServerFile('settings', $settingsFile); } if ($_POST['m'] == 'openEditContainer') { @@ -773,7 +826,7 @@ Web UI - This will create a dockwatch label, should be a valid URL (Ex: http://dockwatch or http://10.1.0.1:9999) + This will create a dockwatch label, should be a valid URL (Ex: http://dockwatch or http://10.1.0.1:) @@ -878,6 +931,8 @@ } if ($_POST['m'] == 'updateContainerOption') { - $settingsFile['containers'][$_POST['hash']][$_POST['option']] = $_POST['setting']; - $saveSettings = setServerFile('settings', $settingsFile); + $containersTable = $database->getContainers(); + $container = $database->getContainerGroupFromHash($_POST['hash'], $containersTable); + + $database->updateContainer($_POST['hash'], [$_POST['option'] => $database->prepare($_POST['setting'])]); } diff --git a/root/app/www/public/ajax/login.php b/root/app/www/public/ajax/login.php index 700a380..a4aaaa6 100644 --- a/root/app/www/public/ajax/login.php +++ b/root/app/www/public/ajax/login.php @@ -9,6 +9,11 @@ require 'shared.php'; +if ($_POST['m'] == 'resetSession') { + session_unset(); + session_destroy(); +} + if ($_POST['m'] == 'login') { logger(SYSTEM_LOG, 'login ->'); diff --git a/root/app/www/public/ajax/notification.php b/root/app/www/public/ajax/notification.php index 642e1e9..0e8511c 100644 --- a/root/app/www/public/ajax/notification.php +++ b/root/app/www/public/ajax/notification.php @@ -9,174 +9,281 @@ require 'shared.php'; -$triggers = [ - [ - 'name' => 'updated', - 'label' => 'Updated', - 'desc' => 'Send a notification when a container has had an update applied', - 'event' => 'updates' - ],[ - 'name' => 'updates', - 'label' => 'Updates', - 'desc' => 'Send a notification when a container has an update available', - 'event' => 'updates' - ],[ - 'name' => 'stateChange', - 'label' => 'State change', - 'desc' => 'Send a notification when a container has a state change (running -> down)', - 'event' => 'state' - ],[ - 'name' => 'added', - 'label' => 'Added', - 'desc' => 'Send a notification when a container is added', - 'event' => 'state' - ],[ - 'name' => 'removed', - 'label' => 'Removed', - 'desc' => 'Send a notification when a container is removed', - 'event' => 'state' - ],[ - 'name' => 'prune', - 'label' => 'Prune', - 'desc' => 'Send a notification when an image or volume is pruned', - 'event' => 'prune' - ],[ - 'name' => 'cpuHigh', - 'label' => 'CPU usage', - 'desc' => 'Send a notification when container CPU usage exceeds threshold (set in Settings)', - 'event' => 'usage' - ],[ - 'name' => 'memHigh', - 'label' => 'Memory usage', - 'desc' => 'Send a notification when container memory usage exceeds threshold (set in Settings)', - 'event' => 'usage' - ],[ - 'name' => 'health', - 'label' => 'Health change', - 'desc' => 'Send a notification when container becomes unhealthy', - 'event' => 'health' - ] - ]; - if ($_POST['m'] == 'init') { - $notificationPlatforms = $notifications->getPlatforms(); + $notificationPlatformTable = $database->getNotificationPlatforms(); + $notificationTriggersTable = $database->getNotificationTriggers(); + $notificationLinkTable = $database->getNotificationLinks(); + ?> -
-
-

Triggers

-
- - - - - - - - - - - - - - - - - - - - +
+ Platforms +
+ ' : 'Coming soon!'; + + ?> +
+
+
+

+
+
+
+ +
+
+ +
+
+ Configured senders +
+ +
+
+ Notifications have not been setup yet, click the plus icon above to set them up. +
+
+ + -
-
NotificationDescriptionWebhook EventPlatform
class="form-check-input notification-check" id="notifications-name-"> - -
+
+
+
+

+ + + +

+
+ You have not configured any triggers for this notificationgetNotificationTriggerNameFromId($triggerId, $notificationTriggersTable); + $enabledTriggers[] = $trigger; + } + + echo '
Enabled: ' . implode(', ', $enabledTriggers) . '
'; + } + ?> +
+
+
+
+ +
-
-

Platforms

- $platform) { ?> -
-
- - + + getNotificationPlatforms(); + $notificationTriggersTable = $database->getNotificationTriggers(); + $notificationLinkTable = $database->getNotificationLinks(); + $platformParameters = json_decode($notificationPlatformTable[$_POST['platformId']]['parameters'], true); + $platformName = $notifications->getNotificationPlatformNameFromId($_POST['platformId'], $notificationPlatformTable); + $linkRow = $notificationLinkTable[$_POST['linkId']]; + $existingTriggers = $existingParameters = []; + + if ($linkRow) { + $existingTriggers = $linkRow['trigger_ids'] ? json_decode($linkRow['trigger_ids'], true) : []; + $existingParameters = $linkRow['platform_parameters'] ? json_decode($linkRow['platform_parameters'], true) : []; + $existingName = $linkRow['name']; + } + + ?> +
+

+
+
+ + + + + + + + + + - - - + + + + - - - + + +
TriggerDescriptionEvent
NameSettingDescription type="checkbox" class="form-check-input notification-trigger" id="notificationTrigger-">
+ + + + + + + + + + + + + $platformParameterData) { + ?> - - - + + - - -
Setting
+ Name Required
+ The name of this notification sender +
*' : '') ?>Required' : '') ?>
+ type="text" id="notificationPlatformParameter-" class="form-control" value=""> +
+ + + +
+
+ + + + + +
- -
-
-
$val) { - if (!str_contains($key, '-name-')) { - continue; +if ($_POST['m'] == 'addNotification') { + if (!$_POST['platformId']) { + $error = 'Missing required platform id'; + } + + if (!$error) { + $notificationPlatformTable = $database->getNotificationPlatforms(); + $notificationTriggersTable = $database->getNotificationTriggers(); + $notificationLinkTable = $database->getNotificationLinks(); + $platformParameters = json_decode($notificationPlatformTable[$_POST['platformId']]['parameters'], true); + $platformName = $notifications->getNotificationPlatformNameFromId($_POST['platformId'], $notificationPlatformTable); + + //-- CHECK FOR REQUIRED FIELDS + foreach ($platformParameters as $platformParameterField => $platformParameterData) { + if ($platformParameterData['required'] && !$_POST['notificationPlatformParameter-' . $platformParameterField]) { + $error = 'Missing required platform field: ' . $platformParameterData['label']; + break; + } } - $type = str_replace('notifications-name-', '', $key); - $newSettings[$type] = [ - 'active' => trim($val), - 'platform' => $_POST['notifications-platform-' . $type] - ]; - } - $settingsFile['notifications']['triggers'] = $newSettings; + if (!$error) { + $triggerIds = $platformParameters = []; + $senderName = $platformName; - //-- PLATFORM SETTINGS - $newSettings = []; - foreach ($_POST as $key => $val) { - $strip = str_replace('notifications-platform-', '', $key); - list($platformId, $platformField) = explode('-', $strip); + foreach ($_POST as $key => $val) { + if (str_contains($key, 'notificationTrigger-') && $val) { + $triggerIds[] = str_replace('notificationTrigger-', '', $key); + } - if (!is_numeric($platformId)) { - continue; + if (str_contains($key, 'notificationPlatformParameter-')) { + $field = str_replace('notificationPlatformParameter-', '', $key); + + if ($field != 'name') { + $platformParameters[$field] = $val; + } else { + $senderName = $val; + } + } + } + + $database->addNotificationLink($_POST['platformId'], $triggerIds, $platformParameters, $senderName); } + } - $newSettings[$platformId][$platformField] = trim($val); + echo json_encode(['error' => $error]); +} + +if ($_POST['m'] == 'saveNotification') { + if (!$_POST['platformId']) { + $error = 'Missing required platform id'; + } + if (!$_POST['linkId']) { + $error = 'Missing required link id'; } - $settingsFile['notifications']['platforms'] = $newSettings; - $saveSettings = setServerFile('settings', $settingsFile); + if (!$error) { + $notificationPlatformTable = $database->getNotificationPlatforms(); + $notificationTriggersTable = $database->getNotificationTriggers(); + $notificationLinkTable = $database->getNotificationLinks(); + $platformParameters = json_decode($notificationPlatformTable[$_POST['platformId']]['parameters'], true); + $platformName = $notifications->getNotificationPlatformNameFromId($_POST['platformId'], $notificationPlatformTable); + + //-- CHECK FOR REQUIRED FIELDS + foreach ($platformParameters as $platformParameterField => $platformParameterData) { + if ($platformParameterData['required'] && !$_POST['notificationPlatformParameter-' . $platformParameterField]) { + $error = 'Missing required platform field: ' . $platformParameterData['label']; + break; + } + } + + if (!$error) { + $triggerIds = $platformParameters = []; + $senderName = $platformName; - if ($saveSettings['code'] != 200) { - $error = 'Error saving notification settings on server ' . ACTIVE_SERVER_NAME; + foreach ($_POST as $key => $val) { + if (str_contains($key, 'notificationTrigger-') && $val) { + $triggerIds[] = str_replace('notificationTrigger-', '', $key); + } + + if (str_contains($key, 'notificationPlatformParameter-')) { + $field = str_replace('notificationPlatformParameter-', '', $key); + + if ($field != 'name') { + $platformParameters[$field] = $val; + } else { + $senderName = $val; + } + } + } + + $database->updateNotificationLink($_POST['linkId'], $triggerIds, $platformParameters, $senderName); + } } - echo json_encode(['error' => $error, 'server' => ACTIVE_SERVER_NAME]); + echo json_encode(['error' => $error]); +} + +if ($_POST['m'] == 'deleteNotification') { + $database->deleteNotificationLink($_POST['linkId']); } if ($_POST['m'] == 'testNotify') { - $apiResponse = apiRequest('testNotify', [], ['platform' => $_POST['platform']]); + $apiResponse = apiRequest('testNotify', [], ['linkId' => $_POST['linkId']]); if ($apiResponse['code'] == 200) { $result = 'Test notification sent on server ' . ACTIVE_SERVER_NAME; diff --git a/root/app/www/public/ajax/overview.php b/root/app/www/public/ajax/overview.php index 70c3208..23e910f 100644 --- a/root/app/www/public/ajax/overview.php +++ b/root/app/www/public/ajax/overview.php @@ -83,8 +83,8 @@ $network += bytesFromString($netUsed); } - if (intval($settingsFile['global']['cpuAmount']) > 0) { - $cpuActual = number_format(($cpu / intval($settingsFile['global']['cpuAmount'])), 2); + if (intval($settingsTable['cpuAmount']) > 0) { + $cpuActual = number_format(($cpu / intval($settingsTable['cpuAmount'])), 2); } ?> @@ -98,7 +98,7 @@
Running:
Stopped:
- Total: + Total:
@@ -122,7 +122,7 @@
Up to date:
Outdated:
- Unchecked: + Unchecked:
@@ -197,4 +197,4 @@ Server name - + The name of this server, also passed in the notification payload Maintenance IP - + This IP is used to do updates/restarts for Dockwatch. It will create another container dockwatch-maintenance with this IP and after it has updated/restarted Dockwatch it will be removed. This is only required if you do static IP assignment for your containers. Maintenance port - + This port is used to do updates/restarts for Dockwatch. It will create another container dockwatch-maintenance with this port and after it has updated/restarted Dockwatch it will be removed. -

Login Failures

+

Login failures

@@ -81,7 +80,7 @@
-

Server list

+

Instances list

@@ -93,7 +92,7 @@ @@ -102,21 +101,21 @@ } else { ?> - - - + + + 1) { - foreach ($serversFile as $serverIndex => $serverSettings) { - if ($serverIndex == 0) { + if (count($serversTable) > 1) { + foreach ($serversTable as $serverSettings) { + if ($serverSettings['id'] == APP_SERVER_ID) { continue; } ?> - - - + + +
Sorry, remote management of server access is not allowed. Go to the server to make those changes.
-

New Containers

+

New containers

@@ -148,18 +147,18 @@
Updates1 - + What settings to use for new containers that are added
-

Auto Prune

+

Auto prune

@@ -173,21 +172,21 @@ @@ -198,7 +197,7 @@ '. $x .''; + $option .= ''; } echo $option; ?> @@ -223,21 +222,21 @@ @@ -258,7 +257,7 @@ @@ -279,28 +278,28 @@ @@ -322,25 +321,25 @@ - + - + @@ -358,7 +357,6 @@ } if ($_POST['m'] == 'saveGlobalSettings') { - $currentSettings = getServerFile('settings'); $newSettings = []; foreach ($_POST as $key => $val) { @@ -378,7 +376,7 @@ } //-- ENVIRONMENT SWITCHING - if ($currentSettings['global']['environment'] != $_POST['environment']) { + if ($settingsTable['environment'] != $_POST['environment']) { if ($_POST['environment'] == 0) { //-- USE INTERNAL linkWebroot('internal'); } else { //-- USE EXTERNAL @@ -386,18 +384,15 @@ } } - $settingsFile['global'] = $newSettings; - $saveSettings = setServerFile('settings', $settingsFile); - - if ($saveSettings['code'] != 200) { - $error = 'Error saving global settings on server ' . ACTIVE_SERVER_NAME; - } + $settingsTable = $database->setSettings($newSettings, $settingsTable); //-- ONLY MAKE SERVER CHANGES ON LOCAL - if ($_SESSION['serverIndex'] == 0) { + if ($_SESSION['serverId'] == APP_SERVER_ID) { + $serverList = []; + //-- ADD SERVER TO LIST if ($_POST['serverList-name-new'] && $_POST['serverList-url-new'] && $_POST['serverList-apikey-new']) { - $serversFile[] = ['name' => $_POST['serverList-name-new'], 'url' => rtrim($_POST['serverList-url-new'], '/'), 'apikey' => $_POST['serverList-apikey-new']]; + $serverList[] = ['name' => $_POST['serverList-name-new'], 'url' => rtrim($_POST['serverList-url-new'], '/'), 'apikey' => $_POST['serverList-apikey-new']]; } //-- UPDATE SERVER LIST @@ -406,14 +401,14 @@ continue; } - list($name, $field, $index) = explode('-', $key); + list($name, $field, $instanceId) = explode('-', $key); - if (!is_numeric($index)) { + if (!is_numeric($instanceId)) { continue; } - if ($_POST['serverList-name-' . $index] && $_POST['serverList-url-' . $index] && $_POST['serverList-apikey-' . $index]) { - $serversFile[$index] = ['name' => $_POST['serverList-name-' . $index], 'url' => rtrim($_POST['serverList-url-' . $index], '/'), 'apikey' => $_POST['serverList-apikey-' . $index]]; + if ($_POST['serverList-name-' . $instanceId] && $_POST['serverList-url-' . $instanceId] && $_POST['serverList-apikey-' . $instanceId]) { + $serverList[$instanceId] = ['name' => $_POST['serverList-name-' . $instanceId], 'url' => rtrim($_POST['serverList-url-' . $instanceId], '/'), 'apikey' => $_POST['serverList-apikey-' . $instanceId]]; } } @@ -423,41 +418,44 @@ continue; } - list($name, $field, $index) = explode('-', $key); + list($name, $field, $instanceId) = explode('-', $key); - if (!is_numeric($index)) { + if (!is_numeric($instanceId)) { continue; } - if (!$_POST['serverList-name-' . $index] && !$_POST['serverList-url-' . $index] && !$_POST['serverList-apikey-' . $index]) { - unset($serversFile[$index]); + if (!$_POST['serverList-name-' . $instanceId] && !$_POST['serverList-url-' . $instanceId] && !$_POST['serverList-apikey-' . $instanceId]) { + $serverList[$instanceId]['remove'] = true; } } - $saveServers = setServerFile('servers', $serversFile); - - if ($saveServers['code'] != 200) { - $error = 'Error saving server list on server ' . ACTIVE_SERVER_NAME; - } - } else { - $serversFile = getFile(SERVERS_FILE); + $serversTable = $database->setServers($serverList); } - $serverList = ''; - foreach ($serversFile as $serverIndex => $serverDetails) { - $ping = curl($serverDetails['url'] . '/api/?request=ping', ['x-api-key: ' . $serverDetails['apikey']]); + $serverList = ''; + $serverList .= ' '; + + $_SESSION['serverList'] = $serverList; + $_SESSION['serverListUpdated'] = time(); echo json_encode(['error' => $error, 'server' => ACTIVE_SERVER_NAME, 'serverList' => $serverList]); } //-- CALLED FROM THE NAV MENU SELECT if ($_POST['m'] == 'updateServerIndex') { - $_SESSION['serverIndex'] = intval($_POST['index']); - $_SESSION['serverList'] = ''; -} \ No newline at end of file + $_SESSION['serverId'] = intval($_POST['index']); + $_SESSION['serverList'] = ''; +} diff --git a/root/app/www/public/ajax/tasks.php b/root/app/www/public/ajax/tasks.php index 631febb..b192f7a 100644 --- a/root/app/www/public/ajax/tasks.php +++ b/root/app/www/public/ajax/tasks.php @@ -26,43 +26,43 @@ - + - + - + - + - + - + - + @@ -141,17 +141,11 @@ } if ($_POST['m'] == 'updateTaskDisabled') { - if ($_POST['task'] == 'sse') { - $settingsFile['global']['sseEnabled'] = !intval($_POST['disabled']); + if ($_POST['task'] == 'sseEnabled') { + $database->setSetting('sseEnabled', !intval($_POST['disabled'])); } else { - $settingsFile['tasks'][$_POST['task']] = ['disabled' => intval($_POST['disabled'])]; - } - - $saveSettings = setServerFile('settings', $settingsFile); - - if ($saveSettings['code'] != 200) { - $error = 'Error saving task settings on server ' . ACTIVE_SERVER_NAME; + $database->setSetting($_POST['task'], intval($_POST['disabled'])); } echo json_encode(['error' => $error, 'server' => ACTIVE_SERVER_NAME]); -} \ No newline at end of file +} diff --git a/root/app/www/public/api/index.php b/root/app/www/public/api/index.php index e000dc0..c54d593 100644 --- a/root/app/www/public/api/index.php +++ b/root/app/www/public/api/index.php @@ -19,7 +19,7 @@ $code = 200; $apikey = $_SERVER['HTTP_X_API_KEY'] ? $_SERVER['HTTP_X_API_KEY'] : $_GET['apikey']; -if ($apikey != $serversFile[0]['apikey']) { +if ($apikey != $serversTable[APP_SERVER_ID]['apikey']) { apiResponse(401, ['error' => 'Invalid apikey']); } @@ -29,8 +29,8 @@ case $_GET['request']: //-- GETTERS switch ($_GET['request']) { - case 'dwStats': - $response = getStats(); + case 'dependency': + $response = ['dependency' => getFile(DEPENDENCY_FILE)]; break; case 'dockerAutoCompose': if (!$_GET['name']) { @@ -115,6 +115,15 @@ case 'dockerStats': $response = ['docker' => $docker->stats($_GET['useCache'])]; break; + case 'dwStats': + $response = getStats(); + break; + case 'getServersTable': + $response = ['servers' => $serversTable]; + break; + case 'getSettingsTable': + $response = ['settings' => $settingsTable]; + break; case 'health': $response = ['health' => getFile(HEALTH_FILE)]; break; @@ -124,8 +133,9 @@ case 'pull': $response = ['pull' => getFile(PULL_FILE)]; break; - case 'settings': - $response = ['settings' => getFile(SETTINGS_FILE)]; + case 'runMigrations': + $database->migrations(); + $response = ['migrations' => []]; break; case 'state': $response = ['state' => getFile(STATE_FILE)]; @@ -133,12 +143,6 @@ case 'stats': $response = ['state' => getFile(STATS_FILE)]; break; - case 'servers': - $response = ['servers' => getFile(SERVERS_FILE)]; - break; - case 'dependency': - $response = ['dependency' => getFile(DEPENDENCY_FILE)]; - break; case 'sse': $response = ['sse' => getFile(SSE_FILE)]; break; @@ -296,7 +300,7 @@ $response = ['result' => executeTask($_POST['task'])]; break; case 'testNotify': - $testNotification = sendTestNotification($_POST['platform']); + $testNotification = sendTestNotification($_POST['linkId']); if ($testNotification) { $code = '400'; diff --git a/root/app/www/public/classes/Database.php b/root/app/www/public/classes/Database.php new file mode 100644 index 0000000..44a34a5 --- /dev/null +++ b/root/app/www/public/classes/Database.php @@ -0,0 +1,101 @@ +connect(DATABASE_PATH . 'dockwatch.db'); + } + + public function connect($dbFile) + { + $db = new SQLite3($dbFile, SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE); + $this->db = $db; + + return $db; + } + + public function query($query) + { + return $this->db->query($query); + } + + public function fetchAssoc($res) + { + return !$res ? [] : $res->fetchArray(SQLITE3_ASSOC); + } + + public function affectedRows($res) + { + return !$res ? 0 : $res->changes(SQLITE3_ASSOC); + } + + public function insertId() + { + return $this->db->lastInsertRowID(); + } + + public function error() + { + return $this->db->lastErrorMsg(); + } + + public function prepare($in) + { + $out = addslashes(stripslashes($in)); + return $out; + } + + public function migrations() + { + if (ACTIVE_SERVER_ID != APP_SERVER_ID) { + return apiRequest('runMigrations'); + } + + $database = $this; + $db = $this->db; + + if (filesize(DATABASE_PATH . 'dockwatch.db') == 0) { //-- INITIAL SETUP + logger(SYSTEM_LOG, 'Creating database and applying migration 001_initial_setup'); + require MIGRATIONS_PATH . '001_initial_setup.php'; + } else { //-- GET CURRENT MIGRATION & CHECK FOR NEEDED MIGRATIONS + $dir = opendir(MIGRATIONS_PATH); + while ($migration = readdir($dir)) { + if (substr($migration, 0, 3) > $this->getSetting('migration') && str_contains($migration, '.php')) { + logger(SYSTEM_LOG, 'Applying migration ' . $migration); + require MIGRATIONS_PATH . $migration; + } + } + closedir($dir); + } + } +} diff --git a/root/app/www/public/classes/Docker.php b/root/app/www/public/classes/Docker.php index 433bd9b..ad469bf 100644 --- a/root/app/www/public/classes/Docker.php +++ b/root/app/www/public/classes/Docker.php @@ -8,14 +8,7 @@ */ //-- BRING IN THE TRAITS -$traits = ABSOLUTE_PATH . 'classes/traits/Docker/'; -$traitsDir = opendir($traits); -while ($traitFile = readdir($traitsDir)) { - if (str_contains($traitFile, '.php')) { - require $traits . $traitFile; - } -} -closedir($traitsDir); +loadClassExtras('Docker'); class Docker { @@ -27,10 +20,14 @@ class Docker use Volume; protected $shell; + protected $database; public function __construct() { - $this->shell = new Shell(); + global $shell, $database; + + $this->shell = $shell ?? new Shell(); + $this->database = $database ?? new Database(); } public function stats($useCache) @@ -104,45 +101,3 @@ public function isIO($name) return str_contains($name, '/') ? $name : 'library/' . $name; } } - -interface DockerApi -{ - //-- CONTAINER SPECIFIC - public const STOP_CONTAINER = '/containers/%s/stop'; - public const CREATE_CONTAINER = '/containers/create?name=%s'; -} - -//-- https://docs.docker.com/reference/cli/docker -interface DockerSock -{ - //-- GENERAL - public const VERSION = '/usr/bin/docker version %s'; - public const RUN = '/usr/bin/docker run %s'; - public const LOGS = '/usr/bin/docker logs %s'; - public const PROCESSLIST_FORMAT = '/usr/bin/docker ps --all --no-trunc --size=false --format="{{json . }}" | jq -s --tab .'; - public const PROCESSLIST_CUSTOM = '/usr/bin/docker ps %s'; - public const STATS_FORMAT = '/usr/bin/docker stats --all --no-trunc --no-stream --format="{{json . }}" | jq -s --tab .'; - public const INSPECT_FORMAT = '/usr/bin/docker inspect %s --format="{{json . }}" | jq -s --tab .'; - public const INSPECT_CUSTOM = '/usr/bin/docker inspect %s %s'; - public const IMAGE_SIZES = '/usr/bin/docker images --format=\'{"ID":"{{ .ID }}", "Size": "{{ .Size }}"}\' | jq -s --tab .'; - //-- CONTAINER SPECIFIC - public const REMOVE_CONTAINER = '/usr/bin/docker container rm -f %s'; - public const START_CONTAINER = '/usr/bin/docker container start %s'; - public const STOP_CONTAINER = '/usr/bin/docker container stop %s%s'; - public const ORPHAN_CONTAINERS = '/usr/bin/docker images -f dangling=true --format="{{json . }}" | jq -s --tab .'; - public const CONTAINER_PORT = '/usr/bin/docker port %s %s'; - //-- IMAGE SPECIFIC - public const REMOVE_IMAGE = '/usr/bin/docker image rm %s'; - public const PULL_IMAGE = '/usr/bin/docker image pull %s'; - public const PRUNE_IMAGE = '/usr/bin/docker image prune -af'; - //-- VOLUME SPECIFIC - public const ORPHAN_VOLUMES = '/usr/bin/docker volume ls -qf dangling=true --format="{{json . }}" | jq -s --tab .'; - public const PRUNE_VOLUME = '/usr/bin/docker volume prune -af'; - public const REMOVE_VOLUME = '/usr/bin/docker volume rm %s'; - //-- NETWORK SPECIFIC - public const ORPHAN_NETWORKS = '/usr/bin/docker network ls -q --format="{{json . }}" | jq -s --tab .'; - public const INSPECT_NETWORK = '/usr/bin/docker network inspect %s --format="{{json . }}" | jq -s --tab .'; - public const PRUNE_NETWORK = '/usr/bin/docker network prune -af'; - public const REMOVE_NETWORK = '/usr/bin/docker network rm %s'; - public const GET_NETWORKS = '/usr/bin/docker network %s'; -} diff --git a/root/app/www/public/classes/Maintenance.php b/root/app/www/public/classes/Maintenance.php index a60d8b8..7dfb879 100644 --- a/root/app/www/public/classes/Maintenance.php +++ b/root/app/www/public/classes/Maintenance.php @@ -13,14 +13,7 @@ */ //-- BRING IN THE TRAITS -$traits = ABSOLUTE_PATH . 'classes/traits/Maintenance/'; -$traitsDir = opendir($traits); -while ($traitFile = readdir($traitsDir)) { - if (str_contains($traitFile, '.php')) { - require $traits . $traitFile; - } -} -closedir($traitsDir); +loadClassExtras('Maintenance'); class Maintenance { @@ -31,26 +24,21 @@ class Maintenance protected $maintenanceContainerName = 'dockwatch-maintenance'; protected $maintenancePort; protected $maintenanceIP; - protected $settingsFile; + protected $settingsTable; protected $hostContainer = []; protected $maintenanceContainer = []; protected $processList = []; public function __construct() { - global $docker, $settingsFile; + global $docker, $settingsTable; logger(MAINTENANCE_LOG, '$maintenance->__construct() ->'); - if (!$settingsFile) { - $settingsFile = getServerFile('settings'); - $settingsFile = $settingsFile['file']; - } - $this->docker = $docker; - $this->settingsFile = $settingsFile; - $this->maintenancePort = $settingsFile['global']['maintenancePort']; - $this->maintenanceIP = $settingsFile['global']['maintenanceIP']; + $this->settingsTable = $settingsTable; + $this->maintenancePort = $settingsTable['maintenancePort']; + $this->maintenanceIP = $settingsTable['maintenanceIP']; $getExpandedProcessList = getExpandedProcessList(true, true, true, true); $this->processList = is_array($getExpandedProcessList['processList']) ? $getExpandedProcessList['processList'] : []; $imageMatch = str_replace(':main', '', APP_IMAGE); diff --git a/root/app/www/public/classes/Notifications.php b/root/app/www/public/classes/Notifications.php index d9d47b3..6570ba5 100644 --- a/root/app/www/public/classes/Notifications.php +++ b/root/app/www/public/classes/Notifications.php @@ -8,37 +8,28 @@ */ //-- BRING IN THE TRAITS -$traits = ABSOLUTE_PATH . 'classes/traits/Notifications/'; -$traitsDir = opendir($traits); -while ($traitFile = readdir($traitsDir)) { - if (str_contains($traitFile, '.php')) { - require $traits . $traitFile; - } -} -closedir($traitsDir); +loadClassExtras('Notifications'); class Notifications { use Notifiarr; + use NotificationTemplates; protected $platforms; - protected $platformSettings; protected $headers; protected $logpath; protected $serverName; + protected $database; + protected $settingsTable; + public function __construct() { - global $platforms, $settingsFile; - - if (!$settingsFile) { - $settingsFile = getServerFile('settings'); - $settingsFile = $settingsFile['file']; - } + global $platforms, $settingsTable, $database; - $this->platforms = $platforms; //-- includes/platforms.php - $this->platformSettings = $settingsFile['notifications']['platforms']; - $this->logpath = LOGS_PATH . 'notifications/'; - $this->serverName = $settingsFile['global']['serverName']; + $this->database = $database ?? new Database(); + $this->platforms = $platforms; //-- includes/platforms.php + $this->logpath = LOGS_PATH . 'notifications/'; + $this->serverName = $settingsTable['serverName']; } public function __toString() @@ -46,34 +37,89 @@ public function __toString() return 'Notifications initialized'; } - public function notify($platform, $payload) + public function notify($linkId, $trigger, $payload) { + $linkIds = []; + $notificationPlatformTable = $this->database->getNotificationPlatforms(); + $notificationTriggersTable = $this->database->getNotificationTriggers(); + $notificationLinkTable = $this->database->getNotificationLinks(); + $triggerFields = $this->getTemplate($trigger); + + //-- MAKE IT MATCH THE TEMPLATE + foreach ($payload as $payloadField => $payloadVal) { + if (!array_key_exists($payloadField, $triggerFields) || !$payloadVal) { + unset($payload[$payloadField]); + } + } + if ($this->serverName) { $payload['server']['name'] = $this->serverName; } - $platformData = $this->getNotificationPlatformFromId($platform); - $logfile = $this->logpath . $platformData['name'] . '.log'; + if ($linkId) { + foreach ($notificationLinkTable as $notificationLink) { + if ($notificationLink['id'] == $linkId) { + $linkIds[] = $notificationLink; + } + } - logger($logfile, 'notification request to ' . $platformData['name']); - logger($logfile, 'notification payload: ' . json_encode($payload)); + $notificationLink = $notificationLinkTable[$linkId]; + $notificationPlatform = $notificationPlatformTable[$notificationLink['platform_id']]; + } else { + foreach ($notificationTriggersTable as $notificationTrigger) { + if ($notificationTrigger['name'] == $trigger) { + foreach ($notificationLinkTable as $notificationLink) { + $triggers = makeArray(json_decode($notificationLink['trigger_ids'], true)); - /* - Everything should return an array with code => ..., error => ... (if no error, just code is fine) - */ - switch ($platform) { - case 1: //-- Notifiarr - return $this->notifiarr($logfile, $payload); + foreach ($triggers as $trigger) { + if ($trigger == $notificationTrigger['id']) { + $linkIds[] = $notificationLink; + } + } + } + break; + } + } + } + + foreach ($linkIds as $linkId) { + $platformId = $linkId['platform_id']; + $platformParameters = json_decode($linkId['platform_parameters'], true); + $platformName = ''; + + foreach ($notificationPlatformTable as $notificationPlatform) { + if ($notificationPlatform['id'] == $platformId) { + $platformName = $notificationPlatform['platform']; + break; + } + } + + $logfile = $this->logpath . $platformName . '.log'; + logger($logfile, 'notification request to ' . $platformName); + logger($logfile, 'notification payload: ' . json_encode($payload)); + + switch ($platformId) { + case NotificationPlatforms::NOTIFIARR: + return $this->notifiarr($logfile, $platformParameters['apikey'], $payload); + } } } - public function getPlatforms() + public function getNotificationPlatformNameFromId($id, $platforms) { - return $this->platforms; + foreach ($platforms as $platform) { + if ($id == $platform['id']) { + return $platform['platform']; + } + } } - public function getNotificationPlatformFromId($platform) + public function getNotificationTriggerNameFromId($id, $triggers) { - return $this->platforms[$platform]; + foreach ($triggers as $trigger) { + if ($id == $trigger['id']) { + return $trigger['label']; + } + } } } diff --git a/root/app/www/public/classes/interfaces/Docker.php b/root/app/www/public/classes/interfaces/Docker.php new file mode 100644 index 0000000..25a1c31 --- /dev/null +++ b/root/app/www/public/classes/interfaces/Docker.php @@ -0,0 +1,50 @@ +containerGroupLinksTable) { + return $this->containerGroupLinksTable; + } + + $q = "SELECT * + FROM " . CONTAINER_GROUPS_LINK_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $containerLinks[] = $row; + } + + $this->containerGroupLinksTable = $containerLinks; + return $containerLinks ?? []; + } + + public function getGroupLinkContainersFromGroupId($groupLinks, $containers, $groupId) + { + $groupContainers = []; + foreach ($groupLinks as $groupLink) { + if ($groupLink['group_id'] == $groupId) { + $groupContainers[] = $containers[$groupLink['container_id']]; + } + } + + return $groupContainers; + } + + public function addContainerGroupLink($groupId, $containerId) + { + $q = "INSERT INTO " . CONTAINER_GROUPS_LINK_TABLE . " + (`group_id`, `container_id`) + VALUES + ('" . intval($groupId) . "', '" . intval($containerId) . "')"; + $this->query($q); + + $this->containerGroupLinksTable = ''; + } + + public function removeContainerGroupLink($groupId, $containerId) + { + $q = "DELETE FROM " . CONTAINER_GROUPS_LINK_TABLE . " + WHERE group_id = '" . intval($groupId) . "' + AND container_id = '" . intval($containerId) . "'"; + $this->query($q); + + $this->containerGroupLinksTable = ''; + } + + public function deleteGroupLinks($groupId) + { + $q = "DELETE FROM " . CONTAINER_GROUPS_LINK_TABLE . " + WHERE group_id = " . $groupId; + $this->query($q); + + $this->containerGroupLinksTable = ''; + } +} diff --git a/root/app/www/public/classes/traits/Database/ContainerGroups.php b/root/app/www/public/classes/traits/Database/ContainerGroups.php new file mode 100644 index 0000000..f3f5104 --- /dev/null +++ b/root/app/www/public/classes/traits/Database/ContainerGroups.php @@ -0,0 +1,80 @@ +containerGroupsTable) { + return $this->containerGroupsTable; + } + + $q = "SELECT * + FROM " . CONTAINER_GROUPS_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $containerGroups[] = $row; + } + + $this->containerGroupsTable = $containerGroups; + return $containerGroups ?? []; + } + + public function getContainerGroupFromHash($hash, $groups) + { + if (!$groups) { + $groups = $this->getContainerGroups(); + } + + foreach ($groups as $group) { + if ($group['hash'] == $hash) { + return $group; + } + } + + return []; + } + + public function updateContainerGroup($groupId, $fields = []) + { + $updateList = []; + foreach ($fields as $field => $val) { + $updateList[] = $field . ' = "' . $val . '"'; + } + + $q = "UPDATE " . CONTAINER_GROUPS_TABLE . " + SET " . implode(', ', $updateList) . " + WHERE id = '" . $groupId . "'"; + $this->query($q); + + $this->containerGroupsTable = ''; + } + + public function addContainerGroup($groupName) + { + $q = "INSERT INTO " . CONTAINER_GROUPS_TABLE . " + (`hash`, `name`) + VALUES + ('" . md5($groupName) . "', '" . $this->prepare($groupName) . "')"; + $this->query($q); + + $this->containerGroupsTable = ''; + return $this->insertId(); + } + + public function deleteContainerGroup($groupId) + { + $q = "DELETE FROM " . CONTAINER_GROUPS_TABLE . " + WHERE id = " . $groupId; + $this->query($q); + + $this->containerGroupsTable = ''; + $this->deleteGroupLinks($groupId); + } +} diff --git a/root/app/www/public/classes/traits/Database/ContainerSettings.php b/root/app/www/public/classes/traits/Database/ContainerSettings.php new file mode 100644 index 0000000..ecd83a4 --- /dev/null +++ b/root/app/www/public/classes/traits/Database/ContainerSettings.php @@ -0,0 +1,85 @@ +containersTable) { + return $this->containersTable; + } + + $q = "SELECT * + FROM " . CONTAINER_SETTINGS_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $containers[$row['id']] = $row; + } + + $this->containersTable = $containers; + return $containers ?? []; + } + + public function getContainerFromHash($hash, $containers) + { + if (!$containers) { + $containers = $this->getContainers(); + } + + foreach ($containers as $container) { + if ($container['hash'] == $hash) { + return $container; + } + } + + return []; + } + + public function getContainer($hash) + { + $q = "SELECT * + FROM " . CONTAINER_SETTINGS_TABLE . " + WHERE hash = '" . $hash . "'"; + $r = $this->query($q); + + return $this->fetchAssoc($r); + } + + public function addContainer($fields = []) + { + $fieldList = $valList = []; + foreach ($fields as $field => $val) { + $fieldList[] = '`' . $field . '`'; + $valList[] = '"'. $val .'"'; + } + + $q = "INSERT INTO " . CONTAINER_SETTINGS_TABLE . " + (". implode(', ', $fieldList) .") + VALUES + (". implode(', ', $valList) .")"; + $this->query($q); + + $this->containersTable = ''; + } + + public function updateContainer($hash, $fields = []) + { + $updateList = []; + foreach ($fields as $field => $val) { + $updateList[] = $field . ' = "' . $val . '"'; + } + + $q = "UPDATE " . CONTAINER_SETTINGS_TABLE . " + SET " . implode(', ', $updateList) . " + WHERE hash = '" . $hash . "'"; + $this->query($q); + + $this->containersTable = ''; + } +} diff --git a/root/app/www/public/classes/traits/Database/NotificationLink.php b/root/app/www/public/classes/traits/Database/NotificationLink.php new file mode 100644 index 0000000..86b20aa --- /dev/null +++ b/root/app/www/public/classes/traits/Database/NotificationLink.php @@ -0,0 +1,81 @@ +notificationLinkTable) { + return $this->notificationLinkTable; + } + + $notificationLinkTable = []; + + $q = "SELECT * + FROM " . NOTIFICATION_LINK_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $notificationLinkTable[$row['id']] = $row; + } + + $this->notificationLinkTable = $notificationLinkTable; + return $notificationLinkTable; + } + + public function updateNotificationLink($linkId, $triggerIds, $platformParameters, $senderName) + { + $q = "UPDATE " . NOTIFICATION_LINK_TABLE . " + SET name = '" . $this->prepare($senderName) . "', platform_parameters = '" . json_encode($platformParameters) . "', trigger_ids = '" . json_encode($triggerIds) . "' + WHERE id = '" . intval($linkId) . "'"; + $this->query($q); + + $this->notificationLinkTable = ''; + return $this->getNotificationLinks(); + } + + public function addNotificationLink($platformId, $triggerIds, $platformParameters, $senderName) + { + $q = "INSERT INTO " . NOTIFICATION_LINK_TABLE . " + (`name`, `platform_id`, `platform_parameters`, `trigger_ids`) + VALUES + ('" . $this->prepare($senderName) . "', '" . intval($platformId) . "', '" . json_encode($platformParameters) . "', '" . json_encode($triggerIds) . "')"; + $this->query($q); + + $this->notificationLinkTable = ''; + return $this->getNotificationLinks(); + } + + function deleteNotificationLink($linkId) + { + $q = "DELETE FROM " . NOTIFICATION_LINK_TABLE . " + WHERE id = " . intval($linkId); + $this->query($q); + + $this->notificationLinkTable = ''; + return $this->getNotificationLinks(); + } + + public function getNotificationLinkPlatformFromName($name) + { + $notificationLinks = $this->getNotificationLinks(); + $notificationTrigger = $this->getNotificationTriggerFromName($name); + + foreach ($notificationLinks as $notificationLink) { + if ($notificationLink['name'] == $name) { + $triggers = makeArray(json_decode($notificationLink['trigger_ids'], true)); + + foreach ($triggers as $trigger) { + if ($trigger == $notificationTrigger['id']) { + return $notificationLink['platform_id']; + } + } + } + } + } +} diff --git a/root/app/www/public/classes/traits/Database/NotificationPlatform.php b/root/app/www/public/classes/traits/Database/NotificationPlatform.php new file mode 100644 index 0000000..853452e --- /dev/null +++ b/root/app/www/public/classes/traits/Database/NotificationPlatform.php @@ -0,0 +1,30 @@ +notificationPlatformTable) { + return $this->notificationPlatformTable; + } + + $notificationPlatformTable = []; + + $q = "SELECT * + FROM " . NOTIFICATION_PLATFORM_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $notificationPlatformTable[$row['id']] = $row; + } + + $this->notificationPlatformTable = $notificationPlatformTable; + return $notificationPlatformTable; + } +} diff --git a/root/app/www/public/classes/traits/Database/NotificationTrigger.php b/root/app/www/public/classes/traits/Database/NotificationTrigger.php new file mode 100644 index 0000000..9abe959 --- /dev/null +++ b/root/app/www/public/classes/traits/Database/NotificationTrigger.php @@ -0,0 +1,61 @@ +notificationTriggersTable) { + return $this->notificationTriggersTable; + } + + $notificationTriggersTable = []; + + $q = "SELECT * + FROM " . NOTIFICATION_TRIGGER_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $notificationTriggersTable[$row['id']] = $row; + } + + $this->notificationTriggersTable = $notificationTriggersTable; + return $notificationTriggersTable; + } + + public function getNotificationTriggerFromName($name) + { + $triggers = $this->getNotificationTriggers(); + + foreach ($triggers as $trigger) { + if (str_compare($name, $trigger['name'])) { + return $trigger; + } + } + + return []; + } + + public function isNotificationTriggerEnabled($name) + { + $notificationLinks = $this->getNotificationLinks(); + $notificationTrigger = $this->getNotificationTriggerFromName($name); + + foreach ($notificationLinks as $notificationLink) { + $triggers = makeArray(json_decode($notificationLink['trigger_ids'], true)); + + foreach ($triggers as $trigger) { + if ($trigger == $notificationTrigger['id']) { + return true; + } + } + } + + return false; + } +} diff --git a/root/app/www/public/classes/traits/Database/Servers.php b/root/app/www/public/classes/traits/Database/Servers.php new file mode 100644 index 0000000..4938c40 --- /dev/null +++ b/root/app/www/public/classes/traits/Database/Servers.php @@ -0,0 +1,60 @@ +serversTable; + } + + foreach ($serverList as $serverId => $serverSettings) { + switch (true) { + case $serverSettings['remove']: + $q = "DELETE FROM " . SERVERS_TABLE . " + WHERE id = " . $serverId; + break; + case !$serverId: + $q = "INSERT INTO " . SERVERS_TABLE . " + (`name`, `url`, `apikey`) + VALUES + ('" . $this->prepare($serverSettings['name']) . "', '" . $this->prepare($serverSettings['url']) . "', '" . $this->prepare($serverSettings['apikey']) . "')"; + break; + default: + $q = "UPDATE " . SERVERS_TABLE . " + SET name = '" . $this->prepare($serverSettings['name']) . "', url = '" . $this->prepare($serverSettings['url']) . "', apikey = '" . $this->prepare($serverSettings['apikey']) . "' + WHERE id = " . $serverId; + break; + } + $this->query($q); + } + + return $this->getServers(); + } + + public function getServers() + { + if (ACTIVE_SERVER_ID != APP_SERVER_ID) { + return apiRequest('getServersTable'); + } + + $serversTable = []; + + $q = "SELECT * + FROM " . SERVERS_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $serversTable[$row['id']] = $row; + } + + $this->serversTable = $serversTable; + return $serversTable; + } +} diff --git a/root/app/www/public/classes/traits/Database/Settings.php b/root/app/www/public/classes/traits/Database/Settings.php new file mode 100644 index 0000000..10d3204 --- /dev/null +++ b/root/app/www/public/classes/traits/Database/Settings.php @@ -0,0 +1,73 @@ +query($q); + $row = $this->fetchAssoc($r); + + return $row['value']; + } + + public function setSetting($field, $value) + { + $q = "UPDATE " . SETTINGS_TABLE . " + SET value = '" . $this->prepare($value) . "' + WHERE name = '" . $field . "'"; + $this->query($q); + + return $this->getSettings(); + } + + public function setSettings($newSettings = [], $currentSettings = []) + { + if (!$newSettings) { + return; + } + + if (!$currentSettings) { + $currentSettings = $this->getSettings(); + } + + foreach ($newSettings as $field => $value) { + if ($currentSettings[$field] != $value) { + $q = "UPDATE " . SETTINGS_TABLE . " + SET value = '" . $this->prepare($value) . "' + WHERE name = '" . $field . "'"; + $this->query($q); + } + } + + return $this->getSettings(); + } + + public function getSettings() + { + if (ACTIVE_SERVER_ID != APP_SERVER_ID) { + return apiRequest('getSettingsTable'); + } + + $settingsTable = []; + + $q = "SELECT * + FROM " . SETTINGS_TABLE ; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $settingsTable[$row['name']] = $row['value']; + } + + $this->settingsTable = $settingsTable; + return $settingsTable; + } +} diff --git a/root/app/www/public/classes/traits/Docker/Container.php b/root/app/www/public/classes/traits/Docker/Container.php index 5580e5a..dcdd43a 100644 --- a/root/app/www/public/classes/traits/Docker/Container.php +++ b/root/app/www/public/classes/traits/Docker/Container.php @@ -29,18 +29,17 @@ public function startContainer($container) public function stopContainer($container) { - //-- GET CURRENT SETTINGS FILE - $settingsFile = getServerFile('settings'); - $settingsFile = $settingsFile['file']; + $nameHash = md5($container); + $containersTable = $this->database->getContainers(); + $container = $this->database->getContainerFromHash($nameHash, $containersTable); - $nameHash = md5($container); - $delay = (intval($settingsFile['containers'][$nameHash]['shutdownDelaySeconds']) >= 5 ? ' -t ' . $settingsFile['containers'][$nameHash]['shutdownDelaySeconds'] : ' -t 120'); + $delay = (intval($container['shutdownDelaySeconds']) >= 5 ? ' -t ' . $container['shutdownDelaySeconds'] : ' -t 120'); - if ($settingsFile['containers'][$nameHash]['shutdownDelay']) { + if ($container['shutdownDelay']) { logger(SYSTEM_LOG, 'stopContainer() delaying stop command for container ' . $container . ' with ' . $delay); } - $cmd = sprintf(DockerSock::STOP_CONTAINER, $this->shell->prepare($container), ($settingsFile['containers'][$nameHash]['shutdownDelay'] ? $this->shell->prepare($delay) : '')); + $cmd = sprintf(DockerSock::STOP_CONTAINER, $this->shell->prepare($container), ($container['shutdownDelay'] ? $this->shell->prepare($delay) : '')); return $this->shell->exec($cmd . ' 2>&1'); } diff --git a/root/app/www/public/classes/traits/Notifications/Templates.php b/root/app/www/public/classes/traits/Notifications/Templates.php new file mode 100644 index 0000000..e888800 --- /dev/null +++ b/root/app/www/public/classes/traits/Notifications/Templates.php @@ -0,0 +1,64 @@ + '', + 'container' => '', + 'restarted' => '' + ]; + case 'prune': + return [ + 'event' => '', + 'network' => '', + 'volume' => '', + 'image' => '', + 'imageList' => '' + ]; + case 'added': + case 'removed': + case 'stateChange': + return [ + 'event' => '', + 'changes' => '', + 'added' => '', + 'removed' => '' + ]; + case 'test': + return [ + 'event' => '', + 'title' => '', + 'message' => '' + ]; + case 'updated': + case 'updates': + return [ + 'event' => '', + 'available' => '', + 'updated' => '' + ]; + case 'cpuHigh': + case 'memHigh': + return [ + 'event' => '', + 'cpu' => '', + 'cpuThreshold' => '', + 'mem' => '', + 'memThreshold' => '' + ]; + default: + return []; + } + } +} diff --git a/root/app/www/public/classes/traits/Notifications/notifiarr.php b/root/app/www/public/classes/traits/Notifications/notifiarr.php index a06dc15..ddcf7c1 100644 --- a/root/app/www/public/classes/traits/Notifications/notifiarr.php +++ b/root/app/www/public/classes/traits/Notifications/notifiarr.php @@ -9,9 +9,9 @@ trait Notifiarr { - public function notifiarr($logfile, $payload) + public function notifiarr($logfile, $apikey, $payload) { - $headers = ['x-api-key:' . $this->platformSettings[1]['apikey']]; + $headers = ['x-api-key:' . $apikey]; $url = 'https://notifiarr.com/api/v1/notification/dockwatch'; $curl = curl($url, $headers, 'POST', json_encode($payload)); diff --git a/root/app/www/public/crons/health.php b/root/app/www/public/crons/health.php index baa0b5a..174867d 100644 --- a/root/app/www/public/crons/health.php +++ b/root/app/www/public/crons/health.php @@ -14,7 +14,7 @@ logger(CRON_HEALTH_LOG, 'run ->'); echo date('c') . ' Cron: health ->' . "\n"; -if ($settingsFile['tasks']['health']['disabled']) { +if ($settingsTable['taskHealthDisabled']) { logger(CRON_HEALTH_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_HEALTH_LOG, 'run <-'); echo date('c') . ' Cron: health cancelled, disabled in tasks menu' . "\n"; @@ -22,7 +22,7 @@ exit(); } -if (!$settingsFile['global']['restartUnhealthy'] && !$settingsFile['notifications']['triggers']['health']['active']) { +if (!$settingsTable['restartUnhealthy'] && !$database->isNotificationTriggerEnabled('health')) { logger(CRON_HEALTH_LOG, 'Cron cancelled: restart and notify disabled'); logger(CRON_HEALTH_LOG, 'run <-'); echo date('c') . ' Cron health cancelled: restart unhealthy and notify disabled' . "\n"; @@ -69,21 +69,23 @@ logger(CRON_HEALTH_LOG, '$unhealthy=' . json_encode($unhealthy, JSON_UNESCAPED_SLASHES)); if ($unhealthy) { + $containersTable = $database->getContainers(); + foreach ($unhealthy as $nameHash => $container) { $notify = false; if ($container['restart'] || $container['notify']) { continue; } - - $skipActions = skipContainerActions($container['name'], $skipContainerActions); + $thisContainer = $database->getContainerFromHash($nameHash, $containersTable); + $skipActions = skipContainerActions($container['name'], $skipContainerActions); if ($skipActions) { logger(CRON_HEALTH_LOG, 'skipping: ' . $container['name'] . ', blacklisted (no state changes) container'); continue; } - if (!$settingsFile['containers'][$nameHash]['restartUnhealthy']) { + if (!$thisContainer['restartUnhealthy']) { logger(CRON_HEALTH_LOG, 'skipping: ' . $container['name'] . ', restart unhealthy option not enabled'); continue; } @@ -91,9 +93,11 @@ $unhealthy[$nameHash]['notify'] = 0; $unhealthy[$nameHash]['restart'] = 0; - if ($settingsFile['notifications']['triggers']['health']['active'] && $settingsFile['notifications']['triggers']['health']['platform']) { + if ($database->isNotificationTriggerEnabled('health')) { $unhealthy[$nameHash]['notify'] = time(); $notify = true; + } else { + logger(CRON_HEALTH_LOG, 'skipping notification for \'' . $container['name'] . '\', no notification senders with the health event enabled'); } $dependencies = $dependencyFile[$container['name']]['containers']; @@ -119,16 +123,18 @@ } } - if ($settingsFile['containers'][$nameHash]['disableNotifications']) { + if ($notify && $thisContainer['disableNotifications']) { logger(CRON_HEALTH_LOG, 'skipping notification for \'' . $container['name'] . '\', container set to not notify'); $notify = false; } if ($notify) { logger(CRON_HEALTH_LOG, 'sending notification for \'' . $container['name'] . '\''); + $payload = ['event' => 'health', 'container' => $container['name'], 'restarted' => $unhealthy[$nameHash]['restart']]; + $notifications->notify(0, 'health', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['health']['platform'], $payload); } } @@ -140,4 +146,4 @@ } echo date('c') . ' Cron: health <-' . "\n"; -logger(CRON_HEALTH_LOG, 'run <-'); \ No newline at end of file +logger(CRON_HEALTH_LOG, 'run <-'); diff --git a/root/app/www/public/crons/housekeeper.php b/root/app/www/public/crons/housekeeper.php index 2720c56..058372d 100644 --- a/root/app/www/public/crons/housekeeper.php +++ b/root/app/www/public/crons/housekeeper.php @@ -14,7 +14,7 @@ logger(CRON_HOUSEKEEPER_LOG, 'run ->'); echo date('c') . ' Cron: housekeeper ->' . "\n"; -if ($settingsFile['tasks']['housekeeping']['disabled']) { +if ($settingsTable['tasksHousekeepingDisabled']) { logger(CRON_HOUSEKEEPER_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_HOUSEKEEPER_LOG, 'run <-'); echo date('c') . ' Cron: housekeeper cancelled, disabled in tasks menu' . "\n"; @@ -44,12 +44,12 @@ [ 'crons' => [[ 'message' => 'Cron log file cleanup (daily @ midnight)', - 'length' => ($settingsFile['global']['cronLogLength'] <= 1 ? 1 : $settingsFile['global']['cronLogLength']) + 'length' => ($settingsTable['cronLogLength'] <= 1 ? 1 : $settingsTable['cronLogLength']) ]] ],[ 'notifications' => [[ 'message' => 'Notification log file cleanup (daily @ midnight)', - 'length' => ($settingsFile['global']['notificationLogLength'] <= 1 ? 1 : $settingsFile['global']['notificationLogLength']) + 'length' => ($settingsTable['notificationLogLength'] <= 1 ? 1 : $settingsTable['notificationLogLength']) ]] ],[ 'system' => [[ @@ -59,11 +59,11 @@ ],[ 'type' => 'ui', 'message' => 'UI log file cleanup (daily @ midnight)', - 'length' => ($settingsFile['global']['uiLogLength'] <= 1 ? 1 : $settingsFile['global']['uiLogLength']) + 'length' => ($settingsTable['uiLogLength'] <= 1 ? 1 : $settingsTable['uiLogLength']) ],[ 'type' => 'api', 'message' => 'API log file cleanup (daily @ midnight)', - 'length' => ($settingsFile['global']['apiLogLength'] <= 1 ? 1 : $settingsFile['global']['apiLogLength']) + 'length' => ($settingsTable['apiLogLength'] <= 1 ? 1 : $settingsTable['apiLogLength']) ]] ] ]; diff --git a/root/app/www/public/crons/prune.php b/root/app/www/public/crons/prune.php index 227e4f3..1a3e6b4 100644 --- a/root/app/www/public/crons/prune.php +++ b/root/app/www/public/crons/prune.php @@ -16,7 +16,7 @@ logger(CRON_PRUNE_LOG, 'run ->'); echo date('c') . ' Cron: prune ->' . "\n"; -if ($settingsFile['tasks']['prune']['disabled']) { +if ($settingsTable['tasksPruneDisabled']) { logger(CRON_PRUNE_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_PRUNE_LOG, 'run <-'); echo date('c') . ' Cron: prune cancelled, disabled in tasks menu' . "\n"; @@ -24,7 +24,7 @@ exit(); } -$frequencyHour = $settingsFile['global']['autoPruneHour'] ? $settingsFile['global']['autoPruneHour'] : '12'; +$frequencyHour = $settingsTable['autoPruneHour'] ? $settingsTable['autoPruneHour'] : '12'; if ($frequencyHour !== date('G')) { logger(CRON_PRUNE_LOG, 'Cron: skipped, frequency setting will run at hour ' . $frequencyHour); logger(CRON_PRUNE_LOG, 'run <-'); @@ -43,7 +43,7 @@ logger(CRON_PRUNE_LOG, 'volumes=' . json_encode($volumes)); logger(CRON_PRUNE_LOG, 'networks=' . json_encode($networks)); -if ($settingsFile['global']['autoPruneImages']) { +if ($settingsTable['autoPruneImages']) { if ($images) { foreach ($images as $image) { $imagePrune[] = $image['ID']; @@ -54,7 +54,7 @@ logger(CRON_PRUNE_LOG, 'Auto prune images disabled'); } -if ($settingsFile['global']['autoPruneVolumes']) { +if ($settingsTable['autoPruneVolumes']) { if ($volumes) { foreach ($volumes as $volume) { $volumePrune[] = $volume['Name']; @@ -64,7 +64,7 @@ logger(CRON_PRUNE_LOG, 'Auto prune volumes disabled'); } -if ($settingsFile['global']['autoPruneNetworks']) { +if ($settingsTable['autoPruneNetworks']) { if ($networks) { foreach ($networks as $network) { $networkPrune[] = $network['ID']; @@ -98,11 +98,12 @@ logger(CRON_PRUNE_LOG, 'result: ' . $prune); } -if ($settingsFile['notifications']['triggers']['prune']['active'] && (count($volumePrune) > 0 || count($imagePrune) > 0 || count($networkPrune) > 0)) { +if ($database->isNotificationTriggerEnabled('prune') && (count($volumePrune) > 0 || count($imagePrune) > 0 || count($networkPrune) > 0)) { $payload = ['event' => 'prune', 'network' => count($networkPrune), 'volume' => count($volumePrune), 'image' => count($imagePrune), 'imageList' => $imageList]; + $notifications->notify(0, 'prune', $payload); + logger(CRON_PRUNE_LOG, 'Notification payload: ' . json_encode($payload)); - $notifications->notify($settingsFile['notifications']['triggers']['prune']['platform'], $payload); } echo date('c') . ' Cron: prune <-' . "\n"; -logger(CRON_PRUNE_LOG, 'run <-'); \ No newline at end of file +logger(CRON_PRUNE_LOG, 'run <-'); diff --git a/root/app/www/public/crons/pulls.php b/root/app/www/public/crons/pulls.php index 4cbd50d..0445c23 100644 --- a/root/app/www/public/crons/pulls.php +++ b/root/app/www/public/crons/pulls.php @@ -16,7 +16,7 @@ logger(CRON_PULLS_LOG, 'run ->'); echo date('c') . ' Cron: pulls' . "\n"; -if ($settingsFile['tasks']['pulls']['disabled']) { +if ($settingsTable['tasksPullsDisabled']) { logger(CRON_PULLS_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_PULLS_LOG, 'run <-'); echo date('c') . ' Cron: pulls cancelled, disabled in tasks menu' . "\n"; @@ -296,7 +296,7 @@ } } - if ($settingsFile['notifications']['triggers']['updated']['active'] && !$settingsFile['containers'][$containerHash]['disableNotifications']) { + if ($database->isNotificationTriggerEnabled('updated') && !$settingsFile['containers'][$containerHash]['disableNotifications']) { $notify['updated'][] = [ 'container' => $containerState['Names'], 'image' => $image, @@ -309,14 +309,14 @@ logger(CRON_PULLS_LOG, $msg); echo date('c') . ' ' . $msg . "\n"; - if ($settingsFile['notifications']['triggers']['updated']['active'] && !$settingsFile['containers'][$containerHash]['disableNotifications']) { + if ($database->isNotificationTriggerEnabled('updated') && !$settingsFile['containers'][$containerHash]['disableNotifications']) { $notify['failed'][] = ['container' => $containerState['Names']]; } } } break; case 2: //-- Check for updates - if ($settingsFile['notifications']['triggers']['updates']['active'] && $inspectImage[0]['Id'] != $inspectContainer[0]['Image'] && !$settingsFile['containers'][$containerHash]['disableNotifications']) { + if ($database->isNotificationTriggerEnabled('updates') && !$settingsFile['containers'][$containerHash]['disableNotifications'] && $inspectImage[0]['Id'] != $inspectContainer[0]['Image']) { $notify['available'][] = ['container' => $containerState['Names']]; } break; @@ -334,26 +334,28 @@ if ($notify) { //-- IF THEY USE THE SAME PLATFORM, COMBINE THEM - if ($settingsFile['notifications']['triggers']['updated']['platform'] == $settingsFile['notifications']['triggers']['updates']['platform']) { + if ($database->getNotificationLinkPlatformFromName('updated') == $database->getNotificationLinkPlatformFromName('updates')) { $payload = ['event' => 'updates', 'available' => $notify['available'], 'updated' => $notify['updated']]; + $notifications->notify(0, 'updates', $payload); + logger(CRON_PULLS_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['updated']['platform'], $payload); } else { if ($notify['available']) { $payload = ['event' => 'updates', 'available' => $notify['available']]; + $notifications->notify(0, 'updates', $payload); + logger(CRON_PULLS_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['updated']['platform'], $payload); } - if ($notify['usage']['mem']) { + if ($notify['updated']) { $payload = ['event' => 'updates', 'updated' => $notify['updated']]; + $notifications->notify(0, 'updated', $payload); + logger(CRON_PULLS_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['updates']['platform'], $payload); } } } } - echo date('c') . ' Cron: pulls <-' . "\n"; -logger(CRON_PULLS_LOG, 'run <-'); \ No newline at end of file +logger(CRON_PULLS_LOG, 'run <-'); diff --git a/root/app/www/public/crons/sse.php b/root/app/www/public/crons/sse.php index f4423e8..6157a22 100644 --- a/root/app/www/public/crons/sse.php +++ b/root/app/www/public/crons/sse.php @@ -14,7 +14,7 @@ logger(CRON_SSE_LOG, 'run ->'); echo date('c') . ' Cron: sse' . "\n"; -if (!$settingsFile['global']['sseEnabled']) { +if (!$settingsTable['sseEnabled']) { logger(CRON_SSE_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_SSE_LOG, 'run <-'); echo date('c') . ' Cron: sse cancelled, disabled in tasks menu' . "\n"; diff --git a/root/app/www/public/crons/state.php b/root/app/www/public/crons/state.php index b72a4b3..d0563c0 100644 --- a/root/app/www/public/crons/state.php +++ b/root/app/www/public/crons/state.php @@ -14,7 +14,7 @@ logger(CRON_STATE_LOG, 'run ->'); echo date('c') . ' Cron: state' . "\n"; -if ($settingsFile['tasks']['state']['disabled']) { +if ($settingsTable['taskStateDisabled']) { logger(CRON_STATE_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_STATE_LOG, 'run <-'); echo date('c') . ' Cron: state cancelled, disabled in tasks menu' . "\n"; @@ -59,8 +59,8 @@ if (!in_array($currentContainer, $previousContainers)) { $containerHash = md5($currentContainer); - $updates = is_array($settingsFile['global']) && array_key_exists('updates', $settingsFile['global']) ? $settingsFile['global']['updates'] : 3; //-- CHECK ONLY FALLBACK - $frequency = is_array($settingsFile['global']) && array_key_exists('updatesFrequency', $settingsFile['global']) ? $settingsFile['global']['updatesFrequency'] : DEFAULT_CRON; //-- DAILY FALLBACK + $updates = is_array($settingsTable) && array_key_exists('updates', $settingsTable) ? $settingsTable['updates'] : 3; //-- CHECK ONLY FALLBACK + $frequency = is_array($settingsTable) && array_key_exists('updatesFrequency', $settingsTable) ? $settingsTable['updatesFrequency'] : DEFAULT_CRON; //-- DAILY FALLBACK $settingsFile['containers'][$containerHash] = ['updates' => $updates, 'frequency' => $frequency]; if (!$settingsFile['containers'][$containerHash]['disableNotifications']) { @@ -113,27 +113,27 @@ $containerHash = md5($currentState['Names']); //-- CHECK FOR HIGH CPU USAGE CONTAINERS - if ($settingsFile['notifications']['triggers']['cpuHigh']['active'] && floatval($settingsFile['global']['cpuThreshold']) > 0 && !$settingsFile['containers'][$containerHash]['disableNotifications']) { + if ($settingsFile['notifications']['triggers']['cpuHigh']['active'] && floatval($settingsTable['cpuThreshold']) > 0 && !$settingsFile['containers'][$containerHash]['disableNotifications']) { if ($currentState['stats']['CPUPerc']) { $cpu = floatval(str_replace('%', '', $currentState['stats']['CPUPerc'])); - $cpuAmount = intval($settingsFile['global']['cpuAmount']); + $cpuAmount = intval($settingsTable['cpuAmount']); if ($cpuAmount > 0) { $cpu = number_format(($cpu / $cpuAmount), 2); } - if ($cpu > floatval($settingsFile['global']['cpuThreshold'])) { + if ($cpu > floatval($settingsTable['cpuThreshold'])) { $notify['usage']['cpu'][] = ['container' => $currentState['Names'], 'usage' => $cpu]; } } } //-- CHECK FOR HIGH MEMORY USAGE CONTAINERS - if ($settingsFile['notifications']['triggers']['memHigh']['active'] && floatval($settingsFile['global']['memThreshold']) > 0 && !$settingsFile['containers'][$containerHash]['disableNotifications']) { + if ($settingsFile['notifications']['triggers']['memHigh']['active'] && floatval($settingsTable['memThreshold']) > 0 && !$settingsFile['containers'][$containerHash]['disableNotifications']) { if ($currentState['stats']['MemPerc']) { $mem = floatval(str_replace('%', '', $currentState['stats']['MemPerc'])); - if ($mem > floatval($settingsFile['global']['memThreshold'])) { + if ($mem > floatval($settingsTable['memThreshold'])) { $notify['usage']['mem'][] = ['container' => $currentState['Names'], 'usage' => $mem]; } } @@ -154,51 +154,53 @@ if ($notify['state']) { //-- IF THEY USE THE SAME PLATFORM, COMBINE THEM - if ($settingsFile['notifications']['triggers']['stateChange']['platform'] == $settingsFile['notifications']['triggers']['added']['platform'] && $settingsFile['notifications']['triggers']['stateChange']['platform'] == $settingsFile['notifications']['triggers']['removed']['platform']) { + if ( + $database->getNotificationLinkPlatformFromName('stateChange') == $database->getNotificationLinkPlatformFromName('added') && + $database->getNotificationLinkPlatformFromName('stateChange') == $database->getNotificationLinkPlatformFromName('removed') + ) { $payload = ['event' => 'state', 'changes' => $notify['state']['changed'], 'added' => $notify['state']['added'], 'removed' => $notify['state']['removed']]; + $notifications->notify(0, 'stateChange', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['stateChange']['platform'], $payload); } else { if ($notify['state']['changed']) { $payload = ['event' => 'state', 'changes' => $notify['state']['changed']]; + $notifications->notify(0, 'stateChange', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['stateChange']['platform'], $payload); } if ($notify['state']['added']) { $payload = ['event' => 'state', 'added' => $notify['state']['added']]; + $notifications->notify(0, 'added', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['added']['platform'], $payload); } if ($notify['state']['removed']) { $payload = ['event' => 'state', 'removed' => $notify['state']['removed']]; + $notifications->notify(0, 'removed', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['removed']['platform'], $payload); } } } if ($notify['usage']) { - //-- IF THEY USE THE SAME PLATFORM, COMBINE THEM - if ($settingsFile['notifications']['triggers']['cpuHigh']['platform'] == $settingsFile['notifications']['triggers']['memHigh']['platform']) { - $payload = ['event' => 'usage', 'cpu' => $notify['usage']['cpu'], 'cpuThreshold' => $settingsFile['global']['cpuThreshold'], 'mem' => $notify['usage']['mem'], 'memThreshold' => $settingsFile['global']['memThreshold']]; + if ($notify['usage']['cpu']) { + $payload = ['event' => 'usage', 'cpu' => $notify['usage']['cpu'], 'cpuThreshold' => $settingsTable['cpuThreshold']]; + $notifications->notify(0, 'cpuHigh', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['cpuHigh']['platform'], $payload); - } else { - if ($notify['usage']['cpu']) { - $payload = ['event' => 'usage', 'cpu' => $notify['usage']['cpu'], 'cpuThreshold' => $settingsFile['global']['cpuThreshold']]; - logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['cpuHigh']['platform'], $payload); - } + } - if ($notify['usage']['mem']) { - $payload = ['event' => 'usage', 'mem' => $notify['usage']['mem'], 'memThreshold' => $settingsFile['global']['memThreshold']]; - logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['memHigh']['platform'], $payload); - } + if ($notify['usage']['mem']) { + $payload = ['event' => 'usage', 'mem' => $notify['usage']['mem'], 'memThreshold' => $settingsTable['memThreshold']]; + $notifications->notify(0, 'memHigh', $payload); + + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); } } echo date('c') . ' Cron: state <-' . "\n"; -logger(CRON_STATE_LOG, 'run <-'); \ No newline at end of file +logger(CRON_STATE_LOG, 'run <-'); diff --git a/root/app/www/public/crons/stats.php b/root/app/www/public/crons/stats.php index 77e0efb..f48f248 100644 --- a/root/app/www/public/crons/stats.php +++ b/root/app/www/public/crons/stats.php @@ -14,7 +14,7 @@ logger(CRON_STATS_LOG, 'run ->'); echo date('c') . ' Cron: stats' . "\n"; -if ($settingsFile['tasks']['stats']['disabled']) { +if ($settingsTable['tasksStatsDisabled']) { logger(CRON_STATS_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_STATS_LOG, 'run <-'); echo date('c') . ' Cron: stats cancelled, disabled in tasks menu' . "\n"; @@ -26,4 +26,4 @@ setServerFile('stats', $dockerStats); echo date('c') . ' Cron: stats <-' . "\n"; -logger(CRON_STATS_LOG, 'run <-'); \ No newline at end of file +logger(CRON_STATS_LOG, 'run <-'); diff --git a/root/app/www/public/functions/api.php b/root/app/www/public/functions/api.php index 570fa7c..b8ed8eb 100644 --- a/root/app/www/public/functions/api.php +++ b/root/app/www/public/functions/api.php @@ -33,7 +33,11 @@ function apiResponse($code, $response) function apiRequest($request, $params = [], $payload = []) { - global $serverOverride; + global $serverOverride, $database; + + if (!$database) { + $database = new Database(); + } $serverUrl = is_array($serverOverride) && $serverOverride['url'] ? $serverOverride['url'] : ACTIVE_SERVER_URL; $serverName = is_array($serverOverride) && $serverOverride['name'] ? $serverOverride['name'] : ACTIVE_SERVER_NAME; @@ -57,4 +61,9 @@ function apiRequest($request, $params = [], $payload = []) } return $curl['response']; -} \ No newline at end of file +} + +function generateApikey($length = 32) +{ + return bin2hex(random_bytes($length)); +} diff --git a/root/app/www/public/functions/common.php b/root/app/www/public/functions/common.php index d897aca..40f042a 100644 --- a/root/app/www/public/functions/common.php +++ b/root/app/www/public/functions/common.php @@ -7,6 +7,29 @@ ---------------------------------- */ +function loadClassExtras($class) +{ + $extras = ['interfaces', 'traits']; + + foreach ($extras as $extraDir) { + if (file_exists(ABSOLUTE_PATH . 'classes/' . $extraDir . '/' . $class . '.php')) { + require ABSOLUTE_PATH . 'classes/' . $extraDir . '/' . $class . '.php'; + } else { + $extraFolder = ABSOLUTE_PATH . 'classes/' . $extraDir . '/' . $class . '/'; + + if (is_dir($extraFolder)) { + $openExtraDir = opendir($extraFolder); + while ($extraFile = readdir($openExtraDir)) { + if (str_contains($extraFile, '.php')) { + require $extraFolder . $extraFile; + } + } + closedir($openExtraDir); + } + } + } +} + function isDockwatchContainer($container) { $imageMatch = str_replace(':main', '', APP_IMAGE); @@ -19,11 +42,6 @@ function isDockwatchContainer($container) function automation() { - if (!file_exists(SERVERS_FILE)) { - $servers[] = ['name' => 'localhost', 'url' => 'http://localhost', 'apikey' => generateApikey()]; - setFile(SERVERS_FILE, $servers); - } - //-- CREATE DIRECTORIES createDirectoryTree(LOGS_PATH . 'crons'); createDirectoryTree(LOGS_PATH . 'notifications'); @@ -33,6 +51,8 @@ function automation() createDirectoryTree(BACKUP_PATH); createDirectoryTree(TMP_PATH); createDirectoryTree(COMPOSE_PATH); + createDirectoryTree(DATABASE_PATH); + createDirectoryTree(MIGRATIONS_PATH); } function createDirectoryTree($tree) @@ -82,11 +102,6 @@ function linkWebroot($location) } } -function generateApikey($length = 32) -{ - return bin2hex(random_bytes($length)); -} - function trackTime($label, $microtime = 0) { $backtrace = debug_backtrace(); diff --git a/root/app/www/public/functions/containers.php b/root/app/www/public/functions/containers.php index 4a8cd0c..77d386f 100644 --- a/root/app/www/public/functions/containers.php +++ b/root/app/www/public/functions/containers.php @@ -9,7 +9,7 @@ function renderContainerRow($nameHash, $return) { - global $docker, $pullsFile, $settingsFile, $processList, $skipContainerActions, $groupHash; + global $docker, $pullsFile, $database, $settingsTable, $containersTable, $containerGroupsTable, $processList, $skipContainerActions, $groupHash; if (!$pullsFile) { $pullsFile = getServerFile('pull'); @@ -19,14 +19,6 @@ function renderContainerRow($nameHash, $return) $pullsFile = $pullsFile['file']; } - if (!$settingsFile) { - $settingsFile = getServerFile('settings'); - if ($settingsFile['code'] != 200) { - $apiError = $settingsFile['file']; - } - $settingsFile = $settingsFile['file']; - } - foreach ($processList as $process) { if (md5($process['Names']) == $nameHash) { break; @@ -41,7 +33,7 @@ function renderContainerRow($nameHash, $return) } $skipActions = skipContainerActions($process['inspect'][0]['Config']['Image'], $skipContainerActions); - $containerSettings = $settingsFile['containers'][$nameHash]; + $containerSettings = $database->getContainerFromHash($nameHash, $containersTable); $logo = getIcon($process['inspect']); $notificationIcon = ' '; @@ -53,8 +45,8 @@ function renderContainerRow($nameHash, $return) } $cpuUsage = floatval(str_replace('%', '', $process['stats']['CPUPerc'])); - if (intval($settingsFile['global']['cpuAmount']) > 0) { - $cpuUsage = number_format(($cpuUsage / intval($settingsFile['global']['cpuAmount'])), 2) . '%'; + if (intval($settingsTable['cpuAmount']) > 0) { + $cpuUsage = number_format(($cpuUsage / intval($settingsTable['cpuAmount'])), 2) . '%'; } $pullData = $pullsFile[$nameHash]; @@ -63,7 +55,7 @@ function renderContainerRow($nameHash, $return) $updateStatus = ($pullData['regctlDigest'] == $pullData['imageDigest']) ? 'Up to date' : 'Outdated'; } - $restartUnhealthy = $settingsFile['containers'][$nameHash]['restartUnhealthy']; + $restartUnhealthy = $containerSettings['restartUnhealthy']; $healthyRestartClass = 'text-success'; $healthyRestartText = 'Auto restart when unhealthy'; @@ -280,12 +272,12 @@ function renderContainerRow($nameHash, $return) @@ -359,4 +351,4 @@ function skipContainerActions($container, $containers) } return SKIP_OFF; -} \ No newline at end of file +} diff --git a/root/app/www/public/functions/files.php b/root/app/www/public/functions/files.php index 2ffc9f4..41cc5b5 100644 --- a/root/app/www/public/functions/files.php +++ b/root/app/www/public/functions/files.php @@ -22,7 +22,7 @@ function loadJS() function getServerFile($file) { //-- NO NEED FOR AN API REQUEST LOCALLY - if (!$_SESSION['serverIndex']) { + if (!$_SESSION['serverId']) { $localFile = constant(strtoupper($file) . '_FILE'); return ['code' => 200, 'file' => getFile($localFile)]; } @@ -37,7 +37,7 @@ function getServerFile($file) function setServerFile($file, $contents) { //-- NO NEED FOR AN API REQUEST LOCALLY - if (!$_SESSION['serverIndex']) { + if (!$_SESSION['serverId']) { $localFile = constant(strtoupper($file) . '_FILE'); setFile($localFile, $contents); return ['code' => 200]; diff --git a/root/app/www/public/functions/helpers/array.php b/root/app/www/public/functions/helpers/array.php index 1d3f26a..4d084bb 100644 --- a/root/app/www/public/functions/helpers/array.php +++ b/root/app/www/public/functions/helpers/array.php @@ -7,6 +7,11 @@ ---------------------------------- */ +function makeArray($array) +{ + return is_array($array) ? $array : []; +} + function array_equals_any($haystack, $needles) { if (!is_array($haystack) || !is_array($needles)) { diff --git a/root/app/www/public/functions/notifications.php b/root/app/www/public/functions/notifications.php index d2dccbe..7e9b2ce 100644 --- a/root/app/www/public/functions/notifications.php +++ b/root/app/www/public/functions/notifications.php @@ -20,9 +20,9 @@ function notificationPlatformField($platform, $field) } } -function sendTestNotification($platform) +function sendTestNotification($linkId) { - global $notifications, $settingsFile; + global $notifications; //-- INITIALIZE THE NOTIFY CLASS if (!$notifications) { @@ -30,18 +30,13 @@ function sendTestNotification($platform) logger(SYSTEM_LOG, 'Init class: Notifications()'); } + $payload = ['event' => 'test', 'title' => APP_NAME . ' test', 'message' => 'This is a test message sent from ' . APP_NAME]; $return = ''; - $payload = [ - 'event' => 'test', - 'title' => 'DockWatch Test', - 'message' => 'This is a test message sent from DockWatch' - ]; - - $result = $notifications->notify($platform, $payload); + $result = $notifications->notify($linkId, 'test', $payload); if ($result['code'] != 200) { $return = 'Code ' . $result['code'] . ', ' . $result['error']; } return $return; -} \ No newline at end of file +} diff --git a/root/app/www/public/includes/constants.php b/root/app/www/public/includes/constants.php index 1c692e4..3ed0a41 100644 --- a/root/app/www/public/includes/constants.php +++ b/root/app/www/public/includes/constants.php @@ -7,13 +7,27 @@ ---------------------------------- */ -define('APP_NAME', 'DockWatch'); +define('APP_NAME', 'Dockwatch'); define('APP_IMAGE', 'ghcr.io/notifiarr/dockwatch:main'); +define('APP_PORT', 9999); +define('APP_SERVER_ID', 1); +define('APP_SERVER_URL', 'http://localhost'); define('APP_MAINTENANCE_IMAGE', 'ghcr.io/notifiarr/dockwatch:develop'); +define('APP_MAINTENANCE_PORT', 9998); define('ICON_REPO', 'Notifiarr/images'); define('ICON_URL', 'https://gh.notifiarr.com/images/icons/'); +//-- DATABASE +define('SETTINGS_TABLE', 'settings'); +define('SERVERS_TABLE', 'servers'); +define('CONTAINER_SETTINGS_TABLE', 'container_settings'); +define('CONTAINER_GROUPS_TABLE', 'container_groups'); +define('CONTAINER_GROUPS_LINK_TABLE', 'container_group_link'); +define('NOTIFICATION_PLATFORM_TABLE', 'notification_platform'); +define('NOTIFICATION_TRIGGER_TABLE', 'notification_trigger'); +define('NOTIFICATION_LINK_TABLE', 'notification_link'); + //-- CRON FREQUENCY define('DEFAULT_CRON', '0 0 * * *'); @@ -23,6 +37,8 @@ define('LOGS_PATH', APP_DATA_PATH . 'logs/'); define('TMP_PATH', APP_DATA_PATH . 'tmp/'); define('COMPOSE_PATH', APP_DATA_PATH . 'compose/'); +define('DATABASE_PATH', APP_DATA_PATH . 'database/'); +define('MIGRATIONS_PATH', ABSOLUTE_PATH . 'migrations/'); //-- DATA FILES define('SERVERS_FILE', APP_DATA_PATH . 'servers.json'); @@ -81,4 +97,4 @@ 'dockwatch', //-- IF THIS GOES DOWN, IT WILL STOP THE CONTAINER WHICH MEANS IT CAN NEVER FINISH 'cloudflared', //-- IF THIS GOES DOWN, IT WILL KILL THE NETWORK TRAFFIC TO DOCKWATCH 'swag' //-- IS THIS GOES DOWN, IT WILL KILL THE WEB SERVICE TO DOCKWATCH - ]; \ No newline at end of file + ]; diff --git a/root/app/www/public/includes/footer.php b/root/app/www/public/includes/footer.php index e1c7865..399af47 100644 --- a/root/app/www/public/includes/footer.php +++ b/root/app/www/public/includes/footer.php @@ -179,7 +179,7 @@
Consider not running the dockwatch update time at the same time as other containers. When dockwatch starts its update process, everything after it will be ignored since it is stopping its self!
- + Remote control of self restarts and updates is not supported. Wait 30-45 seconds after using these buttons and refresh the page so the process outlined above can complete. If you have notifications enabled you will see the maintenance container start and shortly after the dockwatch container start.

diff --git a/root/app/www/public/includes/header.php b/root/app/www/public/includes/header.php index ac51e46..5c2248c 100644 --- a/root/app/www/public/includes/header.php +++ b/root/app/www/public/includes/header.php @@ -9,38 +9,35 @@ $_SESSION['IN_DOCKWATCH'] = true; -$fetchServers = false; if (!$_SESSION['serverList'] || ($_SESSION['serverListUpdated'] + 300) < time()) { - $fetchServers = true; -} - -if ($fetchServers) { - $serverList = ''; + foreach ($serversTable as $instanceDetails) { $disabled = ''; - if ($ping['code'] != 200) { - $disabled = ' [HTTP: ' . $ping['code'] . ']'; + if ($instanceDetails['id'] != APP_SERVER_ID) { + $ping = curl($instanceDetails['url'] . '/api/?request=ping', ['x-api-key: ' . $instanceDetails['apikey']], 'GET', '', [], 5); + if ($ping['code'] != 200) { + $disabled = ' [HTTP: ' . $ping['code'] . ']'; + } } - $serverList .= ''; - $link = $_SESSION['serverIndex'] == $serverIndex ? $serverDetails['url'] : $link; + + $serverList .= ''; + $link = $_SESSION['serverId'] == $instanceDetails['id'] ? $instanceDetails['url'] : $link; } $serverList .= ''; - $serverList .= ' '; + $serverList .= ' '; $_SESSION['serverList'] = $serverList; $_SESSION['serverListUpdated'] = time(); } else { $serverList = $_SESSION['serverList']; } - ?> - Dockwatch<?= ($settingsFile['global']['serverName'] ? ' - ' . $settingsFile['global']['serverName'] : '') ?> + Dockwatch<?= $settingsTable['serverName'] ? ' - ' . $settingsTable['serverName'] : '' ?> @@ -67,8 +64,8 @@ @@ -86,20 +83,20 @@ diff --git a/root/app/www/public/index.php b/root/app/www/public/index.php index e4f8cd8..14be5ff 100644 --- a/root/app/www/public/index.php +++ b/root/app/www/public/index.php @@ -19,8 +19,8 @@ } $loadError = ''; -if (!$serversFile) { - $loadError = 'Servers file missing or corrupt'; +if (!$serversTable) { + $loadError = 'Instances table is empty'; } if (!file_exists(REGCTL_PATH . REGCTL_BINARY)) { @@ -52,7 +52,7 @@ -
+
If you are seeing this, it means the user:group running this container does not have permission to run docker commands. Please fix that, restart the container and try again.

An example for Ubuntu: @@ -77,4 +77,4 @@
apiIsAvailable(); //-- INITIALIZE THE SHELL CLASS + logger(SYSTEM_LOG, 'Init Shell()'); $shell = new Shell(); } -//-- GET THE SERVERS LIST -$serversFile = getFile(SERVERS_FILE); -$_SESSION['serverIndex'] = is_numeric($_SESSION['serverIndex']) ? $_SESSION['serverIndex'] : 0; +logger(SYSTEM_LOG, 'Init Database()'); +$database = new Database(); +$database->migrations(); +$settingsTable = $database->getSettings(); +$serversTable = $database->getServers(); -define('ACTIVE_SERVER_NAME', $serversFile[$_SESSION['serverIndex']]['name']); -define('ACTIVE_SERVER_URL', rtrim($serversFile[$_SESSION['serverIndex']]['url'], '/')); -define('ACTIVE_SERVER_APIKEY', $serversFile[$_SESSION['serverIndex']]['apikey']); +//-- SET ACTIVE INSTANCE DETAILS +define('ACTIVE_SERVER_NAME', $serversTable[$_SESSION['serverId']]['name']); +define('ACTIVE_SERVER_URL', rtrim($serversTable[$_SESSION['serverId']]['url'], '/')); +define('ACTIVE_SERVER_APIKEY', $serversTable[$_SESSION['serverId']]['apikey']); if (!str_contains_any($_SERVER['PHP_SELF'], ['/api/']) && !str_contains($_SERVER['PWD'], 'oneshot')) { if (!IS_SSE) { //-- CHECK IF SELECTED SERVER CAN BE TALKED TO - $ping = apiRequest('ping'); + $ping = $_SESSION['serverId'] == APP_SERVER_ID ? ['code' => 200] : apiRequest('ping'); + if (!is_array($ping) || $ping['code'] != 200) { - if ($_SESSION['serverIndex'] == 0) { - exit('The connection to this container in the servers file is broken'); + if ($_SESSION['serverId'] == APP_SERVER_ID) { + exit('The connection to this container in the servers file is broken (code:' . $ping['code'] . ')'); } else { - $_SESSION['serverIndex'] = 0; + $_SESSION['serverId'] = APP_SERVER_ID; header('Location: /'); exit(); } } - } - //-- SETTINGS - $settingsFile = getServerFile('settings'); - $apiError = $settingsFile['code'] != 200 ? $settingsFile['file'] : $apiError; - $settingsFile = $settingsFile['file']; - - //-- LOGIN, DEFINE AFTER LOADING SETTINGS - define('LOGIN_FAILURE_LIMIT', ($settingsFile['global']['loginFailures'] ? $settingsFile['global']['loginFailures']: 10)); - define('LOGIN_FAILURE_TIMEOUT', ($settingsFile['global']['loginFailures'] ? $settingsFile['global']['loginTimeout']: 10)); //-- MINUTES TO DISABLE LOGINS - - if (!IS_SSE) { //-- STATE - $stateFile = getServerFile('state'); - $apiError = $stateFile['code'] != 200 ? $stateFile['file'] : $apiError; - $stateFile = $stateFile['file']; + $stateFile = getServerFile('state'); + $apiError = $stateFile['code'] != 200 ? $stateFile['file'] : $apiError; + $stateFile = $stateFile['file']; //-- PULLS - $pullsFile = getServerFile('pull'); - $apiError = $pullsFile['code'] != 200 ? $pullsFile['file'] : $apiError; - $pullsFile = $pullsFile['file']; + $pullsFile = getServerFile('pull'); + $apiError = $pullsFile['code'] != 200 ? $pullsFile['file'] : $apiError; + $pullsFile = $pullsFile['file']; } + //-- LOGIN, DEFINE AFTER LOADING SETTINGS + define('LOGIN_FAILURE_LIMIT', ($settingsTable['loginFailures'] ? $settingsTable['loginFailures']: 10)); + define('LOGIN_FAILURE_TIMEOUT', ($settingsTable['loginFailures'] ? $settingsTable['loginTimeout']: 10)); //-- MINUTES TO DISABLE LOGINS + if (file_exists(LOGIN_FILE) && !str_contains($_SERVER['PHP_SELF'], '/crons/')) { define('USE_AUTH', true); $loginsFile = file(LOGIN_FILE); diff --git a/root/app/www/public/migrations/001_initial_setup.php b/root/app/www/public/migrations/001_initial_setup.php new file mode 100644 index 0000000..a11f762 --- /dev/null +++ b/root/app/www/public/migrations/001_initial_setup.php @@ -0,0 +1,274 @@ + down)', 'state'), + ('added', 'Added', 'Send a notification when a container is added', 'state'), + ('removed', 'Removed', 'Send a notification when a container is removed', 'state'), + ('prune', 'Prune', 'Send a notification when an image or volume is pruned', 'prune'), + ('cpuHigh', 'CPU usage', 'Send a notification when container CPU usage exceeds threshold (set in Settings)', 'usage'), + ('memHigh', 'Memory usage', 'Send a notification when container memory usage exceeds threshold (set in Settings)', 'usage'), + ('health', 'Health change', 'Send a notification when container becomes unhealthy', 'health')"; + +$q[] = "CREATE TABLE " . NOTIFICATION_LINK_TABLE . " ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + platform_id INTEGER NOT NULL, + platform_parameters TEXT NOT NULL, + trigger_ids TEXT NOT NULL + )"; + +$q[] = "CREATE TABLE " . SERVERS_TABLE . " ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + url TEXT NOT NULL, + apikey TEXT NOT NULL + )"; + +$globalSettings = [ + 'serverName' => '', + 'maintenanceIP' => '', + 'maintenancePort' => 9998, + 'loginFailures' => 6, + 'loginTimeout' => 60, + 'updates' => 2, + 'updatesFrequency' => '0 2 * * *', + 'autoPruneImages' => false, + 'autoPruneVolumes' => false, + 'autoPruneNetworks' => false, + 'autoPruneHour' => 12, + 'cpuThreshold' => '', + 'cpuAmount' => '', + 'memThreshold' => '', + 'sseEnabled' => false, + 'cronLogLength' => 1, + 'notificationLogLength' => 1, + 'uiLogLength' => 1, + 'apiLogLength' => 1, + 'environment' => 0, + 'overrideBlacklist' => false, + 'externalLoading' => 0, + 'taskStatsDisabled' => 0, + 'taskStateDisabled' => 0, + 'taskPullsDisabled' => 0, + 'taskHousekeepingDisabled' => 0, + 'taskHealthDisabled' => 0, + 'taskPruneDisabled' => 0 + ]; + +foreach ($globalSettings as $key => $val) { + $settingRows[] = "('" . $key . "', '" . $val . "')"; +} + +$q[] = "INSERT INTO " . SETTINGS_TABLE . " + (`name`, `value`) + VALUES " . implode(', ', $settingRows); + +//-- PRE-DB SUPPORT, POPULATE THE NEW TABLES WITH EXISTING DATA +if (file_exists(APP_DATA_PATH . 'servers.json')) { + $serversFile = getFile(SERVERS_FILE); + $serverRows = []; + + foreach ($serversFile as $server) { + $serverRows[] = "('" . $server['name'] . "', '" . $server['url'] . "', '" . $server['apikey'] . "')"; + } + + if ($serverRows) { + $q[] = "INSERT INTO " . SERVERS_TABLE . " + (`name`, `url`, `apikey`) + VALUES " . implode(', ', $serverRows); + } +} else { + $q[] = "INSERT INTO " . SERVERS_TABLE . " + (`name`, `url`, `apikey`) + VALUES + ('" . APP_NAME . "', '" . APP_SERVER_URL . "', '" . generateApikey() . "')"; +} + +if (file_exists(APP_DATA_PATH . 'settings.json')) { + $settingsFile = getFile(SETTINGS_FILE); + + if ($settingsFile['tasks']) { + foreach ($settingsFile['tasks'] as $task => $taskSettings) { + $q[] = "UPDATE " . SETTINGS_TABLE . " + SET value = '" . $taskSettings['disabled'] . "' + WHERE name = 'task" . ucfirst($task) . "Disabled'"; + } + } + + if ($settingsFile['global']) { + foreach ($settingsFile['global'] as $key => $val) { + $q[] = "UPDATE " . SETTINGS_TABLE . " + SET value = '" . $val . "' + WHERE name = '" . $key . "'"; + } + } + + if ($settingsFile['containers']) { + $containerSettingsRows = []; + + foreach ($settingsFile['containers'] as $hash => $settings) { + $containerSettingsRows[] = "('" . $hash . "', '" . intval($settings['updates']) . "', '" . $settings['frequency'] . "', '" . intval($settings['restartUnhealthy']) . "', '" . intval($settings['disableNotifications']) . "', '" . intval($settings['shutdownDelay']) . "', '" . intval($settings['shutdownDelaySeconds']) . "')"; + } + + if ($containerSettingsRows) { + $q[] = "INSERT INTO " . CONTAINER_SETTINGS_TABLE . " + (`hash`, `updates`, `frequency`, `restartUnhealthy`, `disableNotifications`, `shutdownDelay`, `shutdownDelaySeconds`) + VALUES " . implode(', ', $containerSettingsRows); + } + } + + if ($settingsFile['containerGroups']) { + $containerGroupRows = []; + + foreach ($settingsFile['containerGroups'] as $groupHash => $groupData) { + $containerGroupRows[] = "('" . $groupHash . "', '" . $groupData['name'] . "')"; + } + + if ($containerGroupRows) { + $q[] = "INSERT INTO " . CONTAINER_GROUPS_TABLE . " + (`hash`, `name`) + VALUES " . implode(', ', $containerGroupRows); + } + } + + if ($settingsFile['notifications']) { + if ($settingsFile['notifications']['platforms'] && $settingsFile['notifications']['triggers']) { + $triggerIds = []; + if ($settingsFile['notifications']['triggers']['updated']['active']) { + $triggerIds[] = 1; + } + if ($settingsFile['notifications']['triggers']['updates']['active']) { + $triggerIds[] = 2; + } + if ($settingsFile['notifications']['triggers']['stateChange']['active']) { + $triggerIds[] = 3; + } + if ($settingsFile['notifications']['triggers']['added']['active']) { + $triggerIds[] = 4; + } + if ($settingsFile['notifications']['triggers']['removed']['active']) { + $triggerIds[] = 5; + } + if ($settingsFile['notifications']['triggers']['prune']['active']) { + $triggerIds[] = 6; + } + if ($settingsFile['notifications']['triggers']['cpuHigh']['active']) { + $triggerIds[] = 7; + } + if ($settingsFile['notifications']['triggers']['memHigh']['active']) { + $triggerIds[] = 8; + } + if ($settingsFile['notifications']['triggers']['health']['active']) { + $triggerIds[] = 9; + } + + $q[] = "INSERT INTO " . NOTIFICATION_LINK_TABLE . " + (`name`, `platform_id`, `platform_parameters`, `trigger_ids`) + VALUES + ('Notifiarr', '" . NotificationPlatforms::NOTIFIARR . "', '{\"apikey\":\"" . $settingsFile['notifications']['platforms'][NotificationPlatforms::NOTIFIARR]['apikey'] . "\"}', '[" . implode(',', $triggerIds) . "]')"; + } + } +} + +//-- ALWAYS NEED TO BUMP THE MIGRATION ID +$q[] = "INSERT INTO " . SETTINGS_TABLE . " + (`name`, `value`) + VALUES + ('migration', '001')"; + +foreach ($q as $query) { + $db->query($query); +} + +//-- PRE-DB SUPPORT, POPULATE THE NEW TABLES WITH EXISTING DATA +if ($settingsFile) { + $q = $containerLinkRows = []; + $containers = $database->getContainers(); + $containerGroups = $database->getContainerGroups(); + + if ($settingsFile['containerGroups']) { + foreach ($settingsFile['containerGroups'] as $groupHash => $groupData) { + if ($groupData['containers']) { + foreach ($groupData['containers'] as $groupContainerHash) { + $container = $database->getContainerFromHash($groupContainerHash, $containers); + $group = $database->getContainerGroupFromHash($groupHash, $containerGroups); + + if ($group['id'] && $container['id']) { + $containerLinkRows[] = "('" . $group['id'] . "', '" . $container['id'] . "')"; + } + } + } + } + + if ($containerLinkRows) { + $q[] = "INSERT INTO " . CONTAINER_GROUPS_LINK_TABLE . " + (`group_id`, `container_id`) + VALUES " . implode(', ', $containerLinkRows); + } + } + + foreach ($q as $query) { + $db->query($query); + } +} diff --git a/root/app/www/public/sse.php b/root/app/www/public/sse.php index 4b414c6..4a81569 100644 --- a/root/app/www/public/sse.php +++ b/root/app/www/public/sse.php @@ -15,7 +15,7 @@ $sseFile = getServerFile('sse'); $sseFile = $sseFile['code'] != 200 ? [] : $sseFile['file']; -if ($settingsFile['global']['sseEnabled']) { +if ($settingsTable['sseEnabled']) { //-- DONT SEND ALL THE DATA EVERY TIME, WASTE if (!$sseFile['pushed']) { $sseFile['pushed'] = time(); diff --git a/root/app/www/public/startup.php b/root/app/www/public/startup.php index 4effedf..50a3837 100644 --- a/root/app/www/public/startup.php +++ b/root/app/www/public/startup.php @@ -14,11 +14,15 @@ echo 'require_once ' . ABSOLUTE_PATH . 'loader.php' . "\n"; require_once ABSOLUTE_PATH . 'loader.php'; -//-- SETTINGS -$settingsFile = getFile(SETTINGS_FILE); +//-- INITIALIZE THE NOTIFY CLASS +if (!$database) { + $database = new Database(); +} //-- INITIALIZE THE NOTIFY CLASS -$notifications = new Notifications(); +if (!$notifications) { + $notifications = new Notifications(); +} //-- INITIALIZE THE MAINTENANCE CLASS $maintenance = new Maintenance(); @@ -29,10 +33,14 @@ //-- STARTUP NOTIFICATION $notify['state']['changed'][] = ['container' => $name, 'previous' => '.....', 'current' => 'Started/Restarted']; -if ($settingsFile['notifications']['triggers']['stateChange']['platform']) { + +if ($database->isNotificationTriggerEnabled('stateChange')) { $payload = ['event' => 'state', 'changes' => $notify['state']['changed']]; - $notifications->notify($settingsFile['notifications']['triggers']['stateChange']['platform'], $payload); + $notifications->notify(0, 'stateChange', $payload); + logger(STARTUP_LOG, 'Sending ' . $name . ' started notification'); +} else { + logger(STARTUP_LOG, 'Skipping ' . $name . ' started notification, no senders found with stateChange enabled'); } //-- MAINTENANCE CHECK diff --git a/root/etc/php82/conf.d/dockwatch.ini b/root/etc/php82/conf.d/dockwatch.ini index 0299379..0024d7f 100644 --- a/root/etc/php82/conf.d/dockwatch.ini +++ b/root/etc/php82/conf.d/dockwatch.ini @@ -8,4 +8,4 @@ max_input_vars = 10000 error_log = '/config/log/php/errors.log' error_reporting = -1 log_errors = 1 -session.gc_maxlifetime = 86400 +session.gc_maxlifetime = 86400 \ No newline at end of file
Orphan images - > + > Automatically try to prune all orphan images daily
Orphan volumes - > + > Automatically try to prune all orphan volumes daily
Orphan networks - > + > Automatically try to prune all orphan networks daily
CPU1 - + If a container usage is above this number, send a notification (if notification is enabled)
CPUs - + Detected count:
Memory1 - + If a container usage is above this number, send a notification (if notification is enabled)
Enabled2,3 - > + > SSE will update the container list UI every minute with current status of Updates, State, Health, CPU and Memory
Crons - + How long to store cron run log files (min 1 day)
Notifications - + How long to store logs generated when notifications are sent (min 1 day)
UI - + How long to store logs generated from using the UI (min 1 day)
API - + How long to store logs generated when api requests are made (min 1 day)
Environment Location of webroot, requires a container restart after changing. Do not change this without working files externally!
Override BlacklistOverride blacklist - > + > Generally not recommended, it's at your own risk.
Page Loading3Page loading3 Internal: On a full page refresh you will go back to the overview. (state lost)
External: On a full page refresh you will stay on this current page. (state saved in URL)
>> Stats changes 1m
>> SSE 1m
>> State changes 5m
>> Pulls 5m
>> Housekeeping 10m
>> Health 15m
>> Prune 24h