diff --git a/web/_data/osReleases.ts b/web/_data/osReleases.ts index 1a5225796..4c1cd5b43 100644 --- a/web/_data/osReleases.ts +++ b/web/_data/osReleases.ts @@ -43,32 +43,32 @@ const testOsReleasesResponse: OsReleasesResponse = { } ], "stable": [ - { - "version": "6.12.5", - "name": "Unraid Server 6.12.5", - "basefile": "unRAIDServer-6.12.5-x86_64.zip", - "date": "2023-08-31", - "url": "https://dl.stable.unraid.net/unRAIDServer-6.12.5-x86_64.zip", - "changelog": "https://unraid.net/blog/unraid-os-6.12.5-release-notes", - "md5": "FAKEbddcf415f2d0518804e551c16125", - "size": 12345122, - "sha256": "fda177bb1336270b24e4df0fd0c1dd0596c44699204f57c83ce70a0f19173be4", - "plugin_url": "https://dl.stable.unraid.net/unRAIDServer-6.12.5.plg", - "plugin_sha256": "83850536ed6982bd582ed107d977d59e9b9b786363e698b14d1daf52e2dec2d9" - }, - { - "version": "6.12.4", - "name": "Unraid Server 6.12.4", - "basefile": "unRAIDServer-6.12.4-x86_64.zip", - "date": "2023-08-31", - "url": "https://dl.stable.unraid.net/unRAIDServer-6.12.4-x86_64.zip", - "changelog": "https://unraid.net/blog/unraid-os-6.12.4-release-notes", - "md5": "9050bddcf415f2d0518804e551c1be98", - "size": 12345122, - "sha256": "fda177bb1336270b24e4df0fd0c1dd0596c44699204f57c83ce70a0f19173be4", - "plugin_url": "https://dl.stable.unraid.net/unRAIDServer-6.12.4.plg", - "plugin_sha256": "83850536ed6982bd582ed107d977d59e9b9b786363e698b14d1daf52e2dec2d9" - }, + // { + // "version": "6.12.5", + // "name": "Unraid Server 6.12.5", + // "basefile": "unRAIDServer-6.12.5-x86_64.zip", + // "date": "2023-08-31", + // "url": "https://dl.stable.unraid.net/unRAIDServer-6.12.5-x86_64.zip", + // "changelog": "https://unraid.net/blog/unraid-os-6.12.5-release-notes", + // "md5": "FAKEbddcf415f2d0518804e551c16125", + // "size": 12345122, + // "sha256": "fda177bb1336270b24e4df0fd0c1dd0596c44699204f57c83ce70a0f19173be4", + // "plugin_url": "https://dl.stable.unraid.net/unRAIDServer-6.12.5.plg", + // "plugin_sha256": "83850536ed6982bd582ed107d977d59e9b9b786363e698b14d1daf52e2dec2d9" + // }, + // { + // "version": "6.12.4", + // "name": "Unraid Server 6.12.4", + // "basefile": "unRAIDServer-6.12.4-x86_64.zip", + // "date": "2023-08-31", + // "url": "https://dl.stable.unraid.net/unRAIDServer-6.12.4-x86_64.zip", + // "changelog": "https://unraid.net/blog/unraid-os-6.12.4-release-notes", + // "md5": "9050bddcf415f2d0518804e551c1be98", + // "size": 12345122, + // "sha256": "fda177bb1336270b24e4df0fd0c1dd0596c44699204f57c83ce70a0f19173be4", + // "plugin_url": "https://dl.stable.unraid.net/unRAIDServer-6.12.4.plg", + // "plugin_sha256": "83850536ed6982bd582ed107d977d59e9b9b786363e698b14d1daf52e2dec2d9" + // }, { "version": "6.12.3", "name": "Unraid Server 6.12.3", diff --git a/web/_data/serverState.ts b/web/_data/serverState.ts index c3901b69a..db56cfe01 100644 --- a/web/_data/serverState.ts +++ b/web/_data/serverState.ts @@ -70,7 +70,7 @@ export const serverState: Server = { bgColor: '', descriptionShow: true, metaColor: '', - name: 'black', + name: 'white', textColor: '' }, uptime, diff --git a/web/components/Brand/Button.vue b/web/components/Brand/Button.vue index 0d480173d..464f013ea 100644 --- a/web/components/Brand/Button.vue +++ b/web/components/Brand/Button.vue @@ -30,11 +30,11 @@ defineEmits(['click']); const classes = computed(() => { switch (props.btnStyle) { case 'fill': - return 'text-white bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60'; + return 'text-white bg-gradient-to-r from-unraid-red to-orange shadow-none hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60 hover:shadow-md focus:shadow-md'; case 'outline': - return 'text-orange bg-gradient-to-r from-transparent to-transparent border-2 border-solid border-orange hover:text-white focus:text-white hover:from-unraid-red hover:to-orange focus:from-unraid-red focus:to-orange hover:border-transparent focus:border-transparent'; + return 'text-orange bg-gradient-to-r from-transparent to-transparent border-2 border-solid border-orange shadow-none hover:text-white focus:text-white hover:from-unraid-red hover:to-orange focus:from-unraid-red focus:to-orange hover:shadow-md focus:shadow-md'; case 'underline': - return 'opacity-75 hover:opacity-100 focus:opacity-100 underline transition hover:text-alpha hover:bg-beta focus:text-alpha focus:bg-beta'; + return 'opacity-75 hover:opacity-100 focus:opacity-100 underline transition shadow-none hover:text-alpha hover:bg-beta focus:text-alpha focus:bg-beta hover:shadow-md focus:shadow-md'; } }); diff --git a/web/components/I18nHost.ce.vue b/web/components/I18nHost.ce.vue index 3690d6f95..2564235e9 100644 --- a/web/components/I18nHost.ce.vue +++ b/web/components/I18nHost.ce.vue @@ -45,3 +45,9 @@ provide(I18nInjectionKey, i18n); + + \ No newline at end of file diff --git a/web/components/Ui/Badge.vue b/web/components/Ui/Badge.vue new file mode 100644 index 000000000..40b104882 --- /dev/null +++ b/web/components/Ui/Badge.vue @@ -0,0 +1,109 @@ + + + diff --git a/web/components/Ui/CardWrapper.vue b/web/components/Ui/CardWrapper.vue new file mode 100644 index 000000000..cd1236e7e --- /dev/null +++ b/web/components/Ui/CardWrapper.vue @@ -0,0 +1,14 @@ + + + diff --git a/web/components/UpdateOs.ce.vue b/web/components/UpdateOs.ce.vue index bfca23aa2..86d08e625 100644 --- a/web/components/UpdateOs.ce.vue +++ b/web/components/UpdateOs.ce.vue @@ -3,118 +3,44 @@ * @todo require keyfile to be set before allowing user to check for updates * @todo require keyfile to update * @todo require valid guid / server state to update - * @todo detect downgrade possibility */ -import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue' -import { ArrowTopRightOnSquareIcon, BellAlertIcon } from '@heroicons/vue/24/solid'; -import dayjs from 'dayjs'; +import dayjs, { extend } from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; import { storeToRefs } from 'pinia'; -import { ref, watchEffect } from 'vue'; import { useI18n } from 'vue-i18n'; +import { useUpdateOsStore } from '~/store/updateOsActions'; + import 'tailwindcss/tailwind.css'; import '~/assets/main.css'; -import { useServerStore } from '~/store/server'; -import { useUpdateOsStore, useUpdateOsActionsStore } from '~/store/updateOsActions'; -import type { UserProfileLink } from '~/types/userProfile'; - const { t } = useI18n(); -const serverStore = useServerStore(); -const updateOsStore = useUpdateOsStore(); -const updateOsActionsStore = useUpdateOsActionsStore(); - -const { guid, keyfile, osVersion } = storeToRefs(serverStore); -const { available, cachedReleasesTimestamp } = storeToRefs(updateOsStore); - -const includeNext = ref(false); - -const updateButton = ref(); - -const parsedCachedReleasesTimestamp = computed(() => { - if (!cachedReleasesTimestamp.value) { return ''; } - return dayjs(cachedReleasesTimestamp.value).format('YYYY-MM-DD HH:mm:ss'); +export interface Props { + restoreVersion?: string; +} +withDefaults(defineProps(), { + restoreVersion: '', }); -const check = () => { - updateOsStore.checkForUpdate({ - cache: true, - guid: guid.value, - includeNext: includeNext.value, - keyfile: keyfile.value, - osVersion: osVersion.value, - skipCache: true, - }); -}; +const updateOsStore = useUpdateOsStore(); +const { cachedReleasesTimestamp } = storeToRefs(updateOsStore); -watchEffect(() => { - if (available.value) { - updateButton.value = updateOsActionsStore.initUpdateOsCallback(); - } else { - updateButton.value = undefined; - } +extend(relativeTime); +const parsedReleaseTimestamp = computed(() => { + if (!cachedReleasesTimestamp.value) { return ''; } + return { + formatted: dayjs(cachedReleasesTimestamp.value).format('YYYY-MM-DD HH:mm:ss'), + relative: dayjs().to(dayjs(cachedReleasesTimestamp.value)), + }; }); diff --git a/web/components/UpdateOs/CheckButton.vue b/web/components/UpdateOs/CheckButton.vue new file mode 100644 index 000000000..8951a5429 --- /dev/null +++ b/web/components/UpdateOs/CheckButton.vue @@ -0,0 +1,88 @@ + + + diff --git a/web/components/UpdateOs/Downgrade.vue b/web/components/UpdateOs/Downgrade.vue new file mode 100644 index 000000000..c95b04518 --- /dev/null +++ b/web/components/UpdateOs/Downgrade.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/web/components/UpdateOs/Status.vue b/web/components/UpdateOs/Status.vue new file mode 100644 index 000000000..5e9f5edcc --- /dev/null +++ b/web/components/UpdateOs/Status.vue @@ -0,0 +1,66 @@ + + + diff --git a/web/components/UpdateOs/Update.vue b/web/components/UpdateOs/Update.vue new file mode 100644 index 000000000..7fddb850c --- /dev/null +++ b/web/components/UpdateOs/Update.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/web/components/UserProfile.ce.vue b/web/components/UserProfile.ce.vue index 2f524b5a1..dadfb920e 100644 --- a/web/components/UserProfile.ce.vue +++ b/web/components/UserProfile.ce.vue @@ -36,6 +36,7 @@ const { connectPluginInstalled, } = storeToRefs(serverStore); const { bannerGradient, theme } = storeToRefs(useThemeStore()); +const { isOsVersionStable } = storeToRefs(updateOsStore); const hideDropdown = computed(() => state.value === 'PRO' && !connectPluginInstalled.value); @@ -84,9 +85,11 @@ onBeforeMount(() => { } callbackStore.watcher(); + updateOsStore.checkForUpdate({ cache: true, guid: guid.value, + includeNext: isOsVersionStable.value, // @todo ensure this is correct keyfile: keyfile.value, osVersion: osVersion.value, }); diff --git a/web/components/UserProfile/DropdownTrigger.vue b/web/components/UserProfile/DropdownTrigger.vue index 2376557f3..8a26dc7ec 100644 --- a/web/components/UserProfile/DropdownTrigger.vue +++ b/web/components/UserProfile/DropdownTrigger.vue @@ -3,6 +3,7 @@ import { storeToRefs } from 'pinia'; import { Bars3Icon, Bars3BottomRightIcon, + BellAlertIcon, ExclamationTriangleIcon, InformationCircleIcon, ShieldExclamationIcon, @@ -11,6 +12,7 @@ import { import { useDropdownStore } from '~/store/dropdown'; import { useErrorsStore } from '~/store/errors'; import { useServerStore } from '~/store/server'; +import { useUpdateOsStore } from '~/store/updateOsActions'; const props = defineProps<{ t: any; }>(); @@ -18,6 +20,7 @@ const dropdownStore = useDropdownStore(); const { dropdownVisible } = storeToRefs(dropdownStore); const { errors } = storeToRefs(useErrorsStore()); const { state, stateData } = storeToRefs(useServerStore()); +const { available: osUpdateAvailable } = storeToRefs(useUpdateOsStore()); const showErrorIcon = computed(() => errors.value.length || stateData.value.error); @@ -49,9 +52,16 @@ const title = computed((): string => { + + - + + + + diff --git a/web/locales/en_US.json b/web/locales/en_US.json index 2b7b1de9c..fbb9cc8c0 100644 --- a/web/locales/en_US.json +++ b/web/locales/en_US.json @@ -208,10 +208,24 @@ "Unraid OS Update Available": "Unraid OS Update Available", "Update Unraid OS confirmation required": "Update Unraid OS confirmation required", "Please confirm the update details below": "Please confirm the update details below", - "Current Version: Unraid {0}": "Current Version: Unraid {0}", + "Current Version {0}": "Current Version {0}", "New Version: {0}": "New Version: {0}", "This update will require a reboot": "This update will require a reboot", "Confirm and start update": "Confirm and start update", "Update Unraid OS": "Update Unraid OS", - "Last checked: {0}": "Last checked: {0}" + "Last checked: {0}": "Last checked: {0}", + "Downgrade Unraid OS": "Downgrade Unraid OS", + "Downgrade Unraid OS to {0}": "Downgrade Unraid OS to {0}", + "Begin restore to Unraid {0}": "Begin restore to Unraid {0}", + "Version available for restore {0}": "Version available for restore {0}", + "Check for Updates": "Check for Updates", + "Include Prereleases": "Include Prereleases", + "Receive the latest and greatest for Unraid OS. Whether it new features, security patches, or bug fixes – keeping your server up-to-date ensures the best experience that Unraid has to offer.": "Receive the latest and greatest for Unraid OS. Whether it new features, security patches, or bug fixes – keeping your server up-to-date ensures the best experience that Unraid has to offer.", + "Check For Updates": "Check For Updates", + "Checking...": "Checking...", + "View changelog for current version {0}": "View changelog for current version {0}", + "View Changelog for {0}": "View Changelog for {0}", + "View changelog & update": "View changelog & update", + "{0} Release Notes": "{0} Release Notes", + "Unable to open release notes": "Unable to open release notes" } diff --git a/web/package-lock.json b/web/package-lock.json index c02888349..cd122c7b5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -20,6 +20,7 @@ "graphql-tag": "^2.12.6", "graphql-ws": "^5.14.0", "hex-to-rgba": "^2.0.1", + "marked": "^9.0.3", "semver": "^7.5.4", "vue-i18n": "^9.2.2", "wretch": "^2.6.0" @@ -13269,6 +13270,17 @@ "node": ">=0.10.0" } }, + "node_modules/marked": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.0.3.tgz", + "integrity": "sha512-pI/k4nzBG1PEq1J3XFEHxVvjicfjl8rgaMaqclouGSMPhk7Q3Ejb2ZRxx/ZQOcQ1909HzVoWCFYq6oLgtL4BpQ==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", diff --git a/web/package.json b/web/package.json index 2feaa8535..f03228fe5 100644 --- a/web/package.json +++ b/web/package.json @@ -49,6 +49,7 @@ "graphql-tag": "^2.12.6", "graphql-ws": "^5.14.0", "hex-to-rgba": "^2.0.1", + "marked": "^9.0.3", "semver": "^7.5.4", "vue-i18n": "^9.2.2", "wretch": "^2.6.0" diff --git a/web/pages/index.vue b/web/pages/index.vue index 63f4e1c0d..398261b04 100644 --- a/web/pages/index.vue +++ b/web/pages/index.vue @@ -18,7 +18,9 @@ onBeforeMount(() => {

UserProfileCe

- +
+ +

DownloadApiLogsCe @@ -43,7 +45,7 @@ onBeforeMount(() => {

UpdateOsCe

- +

ModalsCe diff --git a/web/pages/webComponents.vue b/web/pages/webComponents.vue index bdb62e81d..bfed4f373 100644 --- a/web/pages/webComponents.vue +++ b/web/pages/webComponents.vue @@ -16,7 +16,9 @@ onBeforeMount(() => {

UserProfileCe

- +
+ +

DownloadApiLogsCe diff --git a/web/store/updateOs.ts b/web/store/updateOs.ts index caeb96f56..a9414ffb8 100644 --- a/web/store/updateOs.ts +++ b/web/store/updateOs.ts @@ -71,6 +71,10 @@ export const useUpdateOsStoreGeneric = ( // getters const cachedReleasesTimestamp = computed(() => releases.value?.timestamp); const isOsVersionStable = computed(() => !isVersionStable(osVersion.value)); + const isAvailableStable = computed(() => { + if (!available.value) return undefined; + return !isVersionStable(available.value); + }); const filteredStableReleases = computed(() => { if (!osVersion.value) return undefined; @@ -106,6 +110,7 @@ export const useUpdateOsStoreGeneric = ( }); // actions const setReleasesState = (response: ReleasesResponse) => { + console.debug('[setReleasesState]'); releases.value = { timestamp: Date.now(), response, @@ -113,44 +118,64 @@ export const useUpdateOsStoreGeneric = ( } const cacheReleasesResponse = () => { + console.debug('[cacheReleasesResponse]'); localStorage.setItem(RELEASES_LOCAL_STORAGE_KEY, JSON.stringify(releases.value)); }; - const purgeReleasesCache = () => { + const purgeReleasesCache = async () => { + console.debug('[purgeReleasesCache]'); releases.value = undefined; - localStorage.removeItem(RELEASES_LOCAL_STORAGE_KEY); + await localStorage.removeItem(RELEASES_LOCAL_STORAGE_KEY); }; const requestReleases = async (payload: RequestReleasesPayload): Promise => { + console.debug('[requestReleases]', payload); + if (!payload || !payload.guid || !payload.keyfile) { throw new Error('Invalid Payload for updateOs.requestReleases'); } if (payload.skipCache) { - purgeReleasesCache(); + await purgeReleasesCache(); } - /** - * Compare the timestamp of the cached releases data to the current time, - * if it's older than 7 days, reset releases. - * Which will trigger a new API call to get the releases. - * Otherwise skip the API call and use the cached data. - */ - if (releases.value) { - const currentTime = new Date().getTime(); - const cacheDuration = import.meta.env.DEV ? 30000 : 604800000; // 30 seconds for testing, 7 days for prod - if (currentTime - releases.value.timestamp > cacheDuration) { - purgeReleasesCache(); - } else { - // if the cache is valid return the existing response - return releases.value.response; - } - } + * Compare the timestamp of the cached releases data to the current time, + * if it's older than 7 days, reset releases. + * Which will trigger a new API call to get the releases. + * Otherwise skip the API call and use the cached data. + */ + else if (!payload.skipCache && releases.value) { + const currentTime = new Date().getTime(); + const cacheDuration = import.meta.env.DEV ? 30000 : 604800000; // 30 seconds for testing, 7 days for prod + if (currentTime - releases.value.timestamp > cacheDuration) { + // cache is expired, purge it + console.debug('[requestReleases] cache EXPIRED'); + await purgeReleasesCache(); + } else { + // if the cache is valid return the existing response + console.debug('[requestReleases] cache VALID'); + return releases.value.response; + } + } // If here we're needing to fetch a new releases…whether it's the first time or b/c the cache was expired try { - // const response: ReleasesResponse = await request.url(OS_RELEASES.toString()).get().json(); - const response: ReleasesResponse = await testReleasesResponse; + console.debug('[requestReleases] fetching new releases', testReleasesResponse); + /** + * @todo replace with real api call, note that the structuredClone is required otherwise Vue will not provided a reactive object from the original static response + * const response: ReleasesResponse = await request.url(OS_RELEASES.toString()).get().json(); + */ + const response: ReleasesResponse = await structuredClone(testReleasesResponse); + console.debug('[requestReleases] response', response); + /** + * If we're on stable and the user hasn't requested to include next releases in the check + * then remove next releases from the data + */ + console.debug('[requestReleases] checking for next releases', payload.includeNext, response.next) + if (!payload.includeNext && response.next) { + console.debug('[requestReleases] removing next releases from data') + delete response.next; + } // save it to local state setReleasesState(response); @@ -175,6 +200,9 @@ export const useUpdateOsStoreGeneric = ( // set the osVersion since this is the first thing in this store using it…that way we don't need to import the server store in this store. osVersion.value = payload.osVersion; + // reset available + available.value = ''; + // gets releases from cache or fetches from api await requestReleases(payload); @@ -182,16 +210,6 @@ export const useUpdateOsStoreGeneric = ( return console.error('[checkForUpdate] no releases found'); } - /** - * If we're on stable and the user hasn't requested to include next releases in the check - * then remove next releases from the data - */ - console.debug('[checkForUpdate] checking for next releases', payload.includeNext, isOsVersionStable.value, releases.value.response.next) - if (!payload.includeNext || isOsVersionStable.value && releases.value.response.next) { - console.debug('[checkForUpdate] removing next releases from data') - delete releases.value.response.next; - } - Object.keys(releases.value.response ?? {}).forEach(key => { // this is just to make TS happy (it's already checked above…thanks github copilot for knowing what I needed) if (!releases.value) { @@ -247,6 +265,7 @@ export const useUpdateOsStoreGeneric = ( // getters cachedReleasesTimestamp, isOsVersionStable, + isAvailableStable, filteredStableReleases, filteredNextReleases, allFilteredReleases, diff --git a/web/tailwind.config.ts b/web/tailwind.config.ts index 66d54a06b..b2788dcf1 100644 --- a/web/tailwind.config.ts +++ b/web/tailwind.config.ts @@ -96,6 +96,7 @@ export default >{ '350px': '350px', '640px': '640px', '800px': '800px', + '1024px': '1024px', }, screens: { '2xs': '470px',