From bd97ace974098f49cdfa90b51a79c946e9f9a57c Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Thu, 4 Jun 2026 16:58:01 +0100 Subject: [PATCH] feat: hosting access tab (#5995) * feat: implement access tab with dummy data * fix: spacing * feat: qa * feat: implement backend * qa: qa pass * feat: fix user "search" * fix: lint * feat: change to bitfield * feat: fix fields * fix: lint * fix: lint * feat: hook up api * feat: fix permissions * feat: audit log table event start * feat: better mobile mode for audit log table * feat: i18n * feat: qa * feat: enforce permissions * feat: email template start * feat: qa * fix: tooltip bug * feat: qa * impl: sse support in api-client * feat: sse impl * fix: desync path * feat: time frame picker from analytics * feat: QA * fix: spacing * fix: permisison audit log entries * fix: hosting manage page shared server detection * fix: lint * feat: qa + lint * feat: audit log table sort by time * feat: finish frontend panel stuff * fix: lint * fix: backend alignment * fix: lint * fix: supress friend errors * feat: qa * fix: qa * fix: lint * fix: utils barrel * fix: safari cookies in dev * fix: pin nuxt * feat: fixes + notif fix * fix: notifications * feat: qa * fix: notification sync not happening immediately * fix: qa * fix: qa * feat: qa * blog + prepr * feat: toast shit * blog images * thumbnail update one last time * prepr * feat: use reinvite route * update images * fix: reinvite stuff * fix: lint * fix: alignment of save bar * fix: notif sizing * fix: split up access * fix: lint * fix: lint * fix: link --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com> --- .vscode/settings.json | 2 +- apps/app-frontend/src/App.vue | 41 +- .../src/components/ui/AppActionBar.vue | 39 + .../InstallationSettings.vue | 112 +- .../notifications/ServerInvitePopupBody.vue | 52 - apps/app-frontend/src/helpers/state.ts | 1 + .../src/pages/hosting/manage/Access.vue | 33 + .../src/pages/hosting/manage/index.js | 3 +- apps/app-frontend/src/routes.js | 8 + apps/frontend/CLAUDE.md | 1 - apps/frontend/nuxt.config.ts | 1 + apps/frontend/package.json | 2 +- .../src/components/ui/NotificationItem.vue | 638 +++++---- .../components/ui/admin/AssignNoticeModal.vue | 54 +- .../components/ui/admin/BatchCreditModal.vue | 9 +- .../src/components/ui/admin/TransferModal.vue | 46 +- .../ui/dashboard/RevenueTransaction.vue | 7 +- apps/frontend/src/composables/affiliates.ts | 3 +- apps/frontend/src/composables/auth.js | 3 +- apps/frontend/src/composables/featureFlags.ts | 21 +- .../src/composables/servers/servers-fetch.ts | 264 ---- .../src/composables/useCdnDownloadContext.ts | 11 +- apps/frontend/src/helpers/api.ts | 4 +- apps/frontend/src/helpers/archon.ts | 12 + apps/frontend/src/pages/admin/emails.vue | 8 +- .../src/pages/admin/servers/notices.vue | 72 +- .../src/pages/admin/servers/transfers.vue | 44 +- .../src/pages/dashboard/revenue/index.vue | 2 + .../src/pages/hosting/manage/[id].vue | 1 + .../src/pages/hosting/manage/[id]/access.vue | 82 ++ apps/frontend/src/plugins/cosmetics.ts | 3 +- .../src/plugins/theme/theme-settings.ts | 3 +- .../article/server-access/activity-log.webp | Bin 0 -> 117060 bytes .../article/server-access/add-user-modal.webp | Bin 0 -> 202752 bytes .../article/server-access/server-access.webp | Bin 0 -> 356838 bytes .../news/article/server-access/thumbnail.webp | Bin 0 -> 198990 bytes .../src/public/news/feed/articles.json | 7 + apps/frontend/src/public/news/feed/rss.xml | 10 +- .../api-client/src/core/abstract-client.ts | 67 +- packages/api-client/src/core/abstract-sync.ts | 167 +++ packages/api-client/src/index.ts | 19 + .../src/modules/archon/actions/v1.ts | 35 + .../api-client/src/modules/archon/index.ts | 1 + .../src/modules/archon/nodes/internal.ts | 20 + .../src/modules/archon/notices/v0.ts | 98 ++ .../src/modules/archon/server-users/v1.ts | 83 ++ .../src/modules/archon/transfers/internal.ts | 84 ++ .../api-client/src/modules/archon/types.ts | 453 ++++++- packages/api-client/src/modules/index.ts | 12 + .../src/modules/labrinth/friends/v3.ts | 47 + .../api-client/src/modules/labrinth/index.ts | 1 + .../api-client/src/modules/labrinth/types.ts | 17 + .../src/modules/labrinth/users/v3.ts | 19 + packages/api-client/src/platform/generic.ts | 42 +- packages/api-client/src/platform/nuxt.ts | 42 + .../api-client/src/platform/sync-generic.ts | 229 ++++ packages/api-client/src/platform/tauri.ts | 75 +- .../src/platform/xhr-upload-client.ts | 4 +- packages/api-client/src/types/client.ts | 7 +- packages/api-client/src/types/index.ts | 2 +- packages/api-client/src/utils/fetch.ts | 55 + packages/api-client/src/utils/sse.ts | 139 ++ .../illustrations/intercom_bubble_icon.png | Bin 0 -> 32340 bytes packages/assets/index.ts | 2 + packages/assets/styles/classes.scss | 116 +- packages/blog/articles/server-access.md | 61 + packages/blog/compiled/index.ts | 2 + .../blog/compiled/server_access.content.ts | 2 + packages/blog/compiled/server_access.ts | 12 + .../public/server-access/activity-log.webp | Bin 0 -> 117060 bytes .../public/server-access/add-user-modal.webp | Bin 0 -> 202752 bytes .../public/server-access/server-access.webp | Bin 0 -> 356838 bytes .../blog/public/server-access/thumbnail.webp | Bin 0 -> 198990 bytes packages/ui/.storybook/preview.scss | 17 + packages/ui/.storybook/preview.ts | 1 + .../ui/src/components/base/BaseTerminal.vue | 8 +- .../ui/src/components/base/ButtonStyled.vue | 17 +- packages/ui/src/components/base/Combobox.vue | 45 +- .../src/components/base/DropdownFilterBar.vue | 238 +++- .../src/components/base/FloatingActionBar.vue | 12 +- .../ui/src/components/base/JoinedButtons.vue | 6 + .../src/components/base/MultiStageModal.vue | 3 + .../ui/src/components/base/StyledInput.vue | 9 + packages/ui/src/components/base/Table.vue | 58 +- .../billing/ServersUpgradeModalWrapper.vue | 16 +- .../components/ModpackStage.vue | 20 +- .../creation-flow-context.ts | 10 + .../flows/creation-flow-modal/index.vue | 6 +- .../stages/custom-setup-stage.ts | 7 +- .../stages/final-config-stage.ts | 4 +- .../stages/import-instance-stage.ts | 3 +- packages/ui/src/components/index.ts | 1 + packages/ui/src/components/modal/NewModal.vue | 94 +- .../components/nav/PopupNotificationPanel.vue | 74 +- .../notifications/NotificationStack.vue | 61 + .../notifications/NotificationToast.vue | 273 ++++ .../ui/src/components/notifications/index.ts | 2 + .../components/servers/InstallingBanner.vue | 10 +- .../src/components/servers/ServerListing.vue | 28 +- .../components/servers/ServerManageStats.vue | 4 +- .../components/servers/ServerSetupModal.vue | 14 + .../components/servers/access/AccessTable.vue | 625 +++++++++ .../servers/access/AuditLogEventCell.vue | 13 + .../servers/access/AuditLogTable.vue | 706 ++++++++++ .../servers/access/GrantAccessModal.vue | 459 +++++++ .../servers/access/RemoveAccessModal.vue | 321 +++++ .../servers/access/events/AddonEvent.vue | 100 ++ .../servers/access/events/BackupEvent.vue | 105 ++ .../servers/access/events/BaseEvent.vue | 9 + .../access/events/BasicStringEvent.vue | 80 ++ .../servers/access/events/ConfigEvent.vue | 142 ++ .../servers/access/events/ConsoleEvent.vue | 27 + .../servers/access/events/EventEntityLink.vue | 94 ++ .../servers/access/events/EventEntityList.vue | 147 ++ .../servers/access/events/EventInlineText.vue | 23 + .../servers/access/events/FileEvent.vue | 64 + .../servers/access/events/ModpackEvent.vue | 68 + .../servers/access/events/NetworkEvent.vue | 35 + .../servers/access/events/ServerMetaEvent.vue | 124 ++ .../servers/access/events/UnknownEvent.vue | 25 + .../servers/access/events/UserAccessEvent.vue | 123 ++ .../components/servers/access/events/index.ts | 16 + .../servers/access/events/parser.ts | 609 +++++++++ .../components/servers/access/events/types.ts | 69 + .../ui/src/components/servers/access/index.ts | 7 + .../components/servers/access/permissions.ts | 23 + .../ui/src/components/servers/access/types.ts | 57 + .../servers/admonitions/BackupAdmonition.vue | 18 +- .../admonitions/FileOperationAdmonition.vue | 15 +- .../admonitions/ServerPanelAdmonitions.vue | 8 + .../servers/backups/BackupCreateModal.vue | 33 +- .../servers/backups/BackupDeleteModal.vue | 23 +- .../components/servers/backups/BackupItem.vue | 45 +- .../servers/backups/BackupRenameModal.vue | 33 +- .../servers/backups/BackupRestoreModal.vue | 32 +- .../edit-server-icon/EditServerIcon.vue | 34 +- .../components/servers/icons/LoaderIcon.vue | 5 +- packages/ui/src/components/servers/index.ts | 1 + .../servers/labels/ServerLoaderLabel.vue | 3 +- .../servers/marketing/MedalServerListing.vue | 28 +- .../server-header/PanelServerActionButton.vue | 3 + .../server-header/use-server-power-action.ts | 17 +- packages/ui/src/composables/how-ago.ts | 25 +- packages/ui/src/composables/index.ts | 1 + .../src/composables/server-backups-queue.ts | 2 +- .../composables/server-manage-core-runtime.ts | 14 +- .../ui/src/composables/server-panel-sync.ts | 345 +++++ .../ui/src/composables/server-permissions.ts | 118 ++ .../components/ConsoleActionButtons.vue | 8 +- .../ui/src/layouts/shared/console/layout.vue | 29 + .../console/providers/console-manager.ts | 3 + .../components/ContentCardItem.vue | 53 +- .../components/ContentCardTable.vue | 6 + .../components/ContentModpackCard.vue | 2 + .../components/ContentSelectionBar.vue | 18 +- .../modals/ConfirmBulkUpdateModal.vue | 19 +- .../modals/ConfirmDeletionModal.vue | 27 +- .../modals/ConfirmModpackUpdateModal.vue | 9 +- .../modals/ConfirmReinstallModal.vue | 17 + .../components/modals/ConfirmRepairModal.vue | 6 + .../components/modals/ConfirmUnlinkModal.vue | 38 +- .../components/modals/ContentInstallModal.vue | 8 +- .../components/modals/ContentUpdaterModal.vue | 10 +- .../components/modals/InlineBackupCreator.vue | 35 +- .../components/modals/ModpackContentModal.vue | 27 +- .../src/layouts/shared/content-tab/layout.vue | 41 +- .../src/layouts/shared/content-tab/types.ts | 3 + .../components/editor/EditorFindReplace.vue | 9 +- .../components/editor/FileEditor.vue | 14 +- .../modals/FileUploadZipUrlModal.vue | 30 +- .../src/layouts/shared/files-tab/layout.vue | 19 +- .../composables/use-installation-form.ts | 174 +++ .../shared/installation-settings/layout.vue | 257 +++- .../providers/installation-settings.ts | 1 + .../shared/server-settings/pages/advanced.vue | 76 +- .../shared/server-settings/pages/general.vue | 33 +- .../server-settings/pages/installation.vue | 61 +- .../shared/server-settings/pages/network.vue | 41 +- .../server-settings/pages/properties.vue | 44 +- .../hosting/manage/[id]/access/access.vue | 644 +++++++++ .../manage/[id]/access/audit-log-utils.ts | 348 +++++ .../hosting/manage/[id]/access/audit-log.ts | 496 +++++++ .../hosting/manage/[id]/access/messages.ts | 315 +++++ .../hosting/manage/[id]/onboarding.vue | 28 +- .../wrapped/hosting/manage/backups.vue | 135 +- .../wrapped/hosting/manage/content.vue | 142 +- .../layouts/wrapped/hosting/manage/files.vue | 21 +- .../layouts/wrapped/hosting/manage/index.vue | 198 ++- .../wrapped/hosting/manage/overview.vue | 15 +- .../layouts/wrapped/hosting/manage/root.vue | 42 +- packages/ui/src/layouts/wrapped/index.ts | 1 + packages/ui/src/locales/en-US/index.json | 675 ++++++++++ .../ui/src/providers/popup-notifications.ts | 27 + packages/ui/src/providers/server-context.ts | 22 +- .../ui/src/stories/base/Combobox.stories.ts | 40 + .../ui/src/stories/base/DatePicker.stories.ts | 31 + .../stories/base/DropdownFilterBar.stories.ts | 23 + packages/ui/src/stories/base/Table.stories.ts | 7 + .../NotificationToasts.stories.ts | 159 +++ .../stories/servers/AccessTable.stories.ts | 147 ++ .../stories/servers/AuditLogTable.stories.ts | 929 +++++++++++++ .../stories/servers/EditServerIcon.stories.ts | 11 +- .../servers/GrantAccessModal.stories.ts | 98 ++ .../servers/MedalServerListing.stories.ts | 10 + .../servers/RemoveAccessModal.stories.ts | 76 ++ .../stories/servers/ServerListing.stories.ts | 10 + .../servers/ServerPanelAdmonitions.stories.ts | 11 +- .../servers/audit-log-entry-examples.json | 930 +++++++++++++ packages/ui/src/utils/billing.ts | 5 +- packages/ui/src/utils/common-messages.ts | 4 + packages/ui/src/utils/loaders.ts | 5 + packages/ui/src/utils/truncate.ts | 5 +- packages/utils/index.ts | 1 - packages/utils/servers/errors/index.ts | 3 - .../servers/errors/modrinth-server-error.ts | 61 - .../errors/modrinth-servers-fetch-error.ts | 10 - .../errors/modrinth-servers-multi-error.ts | 25 - packages/utils/servers/index.ts | 2 - packages/utils/servers/types/api.ts | 19 - packages/utils/servers/types/common.ts | 18 - packages/utils/servers/types/content.ts | 13 - packages/utils/servers/types/filesystem.ts | 33 - packages/utils/servers/types/index.ts | 7 - packages/utils/servers/types/server.ts | 77 -- packages/utils/servers/types/stats.ts | 20 - packages/utils/servers/types/websocket.ts | 99 -- pnpm-lock.yaml | 1188 +++++++++-------- 227 files changed, 15578 insertions(+), 2153 deletions(-) delete mode 100644 apps/app-frontend/src/components/ui/notifications/ServerInvitePopupBody.vue create mode 100644 apps/app-frontend/src/pages/hosting/manage/Access.vue delete mode 100644 apps/frontend/src/composables/servers/servers-fetch.ts create mode 100644 apps/frontend/src/helpers/archon.ts create mode 100644 apps/frontend/src/pages/hosting/manage/[id]/access.vue create mode 100644 apps/frontend/src/public/news/article/server-access/activity-log.webp create mode 100644 apps/frontend/src/public/news/article/server-access/add-user-modal.webp create mode 100644 apps/frontend/src/public/news/article/server-access/server-access.webp create mode 100644 apps/frontend/src/public/news/article/server-access/thumbnail.webp create mode 100644 packages/api-client/src/core/abstract-sync.ts create mode 100644 packages/api-client/src/modules/archon/actions/v1.ts create mode 100644 packages/api-client/src/modules/archon/nodes/internal.ts create mode 100644 packages/api-client/src/modules/archon/notices/v0.ts create mode 100644 packages/api-client/src/modules/archon/server-users/v1.ts create mode 100644 packages/api-client/src/modules/archon/transfers/internal.ts create mode 100644 packages/api-client/src/modules/labrinth/friends/v3.ts create mode 100644 packages/api-client/src/platform/sync-generic.ts create mode 100644 packages/api-client/src/utils/fetch.ts create mode 100644 packages/api-client/src/utils/sse.ts create mode 100644 packages/assets/external/illustrations/intercom_bubble_icon.png create mode 100644 packages/blog/articles/server-access.md create mode 100644 packages/blog/compiled/server_access.content.ts create mode 100644 packages/blog/compiled/server_access.ts create mode 100644 packages/blog/public/server-access/activity-log.webp create mode 100644 packages/blog/public/server-access/add-user-modal.webp create mode 100644 packages/blog/public/server-access/server-access.webp create mode 100644 packages/blog/public/server-access/thumbnail.webp create mode 100644 packages/ui/.storybook/preview.scss create mode 100644 packages/ui/src/components/notifications/NotificationStack.vue create mode 100644 packages/ui/src/components/notifications/NotificationToast.vue create mode 100644 packages/ui/src/components/notifications/index.ts create mode 100644 packages/ui/src/components/servers/access/AccessTable.vue create mode 100644 packages/ui/src/components/servers/access/AuditLogEventCell.vue create mode 100644 packages/ui/src/components/servers/access/AuditLogTable.vue create mode 100644 packages/ui/src/components/servers/access/GrantAccessModal.vue create mode 100644 packages/ui/src/components/servers/access/RemoveAccessModal.vue create mode 100644 packages/ui/src/components/servers/access/events/AddonEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/BackupEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/BaseEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/BasicStringEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/ConfigEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/ConsoleEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/EventEntityLink.vue create mode 100644 packages/ui/src/components/servers/access/events/EventEntityList.vue create mode 100644 packages/ui/src/components/servers/access/events/EventInlineText.vue create mode 100644 packages/ui/src/components/servers/access/events/FileEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/ModpackEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/NetworkEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/ServerMetaEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/UnknownEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/UserAccessEvent.vue create mode 100644 packages/ui/src/components/servers/access/events/index.ts create mode 100644 packages/ui/src/components/servers/access/events/parser.ts create mode 100644 packages/ui/src/components/servers/access/events/types.ts create mode 100644 packages/ui/src/components/servers/access/index.ts create mode 100644 packages/ui/src/components/servers/access/permissions.ts create mode 100644 packages/ui/src/components/servers/access/types.ts create mode 100644 packages/ui/src/composables/server-panel-sync.ts create mode 100644 packages/ui/src/composables/server-permissions.ts create mode 100644 packages/ui/src/layouts/wrapped/hosting/manage/[id]/access/access.vue create mode 100644 packages/ui/src/layouts/wrapped/hosting/manage/[id]/access/audit-log-utils.ts create mode 100644 packages/ui/src/layouts/wrapped/hosting/manage/[id]/access/audit-log.ts create mode 100644 packages/ui/src/layouts/wrapped/hosting/manage/[id]/access/messages.ts create mode 100644 packages/ui/src/stories/notifications/NotificationToasts.stories.ts create mode 100644 packages/ui/src/stories/servers/AccessTable.stories.ts create mode 100644 packages/ui/src/stories/servers/AuditLogTable.stories.ts create mode 100644 packages/ui/src/stories/servers/GrantAccessModal.stories.ts create mode 100644 packages/ui/src/stories/servers/RemoveAccessModal.stories.ts create mode 100644 packages/ui/src/stories/servers/audit-log-entry-examples.json delete mode 100644 packages/utils/servers/errors/index.ts delete mode 100644 packages/utils/servers/errors/modrinth-server-error.ts delete mode 100644 packages/utils/servers/errors/modrinth-servers-fetch-error.ts delete mode 100644 packages/utils/servers/errors/modrinth-servers-multi-error.ts delete mode 100644 packages/utils/servers/index.ts delete mode 100644 packages/utils/servers/types/api.ts delete mode 100644 packages/utils/servers/types/common.ts delete mode 100644 packages/utils/servers/types/content.ts delete mode 100644 packages/utils/servers/types/filesystem.ts delete mode 100644 packages/utils/servers/types/index.ts delete mode 100644 packages/utils/servers/types/server.ts delete mode 100644 packages/utils/servers/types/stats.ts delete mode 100644 packages/utils/servers/types/websocket.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 752219881..af3d48383 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,7 +9,7 @@ "files.insertFinalNewline": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", - "source.organizeImports": "always" + "source.organizeImports": "never" }, "editor.defaultFormatter": "esbenp.prettier-vscode", "[vue]": { diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 4e31d5a82..df22421aa 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -11,7 +11,6 @@ import { import { ArrowBigUpDashIcon, ChangeSkinIcon, - CheckIcon, CompassIcon, DownloadIcon, ExternalIcon, @@ -41,7 +40,6 @@ import { defineMessages, I18nDebugPanel, LoadingBar, - ModrinthHostingLogo, NewsArticleCard, NotificationPanel, OverflowMenu, @@ -86,7 +84,6 @@ import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue' import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue' import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue' import NavButton from '@/components/ui/NavButton.vue' -import ServerInvitePopupBody from '@/components/ui/notifications/ServerInvitePopupBody.vue' import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue' import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' @@ -804,6 +801,11 @@ async function declineServerInviteNotification(notification) { } } +function openServerInviteInviterProfile(inviterName) { + if (!inviterName) return + openUrl(`${config.siteUrl}/user/${encodeURIComponent(inviterName)}`) +} + async function handleLiveNotification(notification) { if (notification?.body?.type !== 'server_invite' || notification.read) return if (displayedServerInviteNotifications.has(notification.id)) return @@ -817,30 +819,17 @@ async function handleLiveNotification(notification) { typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null addPopupNotification({ - title: 'Modrinth Hosting', - titleLogo: ModrinthHostingLogo, - bodyComponent: ServerInvitePopupBody, - bodyProps: { - inviterName: invitedBy?.username ?? null, - inviterAvatarUrl: invitedBy?.avatar_url ?? null, - serverName, - }, - type: 'info', - buttons: [ - { - label: 'Accept', - action: () => acceptServerInviteNotification(notification), - icon: CheckIcon, - color: 'brand', - }, - { - label: 'Decline', - action: () => declineServerInviteNotification(notification), - icon: XIcon, - color: 'red', - }, - ], + title: serverName, autoCloseMs: null, + toast: { + type: 'server-invite', + actorName: invitedBy?.username ?? null, + actorAvatarUrl: invitedBy?.avatar_url ?? null, + entityName: serverName, + onAccept: () => acceptServerInviteNotification(notification), + onDecline: () => declineServerInviteNotification(notification), + onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null), + }, }) } diff --git a/apps/app-frontend/src/components/ui/AppActionBar.vue b/apps/app-frontend/src/components/ui/AppActionBar.vue index cdcd3d0b1..9b18e8595 100644 --- a/apps/app-frontend/src/components/ui/AppActionBar.vue +++ b/apps/app-frontend/src/components/ui/AppActionBar.vue @@ -133,6 +133,7 @@ import { type PopupNotificationProgressItem, useVIntl, } from '@modrinth/ui' +import { convertFileSrc } from '@tauri-apps/api/core' import { Dropdown } from 'floating-vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' @@ -284,6 +285,7 @@ function goToTerminal(path?: string) { } const currentLoadingBars = ref([]) +const currentLoadingBarIconUrls = ref>({}) const notificationId = ref(null) const dismissed = ref(false) @@ -303,6 +305,16 @@ function getLoadingText(loadingBar: LoadingBar): string { return loadingBar.message ? `${percent}% ${loadingBar.message}` : `${percent}%` } +function getDisplayIconUrl(icon: string | null | undefined): string | null { + if (!icon) { + return null + } + if (/^(https?:|data:|blob:|asset:|tauri:)/.test(icon)) { + return icon + } + return convertFileSrc(icon) +} + function getNotification(): PopupNotification | null { if (!notificationId.value) { return null @@ -326,6 +338,7 @@ function buildDownloadItems(): PopupNotificationProgressItem[] { id: getLoadingBarKey(bar), title: bar.title ?? '', text: getLoadingText(bar), + iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null, progress: getLoadingProgress(bar), waiting: !bar.total || bar.total <= 0, })) @@ -400,6 +413,32 @@ async function refreshLoadingBars() { .map(formatLoadingBars) .filter((bar) => bar?.bar_type?.type !== 'launcher_update') + const profilePaths = Array.from( + new Set( + currentLoadingBars.value + .map((bar) => bar.bar_type?.profile_path) + .filter((path): path is string => !!path), + ), + ) + const profiles = profilePaths.length + ? await getInstances(profilePaths).catch((error) => { + handleError(error) + return [] + }) + : [] + const profileIconUrls = new Map( + profiles.map((profile) => [profile.path, getDisplayIconUrl(profile.icon_path)]), + ) + currentLoadingBarIconUrls.value = Object.fromEntries( + currentLoadingBars.value.map((bar) => { + const barIconUrl = getDisplayIconUrl(bar.bar_type?.icon) + const profileIconUrl = bar.bar_type?.profile_path + ? profileIconUrls.get(bar.bar_type.profile_path) + : null + return [getLoadingBarKey(bar), barIconUrl ?? profileIconUrl ?? null] + }), + ) + currentLoadingBars.value.sort((a, b) => { const aKey = `${a.loading_bar_uuid ?? a.id ?? ''}` const bKey = `${b.loading_bar_uuid ?? b.id ?? ''}` diff --git a/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue index 4b1e5af37..a9edf12ef 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue +++ b/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue @@ -8,6 +8,7 @@ import { InstallationSettingsLayout, provideAppBackup, provideInstallationSettings, + useDebugLogger, useVIntl, } from '@modrinth/ui' import type { GameVersionTag, PlatformTag } from '@modrinth/utils' @@ -34,9 +35,17 @@ import type { Manifest } from '../../../helpers/types' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() const queryClient = useQueryClient() +const debug = useDebugLogger('AppInstallationSettings') const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings() +debug('metadata load: start', { + instancePath: instance.value.path, + loader: instance.value.loader, + gameVersion: instance.value.game_version, + installStage: instance.value.install_stage, +}) + const [ fabric_versions, forge_versions, @@ -72,6 +81,15 @@ const [ .catch(handleError), ]) +debug('metadata load: done', { + hasFabricManifest: !!fabric_versions?.value, + hasForgeManifest: !!forge_versions?.value, + hasQuiltManifest: !!quilt_versions?.value, + hasNeoforgeManifest: !!neoforge_versions?.value, + gameVersions: all_game_versions?.value?.length ?? 0, + availablePlatforms: loaders?.value?.map((loader) => loader.name) ?? [], +}) + const { data: modpackInfo } = useQuery({ queryKey: computed(() => ['linkedModpackInfo', instance.value.path]), queryFn: () => get_linked_modpack_info(instance.value.path, 'must_revalidate'), @@ -95,11 +113,21 @@ function getManifest(loader: string) { quilt: quilt_versions, neoforge: neoforge_versions, } - return map[loader] + const manifest = map[loader] + debug('getManifest:', { + loader, + hasManifest: !!manifest?.value, + gameVersions: manifest?.value?.gameVersions?.length ?? 0, + }) + return manifest } provideAppBackup({ async createBackup() { + debug('createBackup: start', { + instancePath: instance.value.path, + instanceName: instance.value.name, + }) const allProfiles = await list() const prefix = `${instance.value.name} - Backup #` const existingNums = allProfiles @@ -109,6 +137,7 @@ provideAppBackup({ const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1 const newPath = await duplicate(instance.value.path) await edit(newPath, { name: `${prefix}${nextNum}` }) + debug('createBackup: done', { newPath, backupName: `${prefix}${nextNum}` }) }, }) @@ -165,32 +194,72 @@ provideInstallationSettings({ const manifest = getManifest(loader) return !!manifest?.value?.gameVersions?.some((x) => item.version === x.id) }) - return (showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')).map( - (x) => ({ value: x.version, label: x.version }), - ) + const result = ( + showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release') + ).map((x) => ({ value: x.version, label: x.version })) + debug('resolveGameVersions:', { + loader, + showSnapshots, + totalVersions: versions.length, + filteredVersions: filtered.length, + resultVersions: result.length, + }) + return result }, resolveLoaderVersions(loader, gameVersion) { - if (loader === 'vanilla' || !gameVersion) return [] - const manifest = getManifest(loader) - if (!manifest?.value) return [] - if (loader === 'fabric' || loader === 'quilt') { - return manifest.value.gameVersions[0]?.loaders ?? [] + if (loader === 'vanilla' || !gameVersion) { + debug('resolveLoaderVersions: skipped', { loader, gameVersion }) + return [] } - return manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? [] + const manifest = getManifest(loader) + if (!manifest?.value) { + debug('resolveLoaderVersions: no manifest', { loader, gameVersion }) + return [] + } + if (loader === 'fabric' || loader === 'quilt') { + const result = manifest.value.gameVersions[0]?.loaders ?? [] + debug('resolveLoaderVersions: fabric/quilt result', { + loader, + gameVersion, + count: result.length, + }) + return result + } + const result = + manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? [] + debug('resolveLoaderVersions: result', { loader, gameVersion, count: result.length }) + return result }, resolveHasSnapshots(loader) { const versions = all_game_versions?.value ?? [] - if (loader === 'vanilla') return versions.some((x) => x.version_type !== 'release') + if (loader === 'vanilla') { + const result = versions.some((x) => x.version_type !== 'release') + debug('resolveHasSnapshots: vanilla', { loader, result }) + return result + } const manifest = getManifest(loader) const supported = versions.filter( (item) => !!manifest?.value?.gameVersions?.some((x) => item.version === x.id), ) - return supported.some((x) => x.version_type !== 'release') + const result = supported.some((x) => x.version_type !== 'release') + debug('resolveHasSnapshots:', { + loader, + totalVersions: versions.length, + supportedVersions: supported.length, + result, + }) + return result }, async save(platform, gameVersion, loaderVersionId) { + debug('save: called', { + instancePath: instance.value.path, + platform, + gameVersion, + loaderVersionId, + }) const editProfile: Record = { loader: platform, game_version: gameVersion, @@ -199,17 +268,21 @@ provideInstallationSettings({ editProfile.loader_version = loaderVersionId } await edit(instance.value.path, editProfile).catch(handleError) + debug('save: edit complete', { editProfile }) }, afterSave: async () => { + debug('afterSave: installing', { instancePath: instance.value.path }) await install(instance.value.path, false).catch(handleError) trackEvent('InstanceRepair', { loader: instance.value.loader, game_version: instance.value.game_version, }) + debug('afterSave: done') }, async repair() { + debug('repair: called', { instancePath: instance.value.path }) repairing.value = true await install(instance.value.path, true).catch(handleError) repairing.value = false @@ -217,9 +290,11 @@ provideInstallationSettings({ loader: instance.value.loader, game_version: instance.value.game_version, }) + debug('repair: done') }, async reinstallModpack() { + debug('reinstallModpack: called', { instancePath: instance.value.path }) reinstalling.value = true await update_repair_modrinth(instance.value.path).catch(handleError) reinstalling.value = false @@ -227,9 +302,11 @@ provideInstallationSettings({ loader: instance.value.loader, game_version: instance.value.game_version, }) + debug('reinstallModpack: done') }, async unlinkModpack() { + debug('unlinkModpack: called', { instancePath: instance.value.path }) await edit(instance.value.path, { linked_data: null as unknown as undefined, }) @@ -237,27 +314,38 @@ provideInstallationSettings({ queryKey: ['linkedModpackInfo', instance.value.path], }) onUnlinked() + debug('unlinkModpack: done') }, getCachedModpackVersions: () => null, async fetchModpackVersions() { + debug('fetchModpackVersions: called', { + projectId: instance.value.linked_data?.project_id, + }) const versions = await get_project_versions(instance.value.linked_data!.project_id!).catch( handleError, ) + debug('fetchModpackVersions: done', { count: versions?.length ?? 0 }) return (versions ?? []) as Labrinth.Versions.v2.Version[] }, async getVersionChangelog(versionId: string) { + debug('getVersionChangelog: called', { versionId }) return (await get_version(versionId, 'must_revalidate').catch( () => null, )) as Labrinth.Versions.v2.Version | null }, async onModpackVersionConfirm(version) { + debug('onModpackVersionConfirm: called', { + versionId: version.id, + instancePath: instance.value.path, + }) await update_managed_modrinth_version(instance.value.path, version.id) await queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instance.value.path], }) + debug('onModpackVersionConfirm: done') }, updaterModalProps: computed(() => ({ diff --git a/apps/app-frontend/src/components/ui/notifications/ServerInvitePopupBody.vue b/apps/app-frontend/src/components/ui/notifications/ServerInvitePopupBody.vue deleted file mode 100644 index 2899a0bdb..000000000 --- a/apps/app-frontend/src/components/ui/notifications/ServerInvitePopupBody.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/apps/app-frontend/src/helpers/state.ts b/apps/app-frontend/src/helpers/state.ts index 8e9bb13f5..516d657cf 100644 --- a/apps/app-frontend/src/helpers/state.ts +++ b/apps/app-frontend/src/helpers/state.ts @@ -10,6 +10,7 @@ export interface LoadingBarType { version?: string profile_path?: string pack_name?: string + icon?: string | null } export interface LoadingBar { diff --git a/apps/app-frontend/src/pages/hosting/manage/Access.vue b/apps/app-frontend/src/pages/hosting/manage/Access.vue new file mode 100644 index 000000000..59a3b82e3 --- /dev/null +++ b/apps/app-frontend/src/pages/hosting/manage/Access.vue @@ -0,0 +1,33 @@ + + + diff --git a/apps/app-frontend/src/pages/hosting/manage/index.js b/apps/app-frontend/src/pages/hosting/manage/index.js index 50052e3f9..0c10b0713 100644 --- a/apps/app-frontend/src/pages/hosting/manage/index.js +++ b/apps/app-frontend/src/pages/hosting/manage/index.js @@ -1,7 +1,8 @@ +import Access from './Access.vue' import Backups from './Backups.vue' import Content from './Content.vue' import Files from './Files.vue' import Index from './Index.vue' import Overview from './Overview.vue' -export { Backups, Content, Files, Index, Overview } +export { Access, Backups, Content, Files, Index, Overview } diff --git a/apps/app-frontend/src/routes.js b/apps/app-frontend/src/routes.js index f01df0670..da93b48e8 100644 --- a/apps/app-frontend/src/routes.js +++ b/apps/app-frontend/src/routes.js @@ -73,6 +73,14 @@ export default new createRouter({ breadcrumb: [{ name: '?Server' }], }, }, + { + path: 'access', + name: 'ServerManageAccess', + component: Hosting.Access, + meta: { + breadcrumb: [{ name: '?Server' }], + }, + }, ], }, { diff --git a/apps/frontend/CLAUDE.md b/apps/frontend/CLAUDE.md index 9d0d05c4d..03515cbb3 100644 --- a/apps/frontend/CLAUDE.md +++ b/apps/frontend/CLAUDE.md @@ -40,4 +40,3 @@ These composables are deprecated and should not be used in new code: - **`useAsyncData`** - we use tanstack, not nuxt's built in async data utility. - **`useBaseFetch`** (`src/composables/fetch.js`) — legacy Labrinth fetch wrapper. Use `client.labrinth.*` modules instead. -- **`useServersFetch`** (`src/composables/servers/servers-fetch.ts`) — legacy Archon fetch wrapper with manual retry/circuit-breaker. Use `client.archon.*` modules instead — refer to the `packages/api-client/CLAUDE.md` for more information. diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts index 45f1526eb..f7f8a5296 100644 --- a/apps/frontend/nuxt.config.ts +++ b/apps/frontend/nuxt.config.ts @@ -224,6 +224,7 @@ export default defineNuxtConfig({ globalThis.INTERCOM_APP_ID || 'ykeritl9', production: isProduction(), + cookieSecure: isProduction(), buildEnv: process.env.BUILD_ENV, preview: process.env.PREVIEW === 'true', featureFlagOverrides: getFeatureFlagOverrides(), diff --git a/apps/frontend/package.json b/apps/frontend/package.json index e835e9cfc..fa2e751f4 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -26,7 +26,7 @@ "@types/semver": "^7.7.1", "autoprefixer": "^10.4.19", "glob": "^10.2.7", - "nuxt": "^3.20.2", + "nuxt": "=3.20.2", "postcss": "^8.4.39", "prettier-plugin-tailwindcss": "^0.6.5", "sass": "^1.58.0", diff --git a/apps/frontend/src/components/ui/NotificationItem.vue b/apps/frontend/src/components/ui/NotificationItem.vue index 420e85242..1bbac8bf3 100644 --- a/apps/frontend/src/components/ui/NotificationItem.vue +++ b/apps/frontend/src/components/ui/NotificationItem.vue @@ -1,244 +1,284 @@