mirror of
https://github.com/unraid/api.git
synced 2026-01-05 08:00:33 -06:00
refactor: downgrade and update improvements with store refactors
This commit is contained in:
@@ -37,12 +37,20 @@ withDefaults(defineProps<Props>(), {
|
||||
const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
|
||||
const { rebootType } = storeToRefs(updateOsActionsStore);
|
||||
|
||||
const subtitle = computed(() => {
|
||||
if (rebootType.value === 'downgrade') {
|
||||
return t('Please finish the initiated update to enable a downgrade.');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPageContainer>
|
||||
<UpdateOsStatus
|
||||
:title="t('Downgrade Unraid OS')"
|
||||
:subtitle="subtitle"
|
||||
:downgrade-not-available="restoreVersion === ''"
|
||||
:t="t"
|
||||
/>
|
||||
|
||||
@@ -33,7 +33,7 @@ const showRebootRequired = computed(() => rebootType.value !== '');
|
||||
<button
|
||||
class="group leading-none"
|
||||
:title="t('View release notes')"
|
||||
@click="updateOsActionsStore.viewCurrentReleaseNotes(t('{0} Release Notes', [osVersion]))"
|
||||
@click="updateOsActionsStore.viewReleaseNotes(t('{0} Release Notes', [osVersion]))"
|
||||
>
|
||||
<UiBadge
|
||||
color="custom"
|
||||
|
||||
@@ -23,6 +23,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const { dateTimeFormat } = storeToRefs(serverStore);
|
||||
const {
|
||||
outputDateTimeFormatted: formattedReleaseDate,
|
||||
|
||||
@@ -36,7 +36,7 @@ const serverStore = useServerStore();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
|
||||
const { dateTimeFormat, osVersion, regExp, regUpdatesExpired } = storeToRefs(serverStore);
|
||||
const { dateTimeFormat, osVersion, rebootVersion, regExp, regUpdatesExpired } = storeToRefs(serverStore);
|
||||
const { available, availableWithRenewal, parsedReleaseTimestamp } = storeToRefs(updateOsStore);
|
||||
const { ineligibleText, rebootType, rebootTypeText, status } = storeToRefs(updateOsActionsStore);
|
||||
|
||||
@@ -75,7 +75,7 @@ const regExpOutput = computed(() => {
|
||||
<button
|
||||
class="group"
|
||||
:title="t('View release notes')"
|
||||
@click="updateOsActionsStore.viewCurrentReleaseNotes(t('{0} Release Notes', [osVersion]))"
|
||||
@click="updateOsActionsStore.viewReleaseNotes(t('{0} Release Notes', [osVersion]))"
|
||||
>
|
||||
<UiBadge :icon="InformationCircleIcon" class="underline">
|
||||
{{ t('Current Version {0}', [osVersion]) }}
|
||||
@@ -151,9 +151,9 @@ const regExpOutput = computed(() => {
|
||||
:t="t"
|
||||
/>
|
||||
<BrandButton
|
||||
v-else-if="rebootType === 'downgrade' || rebootType === 'upgrade'"
|
||||
v-else-if="rebootType === 'downgrade' || rebootType === 'update'"
|
||||
:icon="ArrowPathIcon"
|
||||
:text="rebootType === 'downgrade' ? t('Reboot Now to Downgrade') : t('Reboot Now to Update')"
|
||||
:text="rebootType === 'downgrade' ? t('Reboot Now to Downgrade to {0}', [rebootVersion]) : t('Reboot Now to Update to {0}', [rebootVersion])"
|
||||
@click="updateOsActionsStore.rebootServer()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -238,8 +238,12 @@
|
||||
"Downgrades are only recommended if you're unable to solve a critical issue. In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue. You will be prompted with the option to download the Diagnostics zip once the downgrade process is started. From there please open a bug report on our forums with a description of the issue and include your diagnostics.": "Downgrades are only recommended if you're unable to solve a critical issue. In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue. You will be prompted with the option to download the Diagnostics zip once the downgrade process is started. From there please open a bug report on our forums with a description of the issue and include your diagnostics.",
|
||||
"Reboot Now to Downgrade": "Reboot Now to Downgrade",
|
||||
"Reboot Now to Update": "Reboot Now to Update",
|
||||
"Reboot Now to Downgrade to {0}": "Reboot Now to Downgrade to {0}",
|
||||
"Reboot Now to Update to {0}": "Reboot Now to Update to {0}",
|
||||
"Reboot Required for Downgrade": "Reboot Required for Downgrade",
|
||||
"Reboot Required for Update": "Reboot Required for Update",
|
||||
"Reboot Required for Downgrade to {0}": "Reboot Required for Downgrade to {0}",
|
||||
"Reboot Required for Update to {0}": "Reboot Required for Update to {0}",
|
||||
"Updating 3rd party drivers": "Updating 3rd party drivers",
|
||||
"Update Available": "Update Available",
|
||||
"Up-to-date": "Up-to-date",
|
||||
@@ -322,5 +326,7 @@
|
||||
"Calculating OS Update Eligibility…": "Calculating OS Update Eligibility…",
|
||||
"Cancel": "Cancel",
|
||||
"Unknown error": "Unknown error",
|
||||
"Installing Extended": "Installing Extended"
|
||||
"Installing Extended": "Installing Extended",
|
||||
"Please finish the initiated downgrade to enable updates.": "Please finish the initiated downgrade to enable updates.",
|
||||
"Please finish the initiated update to enable a downgrade.": "Please finish the initiated update to enable a downgrade."
|
||||
}
|
||||
|
||||
@@ -89,6 +89,8 @@ export const useServerStore = defineStore('server', () => {
|
||||
const name = ref<string>('');
|
||||
const osVersion = ref<string>('');
|
||||
const osVersionBranch = ref<ServerOsVersionBranch>('stable');
|
||||
const rebootType = ref<'thirdPartyDriversDownloading' | 'downgrade' | 'update' | ''>('');
|
||||
const rebootVersion = ref<string | undefined>();
|
||||
const registered = ref<boolean>();
|
||||
const regDev = ref<number>(0);
|
||||
const regGen = ref<number>(0);
|
||||
@@ -742,6 +744,7 @@ export const useServerStore = defineStore('server', () => {
|
||||
* Actions
|
||||
*/
|
||||
const setServer = (data: Server) => {
|
||||
console.debug('[setServer]', data);
|
||||
if (typeof data?.apiKey !== 'undefined') { apiKey.value = data.apiKey; }
|
||||
if (typeof data?.apiVersion !== 'undefined') { apiVersion.value = data.apiVersion; }
|
||||
if (typeof data?.avatar !== 'undefined') { avatar.value = data.avatar; }
|
||||
@@ -765,6 +768,8 @@ export const useServerStore = defineStore('server', () => {
|
||||
if (typeof data?.name !== 'undefined') { name.value = data.name; }
|
||||
if (typeof data?.osVersion !== 'undefined') { osVersion.value = data.osVersion; }
|
||||
if (typeof data?.osVersionBranch !== 'undefined') { osVersionBranch.value = data.osVersionBranch; }
|
||||
if (typeof data?.rebootType !== 'undefined') { rebootType.value = data.rebootType; }
|
||||
if (typeof data?.rebootVersion !== 'undefined') { rebootVersion.value = data.rebootVersion; }
|
||||
if (typeof data?.registered !== 'undefined') { registered.value = data.registered; }
|
||||
if (typeof data?.regGen !== 'undefined') { regGen.value = data.regGen; }
|
||||
if (typeof data?.regGuid !== 'undefined') { regGuid.value = data.regGuid; }
|
||||
@@ -896,6 +901,12 @@ export const useServerStore = defineStore('server', () => {
|
||||
});
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (rebootVersion.value) {
|
||||
console.debug('[server.rebootVersion]', rebootVersion.value);
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// state
|
||||
apiKey,
|
||||
@@ -919,6 +930,8 @@ export const useServerStore = defineStore('server', () => {
|
||||
name,
|
||||
osVersion,
|
||||
osVersionBranch,
|
||||
rebootType,
|
||||
rebootVersion,
|
||||
registered,
|
||||
regDev,
|
||||
regGen,
|
||||
|
||||
@@ -140,7 +140,7 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
const isAvailableStable = computed(() => available.value ? isVersionStable(available.value) : false);
|
||||
|
||||
const filteredNextReleases = computed(() => {
|
||||
if (!osVersion.value) { return undefined; }
|
||||
if (!osVersion.value) return undefined;
|
||||
|
||||
if (releases.value?.response?.next) {
|
||||
return releases.value?.response?.next.filter(
|
||||
@@ -151,7 +151,7 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
});
|
||||
|
||||
const filteredPreviewReleases = computed(() => {
|
||||
if (!osVersion.value) { return undefined; }
|
||||
if (!osVersion.value) return undefined;
|
||||
|
||||
if (releases.value?.response?.preview) {
|
||||
return releases.value?.response?.preview.filter(
|
||||
@@ -162,7 +162,7 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
});
|
||||
|
||||
const filteredStableReleases = computed(() => {
|
||||
if (!osVersion.value) { return undefined; }
|
||||
if (!osVersion.value) return undefined;
|
||||
|
||||
if (releases.value?.response?.stable) {
|
||||
return releases.value?.response?.stable.filter(
|
||||
@@ -173,7 +173,7 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
});
|
||||
|
||||
const filteredTestReleases = computed(() => {
|
||||
if (!osVersion.value) { return undefined; }
|
||||
if (!osVersion.value) return undefined;
|
||||
|
||||
if (releases.value?.response?.test) {
|
||||
return releases.value?.response?.test.filter(
|
||||
@@ -193,7 +193,7 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
...(filteredNextReleases.value && { next: [...filteredNextReleases.value] }),
|
||||
...(filteredPreviewReleases.value && { preview: [...filteredPreviewReleases.value] }),
|
||||
...(filteredTestReleases.value && { test: [...filteredTestReleases.value] }),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -203,30 +203,35 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
*/
|
||||
const releasesUrl = computed((): typeof OS_RELEASES => {
|
||||
const isOnAccountApp = window.location.origin === ACCOUNT.origin;
|
||||
|
||||
/**
|
||||
* @note The webgui should only use stable and next URLs.
|
||||
* Users with test or preview would need to manually check for updates.
|
||||
*
|
||||
* Alternatively, what could be done is the webgui URLs slightly differ in that the JSON only contains the sha256s of the releases.
|
||||
* Rather than revealing the entire release object with the download URLs.
|
||||
* This may require download URLs to be generated on the fly or the URLs are randomized and don't follow a specific pattern.
|
||||
* Because https://stable.dl.unraid.net/unRAIDServer-6.12.4-x86_64.zip is a pretty obvious pattern.
|
||||
* Even if it means just adding a randomized hash to the end of the URL.
|
||||
* */
|
||||
const webguiNextBranch = !isOnAccountApp && osVersionBranch.value === 'next';
|
||||
const webguiPreviewBranch = !isOnAccountApp && osVersionBranch.value === 'preview';
|
||||
const webguiTestBranch = !isOnAccountApp && osVersionBranch.value === 'test';
|
||||
|
||||
const accountAppLoggedIn = isOnAccountApp && isLoggedIn.value;
|
||||
/** @todo should we remove the || checks directly below and only rely on the group? */
|
||||
const accountAppPreviewBranch = accountAppLoggedIn && (osVersionBranch.value === 'preview' || (authUserGroups.value && authUserGroups.value.includes('download_preview')));
|
||||
const accountAppTestBranch = accountAppLoggedIn && (osVersionBranch.value === 'test' || (authUserGroups.value && authUserGroups.value.includes('download_test')));
|
||||
|
||||
const accountAppPreviewBranch = accountAppLoggedIn && authUserGroups.value && authUserGroups.value.includes('download_preview');
|
||||
const accountAppTestBranch = accountAppLoggedIn && authUserGroups.value && authUserGroups.value.includes('download_test');
|
||||
console.debug('[releasesUrl]', {
|
||||
osVersionBranch: osVersionBranch.value,
|
||||
authUserGroups: authUserGroups.value,
|
||||
isOnAccountApp,
|
||||
webguiNextBranch,
|
||||
webguiPreviewBranch,
|
||||
webguiTestBranch,
|
||||
accountAppLoggedIn,
|
||||
accountAppPreviewBranch,
|
||||
accountAppTestBranch,
|
||||
});
|
||||
|
||||
const useNextBranch = webguiNextBranch || accountAppLoggedIn;
|
||||
const usePreviewBranch = webguiPreviewBranch || accountAppPreviewBranch;
|
||||
const useTestBranch = webguiTestBranch || accountAppTestBranch;
|
||||
const usePreviewBranch = accountAppPreviewBranch;
|
||||
const useTestBranch = accountAppTestBranch;
|
||||
console.debug('[releasesUrl]', {
|
||||
useNextBranch,
|
||||
usePreviewBranch,
|
||||
@@ -234,10 +239,10 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
});
|
||||
|
||||
let releasesUrl = OS_RELEASES;
|
||||
if (useNextBranch) { releasesUrl = OS_RELEASES_NEXT; }
|
||||
if (usePreviewBranch || useTestBranch || import.meta.env.VITE_OS_RELEASES_PREVIEW_FORCE) { releasesUrl = OS_RELEASES_PREVIEW; }
|
||||
/** @todo implement separate test branch json once available */
|
||||
// if (useTestBranch) releasesUrl = OS_RELEASES_PREVIEW.toString();
|
||||
if (useNextBranch) releasesUrl = OS_RELEASES_NEXT;
|
||||
if (usePreviewBranch) releasesUrl = OS_RELEASES_PREVIEW;
|
||||
if (useTestBranch) releasesUrl = OS_RELEASES_TEST;
|
||||
|
||||
return releasesUrl;
|
||||
});
|
||||
// actions
|
||||
@@ -246,7 +251,7 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
timestamp: Date.now(),
|
||||
response,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const cacheReleasesResponse = () => {
|
||||
localStorage.setItem(RELEASES_LOCAL_STORAGE_KEY, JSON.stringify(releases.value));
|
||||
@@ -285,7 +290,7 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
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
|
||||
// cache is expired, purge it
|
||||
console.debug('[requestReleases] cache EXPIRED');
|
||||
await purgeReleasesCache();
|
||||
} else {
|
||||
@@ -293,7 +298,7 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
console.debug('[requestReleases] cache VALID', releases.value.response);
|
||||
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 {
|
||||
@@ -343,7 +348,7 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
return console.error('[checkForUpdate] no releases found');
|
||||
}
|
||||
|
||||
Object.keys(releases.value.response ?? {}).forEach((key) => {
|
||||
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) {
|
||||
return;
|
||||
@@ -359,7 +364,7 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
return;
|
||||
}
|
||||
|
||||
branchReleases.find((release) => {
|
||||
branchReleases.find(release => {
|
||||
if (gt(release.version, osVersion.value)) {
|
||||
// before we set the available version, check if the license key updates have expired to ensure we don't show an update that the user can't install
|
||||
if (regUpdatesExpired.value && releaseDateGtRegExpDate(release.date, regExp.value)) {
|
||||
@@ -379,14 +384,14 @@ export const useUpdateOsStoreGeneric = (payload?: UpdateOsStorePayload) =>
|
||||
|
||||
const findRelease = (searchKey: keyof Release, searchValue: string): Release | null => {
|
||||
const response = releases?.value?.response;
|
||||
if (!response) { return null; }
|
||||
if (!response) return null;
|
||||
|
||||
for (const key of Object.keys(response)) {
|
||||
const branchReleases = response[key as keyof ReleasesResponse];
|
||||
if (!branchReleases || branchReleases.length === 0) { continue; }
|
||||
if (!branchReleases || branchReleases.length === 0) continue;
|
||||
|
||||
const foundRelease = branchReleases.find(release => release[searchKey] === searchValue);
|
||||
if (foundRelease) { return foundRelease; }
|
||||
if (foundRelease) return foundRelease;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '~/store/updateOs';
|
||||
|
||||
import type { ExternalUpdateOsAction } from '~/store/callback';
|
||||
import type { ServerRebootType } from '~/types/server';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
|
||||
/**
|
||||
@@ -43,7 +44,7 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
/** used when coming back from callback, this will be the release to install */
|
||||
const status = ref<'confirming' | 'checking' | 'ineligible' | 'failed' | 'ready' | 'success' | 'updating' | 'downgrading'>('ready');
|
||||
const callbackUpdateRelease = ref<Release | null>(null);
|
||||
const rebootType = ref<'thirdPartyDriversDownloading' | 'downgrade' | 'upgrade' | ''>('');
|
||||
const rebootType = computed(() => serverStore.rebootType);
|
||||
const rebootTypeText = computed(() => {
|
||||
/** translations are handled by rendering template's `t()` */
|
||||
switch (rebootType.value) {
|
||||
@@ -51,12 +52,13 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
return 'Updating 3rd party drivers';
|
||||
case 'downgrade':
|
||||
return 'Reboot Required for Downgrade';
|
||||
case 'upgrade':
|
||||
case 'update':
|
||||
return 'Reboot Required for Update';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
const rebootVersion = computed(() => serverStore.rebootVersion);
|
||||
|
||||
const ineligible = computed(() => !serverStore.guid || !serverStore.keyfile || !osVersion.value || regUpdatesExpired.value);
|
||||
const ineligibleText = computed(() => { // translated in components
|
||||
@@ -100,7 +102,7 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
},
|
||||
type: 'updateOs',
|
||||
}],
|
||||
serverStore.inIframe,
|
||||
serverStore.inIframe ? 'newTab' : '',
|
||||
);
|
||||
},
|
||||
external: !!updateOsStore.available,
|
||||
@@ -111,7 +113,7 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
};
|
||||
};
|
||||
|
||||
const executeUpdateOsCallback = async () => {
|
||||
const executeUpdateOsCallback = async (autoRedirectReplace?: boolean) => {
|
||||
await callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
@@ -120,7 +122,7 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
},
|
||||
type: 'updateOs',
|
||||
}],
|
||||
serverStore.inIframe,
|
||||
serverStore.inIframe ? 'newTab' : (autoRedirectReplace ? 'replace' : ''),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -177,8 +179,10 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
// @ts-ignore • global set in the webgui
|
||||
document.rebootNow.submit();
|
||||
};
|
||||
|
||||
const viewCurrentReleaseNotes = (modalTitle:string, webguiFilePath?:string|undefined) => { // @ts-ignore
|
||||
/**
|
||||
* By default this will display current version's release notes
|
||||
*/
|
||||
const viewReleaseNotes = (modalTitle:string, webguiFilePath?:string|undefined) => { // @ts-ignore
|
||||
if (typeof openChanges === 'function') { // @ts-ignore
|
||||
openChanges(`showchanges ${webguiFilePath ?? '/var/tmp/unRAIDServer.txt'}`, modalTitle);
|
||||
} else {
|
||||
@@ -190,10 +194,6 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
status.value = payload;
|
||||
};
|
||||
|
||||
const setRebootType = (payload: typeof rebootType.value) => {
|
||||
rebootType.value = payload;
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (status.value === 'ready' && ineligible.value) {
|
||||
setStatus('ineligible');
|
||||
@@ -209,6 +209,7 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
regUpdatesExpired,
|
||||
rebootType,
|
||||
rebootTypeText,
|
||||
rebootVersion,
|
||||
status,
|
||||
ineligible,
|
||||
ineligibleText,
|
||||
@@ -221,9 +222,8 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
executeUpdateOsCallback,
|
||||
rebootServer,
|
||||
setStatus,
|
||||
setRebootType,
|
||||
setUpdateOsAction,
|
||||
viewCurrentReleaseNotes,
|
||||
viewReleaseNotes,
|
||||
getReleaseFromKeyServer,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -31,8 +31,9 @@ export type ServerState = 'BASIC'
|
||||
| undefined;
|
||||
|
||||
export type ServerOsVersionBranch = 'stable' | 'next' | 'preview' | 'test';
|
||||
|
||||
export type ServerconnectPluginInstalled = 'dynamix.unraid.net.plg' | 'dynamix.unraid.net.staging.plg' | 'dynamix.unraid.net.plg_installFailed' | 'dynamix.unraid.net.staging.plg_installFailed' | '';
|
||||
export type ServerRebootType = 'thirdPartyDriversDownloading' | 'downgrade' | 'update' | '';
|
||||
|
||||
export interface Server {
|
||||
apiKey?: string;
|
||||
apiVersion?: string;
|
||||
@@ -58,6 +59,8 @@ export interface Server {
|
||||
name?: string;
|
||||
osVersion?: string;
|
||||
osVersionBranch?: ServerOsVersionBranch;
|
||||
rebootType?: ServerRebootType;
|
||||
rebootVersion?: string;
|
||||
registered?: boolean;
|
||||
regDev?: number;
|
||||
regGen?: number;
|
||||
|
||||
Reference in New Issue
Block a user