diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg
index cf3ce5fad..38c4869a9 100755
--- a/plugin/plugins/dynamix.unraid.net.plg
+++ b/plugin/plugins/dynamix.unraid.net.plg
@@ -134,8 +134,8 @@ done
# Handle the unraid-components directory
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
if [ -d "$DIR" ] && [ ! -d "$DIR-" ]; then
- cp -rp "$DIR" "$DIR-"
- echo "Backed up directory: $DIR"
+ mv "$DIR" "$DIR-"
+ echo "Moved directory: $DIR to $DIR-"
fi
echo "Backup complete."
@@ -204,12 +204,13 @@ exit 0
# Handle the unraid-components directory
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
- # certain instances where the directory is not present and others where it is, ensure we delete it before we restore it
+ # Remove the archive's contents before restoring
if [ -d "$DIR" ]; then
rm -rf "$DIR"
fi
if [ -d "$DIR-" ]; then
- mv -f "$DIR-" "$DIR"
+ mv "$DIR-" "$DIR"
+ echo "Restored directory: $DIR- to $DIR"
fi
]]>
diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php
index c771872eb..2e28d4a55 100644
--- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php
+++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php
@@ -402,6 +402,7 @@ class WebComponentTranslations
'Your license key is not eligible for Unraid OS {0}' => sprintf(_('Your license key is not eligible for Unraid OS %s'), '{0}'),
'Your Trial has expired' => _('Your Trial has expired'),
'Your Trial key has been extended!' => _('Your Trial key has been extended!'),
+ 'Unraid OS {0} Changelog' => sprintf(_('Unraid OS %s Changelog'), '{0}'),
];
}
diff --git a/web/__test__/store/updateOs.test.ts b/web/__test__/store/updateOs.test.ts
index 56ce2033c..c9c76e173 100644
--- a/web/__test__/store/updateOs.test.ts
+++ b/web/__test__/store/updateOs.test.ts
@@ -58,7 +58,7 @@ describe('UpdateOs Store', () => {
describe('State and Getters', () => {
it('should initialize with correct default values', () => {
expect(store.checkForUpdatesLoading).toBe(false);
- expect(store.modalOpen).toBe(false);
+ expect(store.updateOsModalVisible).toBe(false);
});
it('should have computed properties with the right types', () => {
@@ -86,15 +86,15 @@ describe('UpdateOs Store', () => {
await store.localCheckForUpdate();
expect(WebguiCheckForUpdate).toHaveBeenCalled();
- expect(store.modalOpen).toBe(true);
+ expect(store.updateOsModalVisible).toBe(true);
});
it('should set modal open state', () => {
store.setModalOpen(true);
- expect(store.modalOpen).toBe(true);
+ expect(store.updateOsModalVisible).toBe(true);
store.setModalOpen(false);
- expect(store.modalOpen).toBe(false);
+ expect(store.updateOsModalVisible).toBe(false);
});
it('should handle errors when checking for updates', async () => {
diff --git a/web/__test__/store/updateOsChangelog.test.ts b/web/__test__/store/updateOsChangelog.test.ts
deleted file mode 100644
index 8f3c8a37c..000000000
--- a/web/__test__/store/updateOsChangelog.test.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-/**
- * UpdateOsChangelog store test coverage
- */
-
-import { nextTick } from 'vue';
-import { createPinia, setActivePinia } from 'pinia';
-
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-
-import type { ServerUpdateOsResponse } from '~/types/server';
-
-import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
-
-vi.mock('~/helpers/markdown', () => ({
- Markdown: {
- create: () => ({
- setOptions: vi.fn(),
- parse: vi.fn().mockResolvedValue('
Test Title Test content
'),
- }),
- },
-}));
-
-vi.mock('~/helpers/urls', () => ({
- DOCS_RELEASE_NOTES: {
- toString: () => 'https://docs.unraid.net/unraid-os/release-notes/',
- },
-}));
-
-vi.mock('marked-base-url', () => ({
- baseUrl: vi.fn().mockReturnValue(vi.fn()),
-}));
-
-vi.mock('semver/functions/prerelease', () => ({
- default: vi.fn((version) => (version && version.includes('-') ? ['beta', '1'] : null)),
-}));
-
-const mockRequestText = vi.fn().mockResolvedValue('# Test Changelog\n\nTest content');
-vi.mock('~/composables/services/request', () => ({
- request: {
- url: () => ({
- get: () => ({
- text: mockRequestText,
- }),
- }),
- },
-}));
-
-const mockSend = vi.fn();
-vi.mock('~/store/callbackActions', () => ({
- useCallbackActionsStore: () => ({
- send: mockSend,
- }),
-}));
-
-const mockStableRelease: Partial = {
- version: '6.12.5',
- name: 'Unraid 6.12.5',
- date: '2023-10-15',
- isEligible: true,
- isNewer: true,
- changelog: 'https://example.com/changelog.md',
- changelogPretty: 'https://example.com/changelog',
- sha256: 'test-sha256',
-};
-
-const mockBetaRelease: Partial = {
- ...mockStableRelease,
- version: '6.12.5-beta1',
-};
-
-describe('UpdateOsChangelog Store', () => {
- let store: ReturnType;
-
- beforeEach(() => {
- setActivePinia(createPinia());
- store = useUpdateOsChangelogStore();
- vi.clearAllMocks();
-
- // Suppress console output
- vi.spyOn(console, 'debug').mockImplementation(() => {});
- vi.spyOn(console, 'error').mockImplementation(() => {});
- });
-
- describe('Store API', () => {
- it('should initialize with default values', () => {
- expect(store.releaseForUpdate).toBeNull();
- expect(store.parseChangelogFailed).toBe('');
- });
-
- it('should set and get releaseForUpdate', () => {
- store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
- expect(store.releaseForUpdate).toEqual(mockStableRelease);
-
- store.setReleaseForUpdate(null);
- expect(store.releaseForUpdate).toBeNull();
- });
-
- it('should determine if release is stable', () => {
- expect(store.isReleaseForUpdateStable).toBe(false);
-
- store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
-
- expect(store.isReleaseForUpdateStable).toBe(true);
-
- store.setReleaseForUpdate(mockBetaRelease as ServerUpdateOsResponse);
- expect(store.isReleaseForUpdateStable).toBe(false);
- });
-
- it('should have a method to fetch and confirm install', () => {
- store.fetchAndConfirmInstall('test-sha256');
-
- expect(mockSend).toHaveBeenCalledWith(
- expect.any(String),
- [
- {
- sha256: 'test-sha256',
- type: 'updateOs',
- },
- ],
- undefined,
- 'forUpc'
- );
- });
-
- it('should have computed properties for changelog display', async () => {
- store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
-
- expect(typeof store.mutatedParsedChangelog).toBe('string');
- expect(typeof store.parsedChangelogTitle).toBe('string');
- });
-
- it('should clear changelog data when release is set to null', () => {
- store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
-
- store.setReleaseForUpdate(null);
-
- expect(store.releaseForUpdate).toBeNull();
- expect(store.parseChangelogFailed).toBe('');
- });
-
- it('should handle state transitions when changing releases', () => {
- store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
-
- const differentRelease = {
- ...mockStableRelease,
- version: '6.12.6',
- };
- store.setReleaseForUpdate(differentRelease as ServerUpdateOsResponse);
-
- expect(store.releaseForUpdate).toEqual(differentRelease);
- });
-
- it('should have proper error handling for failed requests', async () => {
- mockRequestText.mockRejectedValueOnce(new Error('Network error'));
- store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
-
- await nextTick();
-
- expect(store.parseChangelogFailed).toBeTruthy();
- expect(store.parseChangelogFailed).toContain('error');
- });
-
- it('should fetch and parse changelog when releaseForUpdate changes', async () => {
- const internalStore = useUpdateOsChangelogStore();
-
- vi.clearAllMocks();
-
- internalStore.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
-
- await nextTick();
-
- expect(mockRequestText).toHaveBeenCalled();
-
- mockRequestText.mockClear();
-
- const differentRelease = {
- ...mockStableRelease,
- version: '6.12.6',
- changelog: 'https://example.com/different-changelog.md',
- };
-
- internalStore.setReleaseForUpdate(differentRelease as ServerUpdateOsResponse);
-
- await nextTick();
-
- expect(mockRequestText).toHaveBeenCalled();
-
- mockRequestText.mockClear();
-
- internalStore.setReleaseForUpdate(null);
-
- await nextTick();
-
- expect(mockRequestText).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/web/_data/serverState.ts b/web/_data/serverState.ts
index 17e7da77b..92f2b20ad 100644
--- a/web/_data/serverState.ts
+++ b/web/_data/serverState.ts
@@ -47,7 +47,7 @@ import type {
// EBLACKLISTED2
// ENOCONN
-const state: ServerState = 'ENOKEYFILE' as ServerState;
+const state: ServerState = 'BASIC' as ServerState;
const currentFlashGuid = '1111-1111-YIJD-ZACK1234TEST'; // this is the flash drive that's been booted from
const regGuid = '1111-1111-YIJD-ZACK1234TEST'; // this guid is registered in key server
const keyfileBase64 = '';
diff --git a/web/components/Modal.vue b/web/components/Modal.vue
index 55d2440a6..ffb545465 100644
--- a/web/components/Modal.vue
+++ b/web/components/Modal.vue
@@ -121,7 +121,7 @@ const computedVerticalCenter = computed(() => {
]"
class="text-16px text-foreground bg-background text-left relative z-10 mx-auto flex flex-col justify-around border-2 border-solid transform overflow-hidden rounded-lg transition-all"
>
-
+
@@ -20,7 +18,7 @@ const { releaseForUpdate: updateOsChangelogModalVisible } = storeToRefs(useUpdat
-
+
diff --git a/web/components/UpdateOs/ChangelogModal.vue b/web/components/UpdateOs/ChangelogModal.vue
index c122249d7..08cf08ba5 100644
--- a/web/components/UpdateOs/ChangelogModal.vue
+++ b/web/components/UpdateOs/ChangelogModal.vue
@@ -1,23 +1,22 @@
-
-
-
-
- {{ props.t(`Error Parsing Changelog • {0}`, [parseChangelogFailed]) }}
-
-
- {{ props.t(`It's highly recommended to review the changelog before continuing your update`) }}
-
-
-
- {{ props.t('View Changelog on Docs') }}
-
+
-
-
-
{{ props.t('Fetching & parsing changelog…') }}
+
+
+
+
+
+
+
{{ props.t('Loading changelog…') }}
+
+
- {{ props.t('Close') }}
-
+ :icon="ArrowLeftIcon"
+ aria-label="Back to Changelog"
+ @click="revertToInitialChangelog"
+ />
+
+
- {{ props.t('View on Docs') }}
-
+ :href="currentIframeUrl || releaseForUpdate?.changelogPretty"
+ :icon="ArrowTopRightOnSquareIcon"
+ aria-label="View on Docs"
+ />
+
+
{
{{ props.t('Continue') }}
diff --git a/web/components/UpdateOs/CheckUpdateResponseModal.vue b/web/components/UpdateOs/CheckUpdateResponseModal.vue
index 66dda4c4f..6c4ace9d0 100644
--- a/web/components/UpdateOs/CheckUpdateResponseModal.vue
+++ b/web/components/UpdateOs/CheckUpdateResponseModal.vue
@@ -20,7 +20,6 @@ import { useAccountStore } from '~/store/account';
import { usePurchaseStore } from '~/store/purchase';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
-import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
export interface Props {
open?: boolean;
@@ -35,7 +34,6 @@ const accountStore = useAccountStore();
const purchaseStore = usePurchaseStore();
const serverStore = useServerStore();
const updateOsStore = useUpdateOsStore();
-const updateOsChangelogStore = useUpdateOsChangelogStore();
const {
regExp,
@@ -181,7 +179,7 @@ const actionButtons = computed((): BrandButtonProps[] | null => {
buttons.push({
variant: availableWithRenewal.value ? 'outline' : undefined,
click: async () =>
- await updateOsChangelogStore.setReleaseForUpdate(updateOsResponse.value ?? null),
+ await updateOsStore.setReleaseForUpdate(updateOsResponse.value ?? null),
icon: EyeIcon,
text: availableWithRenewal.value
? props.t('View Changelog')
diff --git a/web/components/UpdateOs/RawChangelogRenderer.vue b/web/components/UpdateOs/RawChangelogRenderer.vue
new file mode 100644
index 000000000..afeec90af
--- /dev/null
+++ b/web/components/UpdateOs/RawChangelogRenderer.vue
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+ {{ props.t(`Error Parsing Changelog • {0}`, [parseChangelogFailed]) }}
+
+
+ {{
+ props.t(`It's highly recommended to review the changelog before continuing your update`)
+ }}
+
+
+
+ {{ props.t('View Changelog on Docs') }}
+
+
+
+
+
{{ parsedChangelogTitle }}
+
+
+
+
+
{{ props.t('Loading changelog...') }}
+
+
+
{{ props.t('No changelog content available') }}
+
+
+
\ No newline at end of file
diff --git a/web/helpers/urls.ts b/web/helpers/urls.ts
index 36ccea357..a1fea9faf 100644
--- a/web/helpers/urls.ts
+++ b/web/helpers/urls.ts
@@ -40,6 +40,9 @@ const DOCS_REGISTRATION_REPLACE_KEY = new URL('/go/changing-the-flash-device/',
const SUPPORT = new URL('https://unraid.net');
+export const allowedDocsOriginRegex = /^https:\/\/(?:[\w-]+\.)*docs\.unraid\.net(?::\d+)?$/;
+export const allowedDocsUrlRegex = /^https:\/\/(?:[\w-]+\.)*docs\.unraid\.net(?::\d+)?\//;
+
export {
ACCOUNT,
ACCOUNT_CALLBACK,
diff --git a/web/locales/en_US.json b/web/locales/en_US.json
index 4092ea3a9..807c7dfae 100644
--- a/web/locales/en_US.json
+++ b/web/locales/en_US.json
@@ -384,5 +384,6 @@
"Your Trial key has been extended!": "Your Trial key has been extended!",
"Create an Unraid.net account and activate your key": "Create an Unraid.net account and activate your key",
"Device is ready to configure": "Device is ready to configure",
- "Secure your device": "Secure your device"
+ "Secure your device": "Secure your device",
+ "Unraid OS {0} Changelog": "Unraid OS {0} Changelog"
}
diff --git a/web/pages/changelog.vue b/web/pages/changelog.vue
index b615449c8..fc9efaf1b 100644
--- a/web/pages/changelog.vue
+++ b/web/pages/changelog.vue
@@ -1,50 +1,88 @@
Changelog
-
-
+
+
- Test Changelog Modal
+ Test Changelog Modal (from releases endpoint)
+
+
+ Test Changelog Modal (with test data)
+
+
+ Test Without Pretty Changelog
+
+
+ Test Broken Parse Changelog
-
-
-
diff --git a/web/store/updateOs.ts b/web/store/updateOs.ts
index b75f1c3f5..9ead8f540 100644
--- a/web/store/updateOs.ts
+++ b/web/store/updateOs.ts
@@ -9,6 +9,8 @@ import type { ServerUpdateOsResponse } from '~/types/server';
import { WebguiCheckForUpdate, WebguiUpdateCancel } from '~/composables/services/webgui';
import { useServerStore } from '~/store/server';
+import prerelease from 'semver/functions/prerelease';
+import { useCallbackActionsStore } from '~/store/callbackActions';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
@@ -23,7 +25,9 @@ extend(relativeTime);
export const useUpdateOsStore = defineStore('updateOs', () => {
// state
const checkForUpdatesLoading = ref
(false);
- const modalOpen = ref(false);
+ const updateOsModalVisible = ref(false);
+ const releaseForUpdate = ref(null);
+ const changelogModalVisible = computed(() => !!releaseForUpdate.value);
// getters from other stores
const serverStore = useServerStore();
@@ -59,6 +63,33 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
*/
const availableRequiresAuth = computed((): boolean => !updateOsResponse.value?.sha256);
+ // Changelog logic
+ const changelogUrl = computed((): string => releaseForUpdate.value?.changelog || '');
+ const changelogPretty = computed(() => releaseForUpdate.value?.changelogPretty ?? null);
+ const setReleaseForUpdate = (release: ServerUpdateOsResponse | null) => {
+ releaseForUpdate.value = release;
+ };
+ // isReleaseForUpdateStable logic (true if no prerelease in version)
+ const isReleaseForUpdateStable = computed(() => {
+ if (!releaseForUpdate.value?.version) return false;
+ return !prerelease(releaseForUpdate.value.version);
+ });
+ // fetchAndConfirmInstall logic
+ const callbackStore = useCallbackActionsStore();
+ const fetchAndConfirmInstall = (sha256: string) => {
+ callbackStore.send(
+ window.location.href,
+ [
+ {
+ sha256,
+ type: 'updateOs',
+ },
+ ],
+ undefined,
+ 'forUpc'
+ );
+ };
+
// actions
const localCheckForUpdate = async (): Promise => {
checkForUpdatesLoading.value = true;
@@ -95,7 +126,7 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
};
const setModalOpen = (val: boolean) => {
- modalOpen.value = val;
+ updateOsModalVisible.value = val;
};
return {
@@ -103,14 +134,21 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
available,
availableWithRenewal,
checkForUpdatesLoading,
- modalOpen,
+ updateOsModalVisible,
+ changelogModalVisible,
+ releaseForUpdate,
updateOsIgnoredReleases,
// getters
availableReleaseDate,
availableRequiresAuth,
+ changelogUrl,
+ changelogPretty,
+ isReleaseForUpdateStable,
// actions
localCheckForUpdate,
cancelUpdate,
setModalOpen,
+ setReleaseForUpdate,
+ fetchAndConfirmInstall,
};
});
diff --git a/web/store/updateOsChangelog.ts b/web/store/updateOsChangelog.ts
deleted file mode 100644
index 4bd1ab5cd..000000000
--- a/web/store/updateOsChangelog.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import { computed, ref, watch } from 'vue';
-import { defineStore } from 'pinia';
-
-import { Markdown } from '@/helpers/markdown';
-import { DOCS_RELEASE_NOTES } from '~/helpers/urls';
-import { baseUrl } from 'marked-base-url';
-import prerelease from 'semver/functions/prerelease';
-
-import type { ServerUpdateOsResponse } from '~/types/server';
-
-import { request } from '~/composables/services/request';
-import { useCallbackActionsStore } from '~/store/callbackActions';
-
-export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () => {
- const callbackStore = useCallbackActionsStore();
-
- const releaseForUpdate = ref(null);
- watch(releaseForUpdate, async (newVal, oldVal) => {
- console.debug('[releaseForUpdate] watch', newVal, oldVal);
- resetChangelogDetails(); // reset values when setting and unsetting a selected release
- // Fetch and parse the changelog when the user selects a release
- if (newVal) {
- await fetchAndParseChangelog();
- }
- });
-
- const changelogUrl = computed((): string => {
- if (!releaseForUpdate.value || !releaseForUpdate.value?.changelog) {
- return '';
- }
- return (
- releaseForUpdate.value?.changelog ??
- `https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/${releaseForUpdate.value.version}.md`
- );
- });
-
- const isReleaseForUpdateStable = computed(() =>
- releaseForUpdate.value ? prerelease(releaseForUpdate.value.version) === null : false
- );
- const parsedChangelog = ref('');
- const parseChangelogFailed = ref('');
- // used to remove the first and it's contents from the parsedChangelog
- const mutatedParsedChangelog = computed(() => {
- if (parsedChangelog.value) {
- return parsedChangelog.value.replace(/(.*?)<\/h1>/, '');
- }
- return parsedChangelog.value;
- });
- // used to extract the first and it's contents from the parsedChangelog for the modal header title
- const parsedChangelogTitle = computed(() => {
- if (parseChangelogFailed.value) {
- return parseChangelogFailed.value;
- }
- if (parsedChangelog.value) {
- return (
- parsedChangelog.value.match(/(.*?)<\/h1>/)?.[1] ??
- `Version ${releaseForUpdate.value?.version} ${releaseForUpdate.value?.date}`
- );
- }
- return '';
- });
-
- const setReleaseForUpdate = (release: ServerUpdateOsResponse | null) => {
- console.debug('[setReleaseForUpdate]', release);
- releaseForUpdate.value = release;
- };
- const resetChangelogDetails = () => {
- console.debug('[resetChangelogDetails]');
- parsedChangelog.value = '';
- parseChangelogFailed.value = '';
- };
- const fetchAndParseChangelog = async () => {
- console.debug('[fetchAndParseChangelog]');
- try {
- const changelogMarkdownRaw = await request
- .url(changelogUrl.value ?? '')
- .get()
- .text();
-
- // set base url for relative links
- const marked = Markdown.create(baseUrl(DOCS_RELEASE_NOTES.toString()));
-
- // open links in new tab & replace .md from links
- const renderer = new marked.Renderer();
-
- renderer.link = ({ href, title, tokens }) => {
- const linkText = renderer.parser.parseInline(tokens);
- const cleanHref = href.replace('.md', ''); // remove .md from href
- return `${linkText} `;
- };
-
- marked.setOptions({
- renderer,
- });
-
- parsedChangelog.value = await marked.parse(changelogMarkdownRaw);
- } catch (error: unknown) {
- const caughtError = error as Error;
- parseChangelogFailed.value =
- caughtError && caughtError?.message
- ? caughtError.message
- : `Failed to parse ${releaseForUpdate.value?.version} changelog`;
- }
- };
-
- const fetchAndConfirmInstall = (sha256: string) => {
- callbackStore.send(
- window.location.href,
- [
- {
- sha256,
- type: 'updateOs',
- },
- ],
- undefined,
- 'forUpc'
- );
- };
-
- return {
- // state
- parseChangelogFailed,
- releaseForUpdate,
- // getters
- isReleaseForUpdateStable,
- mutatedParsedChangelog,
- parsedChangelogTitle,
- // actions
- setReleaseForUpdate,
- fetchAndConfirmInstall,
- };
-});