feat: show warning for broken mrpack in moderation (#7136)

* feat: show warning if modpack has a version during time when modpack export broke

* fix type

* fix: dont lazy load versions v3 for staff and convert times to utc

* pnpm prepr
This commit is contained in:
Truman Gao
2026-08-14 15:42:01 +00:00
committed by GitHub
parent 74c672df49
commit 4dbea80e65
3 changed files with 183 additions and 4 deletions
@@ -400,10 +400,10 @@ defineExpose({ show, hide })
</Combobox>
<IconButton
v-tooltip="formatMessage(messages.deleteAllGroups)"
type="quiet"
color="red"
type="base"
:label="formatMessage(messages.deleteAllGroups)"
:disabled="titleButtonsDisabled"
class="[&:not(:disabled):focus-visible>svg]:!text-red [&:not(:disabled):hover>svg]:!text-red"
@click="showConfirmClearGroups"
>
<TrashIcon v-if="!isClearing" aria-hidden="true" />
@@ -3458,6 +3458,12 @@
"project.actions.back-to-project-page": {
"message": "Back to project page"
},
"project.actions.check-modpack-archives": {
"message": "Check modpack unzip"
},
"project.actions.checking-modpack-archives": {
"message": "Checking..."
},
"project.actions.create-server": {
"message": "Create a server"
},
@@ -3743,9 +3749,24 @@
"project.moderation.title": {
"message": "Moderation"
},
"project.modpack-archive-warning.description": {
"message": "Importing this .mrpack might be broken."
},
"project.modpack-archive-warning.title": {
"message": "This modpack was published during export bug"
},
"project.navigation.changelog": {
"message": "Changelog"
},
"project.notification.check-modpack-archives.failed": {
"message": "Some modpack files could not be unzipped"
},
"project.notification.check-modpack-archives.no-files": {
"message": "No .mrpack files were found for this project."
},
"project.notification.check-modpack-archives.success": {
"message": "{count, plural, one {The modpack file can be unzipped.} other {All # modpack files can be unzipped.}}"
},
"project.notification.icon-updated.message": {
"message": "Your project's icon has been updated."
},
+160 -2
View File
@@ -405,6 +405,36 @@
:auth="auth"
:tags="tags"
/>
<Admonition
v-if="
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
project.actualProjectType === 'modpack' &&
hasModpackArchiveInWarningWindow
"
type="warning"
:header="formatMessage(messages.modpackArchiveWarningTitle)"
class="mt-3"
>
{{ formatMessage(messages.modpackArchiveWarningDescription) }}
<template #actions>
<Button
type="colored"
color="orange"
:loading="isCheckingModpackArchives"
@click="checkModpackArchives"
>
<FileArchiveIcon />
{{
formatMessage(
isCheckingModpackArchives
? messages.checkingModpackArchives
: messages.checkModpackArchives,
)
}}
</Button>
</template>
</Admonition>
<Admonition
v-if="
currentMember &&
@@ -529,6 +559,7 @@ import {
ClipboardCopyIcon,
CompassIcon,
DownloadIcon,
FileArchiveIcon,
FolderSearchIcon,
HeartIcon,
LeftArrowIcon,
@@ -663,6 +694,7 @@ const downloadModal = ref()
const openInAppModal = ref()
const overTheTopDownloadAnimation = ref()
const scanModal = ref()
const isCheckingModpackArchives = ref(false)
const projectV3Loaded = computed(() => !projectV3Pending.value || projectV3.value != null)
const isServerProject = computed(() => projectV3.value?.minecraft_server != null)
@@ -818,6 +850,35 @@ const messages = defineMessages({
id: 'project.actions.rescan-modpack',
defaultMessage: 'Rescan modpack',
},
checkModpackArchives: {
id: 'project.actions.check-modpack-archives',
defaultMessage: 'Check modpack unzip',
},
checkingModpackArchives: {
id: 'project.actions.checking-modpack-archives',
defaultMessage: 'Checking...',
},
checkModpackArchivesSuccess: {
id: 'project.notification.check-modpack-archives.success',
defaultMessage:
'{count, plural, one {The modpack file can be unzipped.} other {All # modpack files can be unzipped.}}',
},
checkModpackArchivesFailed: {
id: 'project.notification.check-modpack-archives.failed',
defaultMessage: 'Some modpack files could not be unzipped',
},
checkModpackArchivesNoFiles: {
id: 'project.notification.check-modpack-archives.no-files',
defaultMessage: 'No .mrpack files were found for this project.',
},
modpackArchiveWarningTitle: {
id: 'project.modpack-archive-warning.title',
defaultMessage: 'This modpack was published during export bug',
},
modpackArchiveWarningDescription: {
id: 'project.modpack-archive-warning.description',
defaultMessage: 'Importing this .mrpack might be broken.',
},
serversPromoDescription: {
id: 'project.actions.servers-promo.description',
defaultMessage: 'Modrinth Hosting is the easiest way to play with your friends without hassle!',
@@ -1135,7 +1196,7 @@ const {
const dependencies = computed(() => dependenciesRaw.value ?? null)
// V3 Versions - lazy loaded client-side only
// V3 Versions - lazy loaded client-side only (except for staff, who need v3 versions for moderation)
const versionsEnabled = ref(false)
const {
data: versionsV3,
@@ -1149,7 +1210,7 @@ const {
apiVersion: 3,
}),
staleTime: STALE_TIME_LONG,
enabled: computed(() => !!projectId.value && versionsEnabled.value),
enabled: computed(() => !!projectId.value && (versionsEnabled.value || isStaff(auth.value.user))),
})
// Organization
@@ -1749,6 +1810,90 @@ const showProjectHeaderCreateServerAction = computed(
const projectHeaderCreateServerTo = computed(() =>
project.value ? `/hosting?project=${project.value.id}#plan` : '/hosting',
)
const MRPACK_ARCHIVE_WARNING_START = new Date('2026-08-10T17:00:00.000Z').getTime()
const MRPACK_ARCHIVE_WARNING_END = new Date('2026-08-13T20:00:00.000Z').getTime()
const hasModpackArchiveInWarningWindow = computed(() =>
(versionsV3.value ?? []).some((version) => {
const publishedAt = new Date(version.date_published).getTime()
return (
version.files.some((file) => file.filename.toLowerCase().endsWith('.mrpack')) &&
publishedAt >= MRPACK_ARCHIVE_WARNING_START &&
publishedAt <= MRPACK_ARCHIVE_WARNING_END
)
}),
)
async function checkModpackArchives() {
if (!project.value || isCheckingModpackArchives.value) return
isCheckingModpackArchives.value = true
startLoading()
try {
const versions = await client.labrinth.versions_v2.getProjectVersions(project.value.id)
const filesByUrl = new Map(
versions
.flatMap((version) => version.files)
.filter((file) => file.filename.toLowerCase().endsWith('.mrpack'))
.map((file) => [file.url, file]),
)
const files = [...filesByUrl.values()]
if (files.length === 0) {
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: formatMessage(messages.checkModpackArchivesNoFiles),
type: 'error',
})
return
}
const { default: JSZip } = await import('jszip')
const failures = []
for (const file of files) {
try {
const response = await fetch(file.url)
if (!response.ok) {
throw new Error(`Download failed (${response.status} ${response.statusText})`)
}
await JSZip.loadAsync(await response.blob(), { checkCRC32: true })
} catch (error) {
failures.push({
filename: file.filename,
error: error?.message ?? String(error),
})
}
}
if (failures.length > 0) {
addNotification({
title: formatMessage(messages.checkModpackArchivesFailed),
text: failures.map((failure) => `${failure.filename}: ${failure.error}`).join('\n'),
type: 'error',
})
return
}
addNotification({
title: formatMessage(commonMessages.successLabel),
text: formatMessage(messages.checkModpackArchivesSuccess, { count: files.length }),
type: 'success',
})
} catch (error) {
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: error?.data?.description ?? error?.message ?? String(error),
type: 'error',
})
} finally {
isCheckingModpackArchives.value = false
stopLoading()
}
}
const projectHeaderMoreActions = computed(() => {
const isStaff = !!(auth.value.user && tags.value.staffRoles.includes(auth.value.user.role))
@@ -1787,6 +1932,19 @@ const projectHeaderMoreActions = computed(() => {
tone: 'orange',
shown: !!auth.value.user && isStaff && project.value?.actualProjectType === 'modpack',
},
{
id: 'moderation-modpack-check-archives',
label: formatMessage(
isCheckingModpackArchives.value
? messages.checkingModpackArchives
: messages.checkModpackArchives,
),
icon: FileArchiveIcon,
action: checkModpackArchives,
tone: 'orange',
disabled: isCheckingModpackArchives.value,
shown: !!auth.value.user && isStaff && project.value?.actualProjectType === 'modpack',
},
{ type: 'divider', shown: !!auth.value.user && isStaff },
{
id: 'report',