diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php
index 90e7c7a93..3870d8bb2 100644
--- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php
+++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php
@@ -48,6 +48,7 @@ class ServerState
private $caseModel = '';
private $keyfileBase64UrlSafe = '';
private $updateOsResponse;
+ private $updateOsIgnoredReleases = [];
public $myServersFlashCfg = [];
public $myServersMemoryCfg = [];
@@ -168,6 +169,11 @@ class ServerState
* updateOsResponse is provided by the dynamix.plugin.manager/scripts/unraidcheck script saving to /tmp/unraidcheck/result.json
*/
$this->updateOsResponse = @json_decode(@file_get_contents('/tmp/unraidcheck/result.json'), true);
+
+ /**
+ * updateOsIgnoredReleases is set by the dynamix.plugin.manager/inclue/UnraidIgnore.php script saving to /tmp/unraidcheck/ignored.json
+ */
+ $this->updateOsIgnoredReleases = @json_decode(@file_get_contents('/tmp/unraidcheck/ignored.json'), true) ?? [];
}
/**
@@ -253,6 +259,10 @@ class ServerState
$serverState['updateOsResponse'] = $this->updateOsResponse;
}
+ if ($this->updateOsIgnoredReleases) {
+ $serverState['updateOsIgnoredReleases'] = $this->updateOsIgnoredReleases;
+ }
+
return $serverState;
}
diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/includes/UnraidCheck.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/includes/UnraidCheck.php
index a8897951b..24ad49ee7 100644
--- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/includes/UnraidCheck.php
+++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/includes/UnraidCheck.php
@@ -88,10 +88,23 @@ if (array_key_exists('json',$_GET) && $_GET['json']) {
exit(0);
}
+// before sending a notification, check to see if the user requested to ignore the version
+$ignoredReleasesFile = '/tmp/unraidcheck/ignored.json';
+$ignoredReleasesResult = [];
+if (file_exists($ignoredReleasesFile)) {
+ $ignoredData = json_decode(file_get_contents($ignoredReleasesFile), true);
+ if (is_array($ignoredData) && array_key_exists('updateOsIgnoredReleases', $ignoredData)) {
+ $ignoredReleasesResult = $ignoredData['updateOsIgnoredReleases'];
+ }
+}
+
// send notification if a newer version is available
-if ($json && array_key_exists('isNewer',$json) && $json['isNewer']) {
+$isNewerVersion = array_key_exists('isNewer',$json) ? $json['isNewer'] : false;
+$isReleaseIgnored = in_array($json['version'], $ignoredReleasesResult);
+
+if ($json && $isNewerVersion && !$isReleaseIgnored) {
$newver = (array_key_exists('version',$json) && $json['version']) ? $json['version'] : 'unknown';
exec("$script -e ".escapeshellarg("System - Unraid [$newver]")." -s ".escapeshellarg("Notice [$server] - Version update $newver")." -d ".escapeshellarg("A new version of Unraid is available")." -i ".escapeshellarg("normal $output")." -l '/Tools/Update' -x");
}
exit(0);
-?>
\ No newline at end of file
+?>
diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/includes/UnraidIgnore.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/includes/UnraidIgnore.php
new file mode 100644
index 000000000..055bd118d
--- /dev/null
+++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/includes/UnraidIgnore.php
@@ -0,0 +1,143 @@
+
+
+/**
+ * @param removeAll {boolean} - if true, will remove all versions from the JSON file
+ * @param removeVersion {string} - the version of the OS release we want to remove
+ * @param version {string} - the version of the OS release we want to ignore
+ */
+$json_file_key = 'updateOsIgnoredReleases';
+$json_file = '/tmp/unraidcheck/ignored.json';
+
+function isValidSemVerFormat($version) {
+ return preg_match('/^\d+\.\d+(\.\d+)?(-.+)?$/',$version);
+}
+
+// Ensure that the request is a GET request
+if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+
+ // Read the JSON data from the request body
+ // $json_data = file_get_contents('php://input');
+ $json_data = $_GET;
+
+ if (empty($json_data)) {
+ http_response_code(400); // Bad Request
+ echo "No JSON data found";
+ return;
+ }
+
+ $data = $json_data;
+
+ if ($data !== null) {
+ // Check if the "removeAll" key exists in the $data array
+ if (isset($data['removeAll']) && $data['removeAll'] === true) {
+
+ // Check if the JSON file exists
+ if (file_exists($json_file)) {
+ // Delete the JSON file
+ unlink($json_file);
+ // return empty array to represent no ignored versions
+ header('Content-Type: application/json');
+ echo json_encode([$json_file_key => []], JSON_PRETTY_PRINT);
+ } else {
+ http_response_code(400); // Bad Request
+ echo "No JSON file found";
+ }
+ }
+ // Check if the "removeVersion" key exists in the $data array
+ else if (isset($data['removeVersion'])) {
+ // Check if the "removeVersion" value is a valid PHP-standardized version number string
+ $remove_version = $data['removeVersion'];
+ if (isValidSemVerFormat($remove_version)) {
+ // Check if the JSON file exists
+ if (file_exists($json_file)) {
+ // If the file exists, read its content
+ $existing_data = json_decode(file_get_contents($json_file), true);
+
+ // Check if key exists
+ if (isset($existing_data[$json_file_key])) {
+ // Remove the specified version from the array
+ $existing_data[$json_file_key] = array_diff($existing_data[$json_file_key], [$remove_version]);
+
+ // Save the updated data to the JSON file
+ file_put_contents($json_file, json_encode($existing_data, JSON_PRETTY_PRINT));
+
+ http_response_code(200); // OK
+ header('Content-Type: application/json');
+ echo json_encode($existing_data, JSON_PRETTY_PRINT);
+ } else {
+ http_response_code(400); // Bad Request
+ echo "No versions to remove in the JSON file";
+ }
+ } else {
+ http_response_code(400); // Bad Request
+ echo "No JSON file found";
+ }
+ } else {
+ http_response_code(400); // Bad Request
+ echo "Invalid removeVersion format";
+ }
+ }
+ // Check if the "version" key exists in the $data array
+ else if (isset($data['version'])) {
+
+ // Check if the "version" value is a valid PHP-standardized version number string
+ $version = $data['version'];
+ if (isValidSemVerFormat($version)) {
+ // Prepare the new data structure
+ $new_data = [$json_file_key => [$version]];
+
+ // Check if the JSON file already exists
+ if (file_exists($json_file)) {
+ // If the file exists, read its content
+ $existing_data = json_decode(file_get_contents($json_file), true);
+
+ // Check if key already exists
+ if (isset($existing_data[$json_file_key])) {
+ // Append the new version to the existing array
+ $existing_data[$json_file_key][] = $version;
+ } else {
+ // If key doesn't exist, create it
+ $existing_data[$json_file_key] = [$version];
+ }
+
+ // Update the data to be saved
+ $new_data = $existing_data;
+ }
+
+ // Save the data to the JSON file
+ file_put_contents($json_file, json_encode($new_data, JSON_PRETTY_PRINT));
+
+ http_response_code(200); // OK
+ header('Content-Type: application/json');
+ echo json_encode($new_data, JSON_PRETTY_PRINT);
+ } else {
+ http_response_code(400); // Bad Request
+ echo "Invalid version format";
+ }
+
+ } else {
+ http_response_code(400); // Bad Request
+ echo "Invalid param data";
+ }
+
+ } else {
+ http_response_code(400); // Bad Request
+ echo "Error decoding JSON data";
+ }
+
+} else {
+ // Handle non-GET requests
+ http_response_code(405); // Method Not Allowed
+ echo "Only GET requests are allowed";
+}
diff --git a/web/components/Ui/Switch.vue b/web/components/Ui/Switch.vue
new file mode 100644
index 000000000..0c2137bfd
--- /dev/null
+++ b/web/components/Ui/Switch.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+ {{ label }}
+
+
+
diff --git a/web/components/UpdateOs/CheckUpdateResponseModal.vue b/web/components/UpdateOs/CheckUpdateResponseModal.vue
index 5dd6018da..9545ace86 100644
--- a/web/components/UpdateOs/CheckUpdateResponseModal.vue
+++ b/web/components/UpdateOs/CheckUpdateResponseModal.vue
@@ -7,7 +7,6 @@ import { useAccountStore } from '~/store/account';
import { usePurchaseStore } from '~/store/purchase';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
-// import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
import type { ButtonProps } from '~/types/ui/button';
@@ -24,10 +23,9 @@ const accountStore = useAccountStore();
const purchaseStore = usePurchaseStore();
const serverStore = useServerStore();
const updateOsStore = useUpdateOsStore();
-// const updateOsActionsStore = useUpdateOsActionsStore();
const updateOsChangelogStore = useUpdateOsChangelogStore();
-const { osVersionBranch, updateOsResponse } = storeToRefs(serverStore);
+const { osVersionBranch, updateOsResponse, updateOsIgnoredReleases } = storeToRefs(serverStore);
const { available, availableWithRenewal, checkForUpdatesLoading } = storeToRefs(updateOsStore);
interface ModalCopy {
@@ -109,13 +107,13 @@ const close = () => {
// then ignore the release if applicable
if (ignoreThisRelease.value && (availableWithRenewal.value || available.value)) {
setTimeout(() => {
- updateOsStore.ignoreRelease(availableWithRenewal.value ?? available.value ?? '');
+ serverStore.updateOsIgnoreRelease(availableWithRenewal.value ?? available.value ?? '');
}, 500);
}
};
const renderMainSlot = computed(() => {
- return checkForUpdatesLoading.value || available.value || availableWithRenewal.value;
+ return checkForUpdatesLoading.value || available.value || availableWithRenewal.value || updateOsIgnoredReleases.value.length > 0;
});
@@ -145,10 +143,21 @@ const renderMainSlot = computed(() => {
class="inline-block h-20px w-20px transform rounded-full bg-white transition"
/>
- Ignore this release
+ {{ t('Ignore this release') }}
+
+
+ {{ t('Ignored Releases') }}
+
+
+
diff --git a/web/components/UpdateOs/IgnoredRelease.vue b/web/components/UpdateOs/IgnoredRelease.vue
new file mode 100644
index 000000000..b3136c392
--- /dev/null
+++ b/web/components/UpdateOs/IgnoredRelease.vue
@@ -0,0 +1,38 @@
+
+
+
+
+ {{ label }}
+
+
+
diff --git a/web/components/UpdateOs/IgnoredReleases.vue b/web/components/UpdateOs/IgnoredReleases.vue
new file mode 100644
index 000000000..95d3a019a
--- /dev/null
+++ b/web/components/UpdateOs/IgnoredReleases.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/web/composables/services/webgui.ts b/web/composables/services/webgui.ts
index 518ec6907..fd7fb9b38 100644
--- a/web/composables/services/webgui.ts
+++ b/web/composables/services/webgui.ts
@@ -118,3 +118,30 @@ export const WebguiCheckForUpdate = async (): Promise => {
+ console.debug('[WebguiUpdateIgnore] payload', payload);
+ try {
+ const response = await request
+ .url('/plugins/dynamix.plugin.manager/include/UnraidIgnore.php')
+ .query(payload)
+ .get()
+ .json((json) => {
+ console.debug('[WebguiUpdateIgnore] response', json);
+ return json;
+ })
+ .catch((error) => {
+ console.error('[WebguiUpdateIgnore] catch failed to execute UpdateIgnore', error);
+ throw new Error('Error ignoring update');
+ });
+ return response;
+ } catch (error) {
+ console.error('[WebguiUpdateIgnore] catch failed to execute UpdateIgnore', error);
+ throw new Error('Error ignoring update');
+ }
+};
diff --git a/web/locales/en_US.json b/web/locales/en_US.json
index 819af7b73..e3fead9d1 100644
--- a/web/locales/en_US.json
+++ b/web/locales/en_US.json
@@ -344,5 +344,8 @@
"Extend Key to Update": "Extend Key to Update",
"Install Unraid OS {0}": "Install Unraid OS {0}",
"View Changelog to Start Update": "View Changelog to Start Update",
- "Unraid OS {0} Update Available": "Unraid OS {0} Update Available"
+ "Unraid OS {0} Update Available": "Unraid OS {0} Update Available",
+ "Remove": "Remove",
+ "Remove from ignore list": "Remove from ignore list",
+ "Ignored Releases": "Ignored Releases"
}
diff --git a/web/store/server.ts b/web/store/server.ts
index 901409847..29f7b5803 100644
--- a/web/store/server.ts
+++ b/web/store/server.ts
@@ -16,7 +16,7 @@ import { useQuery } from '@vue/apollo-composable';
import { SERVER_CLOUD_FRAGMENT, SERVER_STATE_QUERY } from './server.fragment';
import { useFragment } from '~/composables/gql/fragment-masking';
-import { WebguiState } from '~/composables/services/webgui';
+import { WebguiState, WebguiUpdateIgnore } from '~/composables/services/webgui';
import { WEBGUI_SETTINGS_MANAGMENT_ACCESS } from '~/helpers/urls';
import { useAccountStore } from '~/store/account';
import { useErrorsStore, type Error } from '~/store/errors';
@@ -117,6 +117,7 @@ export const useServerStore = defineStore('server', () => {
if (newVal) { themeStore.setTheme(newVal); }
});
const updateOsResponse = ref();
+ const updateOsIgnoredReleases = ref([]);
const uptime = ref(0);
const username = ref(''); // @todo potentially move to a user store
const wanFQDN = ref('');
@@ -822,6 +823,7 @@ export const useServerStore = defineStore('server', () => {
if (typeof data?.state !== 'undefined') { state.value = data.state; }
if (typeof data?.theme !== 'undefined') { theme.value = data.theme; }
if (typeof data?.updateOsResponse !== 'undefined') { updateOsResponse.value = data.updateOsResponse; }
+ if (typeof data?.updateOsIgnoredReleases !== 'undefined') { updateOsIgnoredReleases.value = data.updateOsIgnoredReleases; }
if (typeof data?.uptime !== 'undefined') { uptime.value = data.uptime; }
if (typeof data?.username !== 'undefined') { username.value = data.username; }
if (typeof data?.wanFQDN !== 'undefined') { wanFQDN.value = data.wanFQDN; }
@@ -965,6 +967,31 @@ export const useServerStore = defineStore('server', () => {
}
});
+ const updateOsIgnoreRelease = (release: string) => {
+ updateOsIgnoredReleases.value.push(release);
+ const response = WebguiUpdateIgnore({
+ version: release,
+ });
+ console.debug('[updateOsIgnoreRelease] response', response);
+ /** @todo when update check modal is displayed and there's no available updates, allow users to remove ignored releases from the list */
+ };
+
+ const updateOsRemoveIgnoredRelease = (release: string) => {
+ updateOsIgnoredReleases.value = updateOsIgnoredReleases.value.filter(r => r !== release);
+ const response = WebguiUpdateIgnore({
+ removeVersion: release,
+ });
+ console.debug('[updateOsRemoveIgnoredRelease] response', response);
+ };
+
+ const updateOsRemoveAllIgnoredReleases = () => {
+ updateOsIgnoredReleases.value = [];
+ const response = WebguiUpdateIgnore({
+ removeAll: true,
+ });
+ console.debug('[updateOsRemoveAllIgnoredReleases] response', response);
+ };
+
return {
// state
apiKey,
@@ -1003,6 +1030,7 @@ export const useServerStore = defineStore('server', () => {
site,
state,
theme,
+ updateOsIgnoredReleases,
updateOsResponse,
uptime,
username,
@@ -1030,5 +1058,8 @@ export const useServerStore = defineStore('server', () => {
refreshServerState,
filteredKeyActions,
setRebootVersion,
+ updateOsIgnoreRelease,
+ updateOsRemoveIgnoredRelease,
+ updateOsRemoveAllIgnoredReleases,
};
});
diff --git a/web/store/updateOs.ts b/web/store/updateOs.ts
index 096d3c7e1..a358514db 100644
--- a/web/store/updateOs.ts
+++ b/web/store/updateOs.ts
@@ -17,31 +17,24 @@ setActivePinia(createPinia());
extend(customParseFormat);
extend(relativeTime);
-const KEY_IGNORED_RELEASES = 'updateOsIgnoredReleases';
-
export const useUpdateOsStore = defineStore('updateOs', () => {
// state
const checkForUpdatesLoading = ref(false);
const modalOpen = ref(false);
- const ignoredReleases = ref(
- localStorage.getItem(KEY_IGNORED_RELEASES)
- ? (JSON.parse(localStorage.getItem(KEY_IGNORED_RELEASES) ?? '') ?? [])
- : []
- );
-
// getters from other stores
const serverStore = useServerStore();
const regExp = computed(() => serverStore.regExp);
const regUpdatesExpired = computed(() => serverStore.regUpdatesExpired);
const updateOsResponse = computed(() => serverStore.updateOsResponse);
+ const updateOsIgnoredReleases = computed(() => serverStore.updateOsIgnoredReleases);
// local getters
const available = computed(() => {
if (!updateOsResponse.value) {
return undefined;
}
- // ignore any releases that are in the ignoredReleases array
- if (ignoredReleases.value.includes(updateOsResponse.value.version)) {
+ // ignore any releases that are in the updateOsIgnoredReleases array
+ if (updateOsIgnoredReleases.value.includes(updateOsResponse.value.version)) {
return undefined;
}
return updateOsResponse.value.isNewer ? updateOsResponse.value.version : undefined;
@@ -83,25 +76,17 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
modalOpen.value = val;
};
- const ignoreRelease = (release: string) => {
- ignoredReleases.value.push(release);
- localStorage.setItem(KEY_IGNORED_RELEASES, JSON.stringify(ignoredReleases.value));
- /** @todo submit to an endpoint on the server to save to a file */
- /** @todo when update check modal is displayed and there's no available updates, allow users to remove ignored releases from the list */
- };
-
return {
// state
available,
availableWithRenewal,
checkForUpdatesLoading,
modalOpen,
- ignoredReleases,
+ updateOsIgnoredReleases,
// getters
availableReleaseDate,
// actions
localCheckForUpdate,
setModalOpen,
- ignoreRelease,
};
});
diff --git a/web/types/server.ts b/web/types/server.ts
index 4406e578e..c6a7e4160 100644
--- a/web/types/server.ts
+++ b/web/types/server.ts
@@ -101,6 +101,7 @@ export interface Server {
state?: ServerState;
theme?: Theme | undefined;
updateOsResponse?: ServerUpdateOsResponse;
+ updateOsIgnoredReleases?: string[];
uptime?: number;
username?: string;
wanFQDN?: string;