feat: server management in app (#5628)

* start new server settings tabs

* update properties tab to match design

* better stying in general tab

* feat: add suffix input for hostname field

* implement tables for allocations and DNS records

* add tags for dns record type

* small gap adjustment

* polish advanced page

* adjust properties page hierarchy

* fix searching properties, empty state and projection radius appearing

* pnpm prepr

* update copy to match designs

* fix suffix input component

* style fixes and match heading size

* small fix

* fix search allocations placeholder

* adjust table styles

* move all installation settings helper text to below input

* update icon to use overflow menu buttons

* fix modal to be consistent

* open advanced properties when search

* remove other and custom properties, and update styles

* remove hide/show all java versions

* handle mc 26

* refactor: move server settings pages into /ui and add app ServerSettingsModal

* hook up server pages for app

* add server page header to app

* hook up server settings modal

* use large size

* fix card box shadow style

* fix hostname input for app

* fix app/website card containers

* implement external tabs for billing and admin billing

* fix save banner fixed to parent instead of page body

* remove unused prop to FriendsList causing warning in app

* fix client-only not available for app

* fix bottom cut off

* wire node auth

* implement full copy buttons

* dedup copy button tailwind styles

* fix hover class not working in @apply

* fix spacing

* fix error validation styles

* apply consistent styles and spacing

* feat: update hosting server card (#5609)

* fix type errors

* fix some stylesheets not imported for storybook

* add server listing stories

* add fix for frontend stylesheet imports

* remove props.

* convert copy code to use tailwind

* update server listing component styles

* update server info label styles

* start status/player count info label, more style updates and fixes

* add new server card buttons

* hook up server cards and implement updated styles

* hook up on download button

* fix tauri throwing error when api returns 204 No Content

* hook up purchase server modal in app

* fix upgrading state loading icon

* pnpm prepr

* filter out servers past 30 days after cancellation

* do not apply opacity on lock or spiner icons

* fix disabled server icon background

* update pending change stage

* handle known suspension states

* refactor: reduce code duplication for server listing

* update disabled state text color

* fix loading icon color

* clean up copy

* fix disabled opacity for server card

* update server listing files kept to be countdown

* implement resubscribe modal

* implement proper provisioning state for resubscribe

* fix duplicate attribute and pnpm prepr

* feat: add shared UI package auth DI

* feat: update purchase server flow (#5714)

* implement server list empty state component

* fix stories and adjust spacing

* implement select plan design refresh

* implement auth for empty server list

* use refs instead of reactive

* pnpm prepr

* fix auth usage for empty servers list

* move app auth provider setup to src/providers/setup

* pnpm prepr

* fix max height

* style fix

* fix getCreds no auth is blocking api client

* implement servers guest plan modal and signin which redirects back to modal's next step

* refactor guest plan select logic into provider

* implement sign in or create account popup

* remove force empty serverList

* add download button for suspended mod and generic

* add handling for when user logs out

* QA pass style fixes

* more consistent page styles

* fix duplicate export

* refactor: remove all fallback stuff from resubscribe modal

* implement shared download latest backup util

* i18n pass

* pnpm prepr

* fix region being selected if ping failed

* pnpm prepr

* feat: servers in app finalization (#5744)

* feat: start on shared console implementation into logs and overview pages

* fix: terminal gap issues

* feat: swap word wrap for full screen

* fix: stats cards alignment

* fix: stats

* feat: fix console clear + remove copy

* fix: lint

* fix: use reset not clear

* feat: shared server header & overview page for app and website (#5736)

* feat: implement shared server header for app and website

* feat: implement wrapped overview page with shared composable and hook it up

* pnpm prepr

* fix: bugs

* qa: cleanup

* feat: root.vue shared layout

* feat: delete old options pages + fix discovery frontend

* fix: discovery

* fix: misc style/layout issues

* fix page padding

* fix: modal height jankiness

* feat: implement server install content in app and server setup modal with DI

* fix: spacing

* remove servers in app feature flag

* Revert "remove servers in app feature flag"

This reverts commit 86e284c4bd.

* fix: qa

* feat: remove legacy components from apps/frontend/src/components/ui/servers

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>

* qa pass (#5738)

* fix: qa

* feat: qa

* fix: server icon fetch fails due to global node auth race condition overriding each other

* fix: lint

* fix: server icon upload/sync and centralize logic

* fix: server settings modal not closing for server reset

* fix: better server sorting

* feat: copy address in server listing card

* fix: notification panel in modal and when overlapping with action bar

* fix: empty server list empty state flashing when refresh, fixed by adding isReady auth flag

* feat: use floating action bar for save banner

* fix: saving state in save bar

* fix: edit server icon styling

* fix: confirm modal to have consistent buttons

* feat: loading animation for server panel + caching improvements for app

* pnpm prepr

* feat: search page deduplication (#5754)

* fix: action bar behind modal

* fix: remove warning modal for stopping

* fix: server cards states

* we hate webkit we hate webkit

* fix: update allocation creation to not use modal

* fix: properties tab spacing and styles

* feat: add files tab copy

* fix: advanced properties icon

* fix: remove back to all servers link

* feat: add files tab link in copy

* fix: server header styles to be consistent with instance

* fix: add header icons back

* feat: update instance settings icon to be consistent

* fix: icon container

* feat: upload state persistence across tabs

* fix: server labels text wrapping

* fix: use surface-5 border

* fix: loading spinner showing with onboarding below

* feat: new server button shows purchase modal in website

* fix: billing page not showing quarterly interval

* fix: server downgrade not showing updated subscription notification

* fix: server settings invalidate saved state and remove server context provider since its already provided in the page

* pnpm prepr

* add stripe publishable key to app build

* feat: console highlighting

* fix: rename servers title to modrinth hosting

* feat: search fix

* fix: qa/styles

* fix: ip click active and remove power dont ask again

* fix: qa

* feat: highlighting fix console

* fix: disable conflicts action

* fix: error dismiss bug

* feat: modal clarification

* fix: files perms issue

* fix: lint

* feat: modal fix

* enable show uptime

* fix: add loading state to edit server icon

* fix: notification panel take in has sidebar from settings

* fix: consistency pass on app settings

* fix: consistency pass on instance settings

* pnpm prepr

* fix: nagivate to billing button in app to go to website

* fix: stripe return url in app causing app to open modrinth.com in tauri

* refactor: better show polling UI code

* fix: new server polling comparison to use server ids instead of length

* fix: buttonstyled story

* fix: button styling

* fix: content.vue regression

* feat: project url redirects

* fix: breadcrumbs

* fix: purchase with newly added card

* fix: console ordering problems

* fix: app-frontend missing env config and staging environment

* fix: log syncing for instances and server panel accidentally

* fix: QA issues

* fix: server page loading state

* fix: stats card logic

* fix: lint

* fix: qa

* fix: console height padding

* fix: terminal padding + loading indicator

* feat: update medal server listing styling

* fix: no upgrade button for medal server listing in app

* fix: go to overview instead of content tab after onboarding

* fix: qa

* fix: teleport modals to body

* fix: logs tab + qa

* fix: local storage for user preferences

* fix: qa loading indic

* feat: considitonal debug and trace

* fix: jump to top on install bug

* feat: swap out server hard drive icon to server stack icon

* feat: servers in app feature flag default true

* fix: highlight row ufll

* fix: webkit thing onto a tag

* fix: input field

* fix: clear fix

* fix: lint

* fix: fmt

* feat: improve share modal and bring it back for sharing log

* pnpm prepr

* fix: menu overflowing

* feat: remove servers in app feature flag

* fix: server stat charts no longer showing color

* fix: library nav no primary state

* fix: better modal height and width

* fix: highlighting bugs

* fix: empty states

* fix: delay import to fix overview page slow load on MacOS

* fix: medal server listing too bright on light mode

* fix: admon analysis + fix logs

* fix: bug

* fix: clear purchase intent from sign-in after closing modal

* performance: improve server manage stats loading by splitting reactivity

* fix: deploy + admon + disable highlighting

* fix: clippy

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>

* feat: temp wrangler

* fix: lint

* fix: logs upload

* fix: console empty state and admon regressions

* fix: fields

* feat: log deleting + prefetch for Logs.vue

* feat: move delete before share

* feat: clear endpoint

* feat: we ball!

---------

Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
Truman Gao
2026-04-12 21:38:08 +00:00
committed by GitHub
co-authored by Calum H. tdgao Calum H.
parent a2a97d1313
commit 693a371d61
278 changed files with 15973 additions and 12607 deletions
@@ -1,5 +1,6 @@
<template>
<ServersUpgradeModalWrapper ref="upgradeModal" />
<ResubscribeModal ref="pyroResubscribeModal" @resubscribe="handlePyroResubscribeConfirm" />
<section class="universal-card experimental-styles-within">
<h2>{{ formatMessage(messages.subscriptionTitle) }}</h2>
<p>{{ formatMessage(messages.subscriptionDescription) }}</p>
@@ -284,6 +285,8 @@
v-if="subscription.serverInfo"
v-bind="subscription.serverInfo"
:pending-change="getPendingChange(subscription)"
:cancellation-date="getCancellationDate(subscription)"
:on-download-backup="getBackupDownloadForServer(subscription.serverInfo)"
/>
<div v-else class="w-fit">
<p>
@@ -514,15 +517,9 @@
"
color="green"
>
<button
@click="
resubscribePyro(
subscription.id,
$dayjs(getPyroCharge(subscription).due).isBefore($dayjs()),
)
"
>
{{ formatMessage(messages.resubscribe) }} <RightArrowIcon />
<button @click="openPyroResubscribeModal(subscription)">
{{ formatMessage(messages.resubscribe) }}
<RightArrowIcon />
</button>
</ButtonStyled>
</div>
@@ -709,21 +706,25 @@ import {
OverflowMenu,
paymentMethodMessages,
PurchaseModal,
ResubscribeModal,
ServerListing,
useFormatDateTime,
useFormatPrice,
useServerBackupDownload,
useVIntl,
} from '@modrinth/ui'
import { calculateSavings, getCurrency } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { useIntervalFn } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue'
import ModrinthServersIcon from '~/components/brand/ModrinthServersIcon.vue'
import ServersUpgradeModalWrapper from '~/components/ui/servers/ServersUpgradeModalWrapper.vue'
import { products } from '~/generated/state.json'
const { addNotification, handleError } = injectNotificationManager()
const client = injectModrinthClient()
const { getLatestBackupDownload } = useServerBackupDownload()
definePageMeta({
middleware: 'auth',
})
@@ -835,6 +836,14 @@ const messages = defineMessages({
id: 'settings.billing.interval.year',
defaultMessage: 'year',
},
intervalQuarter: {
id: 'settings.billing.interval.quarter',
defaultMessage: 'quarter',
},
intervalQuarterly: {
id: 'settings.billing.interval.quarterly.adjective',
defaultMessage: 'quarterly',
},
intervalMonthly: {
id: 'settings.billing.interval.monthly',
defaultMessage: 'monthly',
@@ -998,7 +1007,7 @@ const messages = defineMessages({
pyroResubscribeRequestSubmittedText: {
id: 'settings.billing.pyro.resubscribe.request-submitted.text',
defaultMessage:
'If the server is currently suspended, it may take up to 10 minutes for another charge attempt to be made.',
'If the server is currently cancelled, it may take 10-15 minutes to set up the server.',
},
pyroResubscribeSuccessText: {
id: 'settings.billing.pyro.resubscribe.success.text',
@@ -1015,15 +1024,20 @@ const messages = defineMessages({
})
function getIntervalNounLabel(interval) {
console.log(interval)
return interval === 'yearly'
? formatMessage(messages.intervalYear)
: formatMessage(messages.intervalMonth)
: interval === 'quarterly'
? formatMessage(messages.intervalQuarter)
: formatMessage(messages.intervalMonth)
}
function getIntervalAdjectiveLabel(interval) {
return interval === 'yearly'
? formatMessage(messages.intervalYearly)
: formatMessage(messages.intervalMonthly)
: interval === 'quarterly'
? formatMessage(messages.intervalQuarterly)
: formatMessage(messages.intervalMonthly)
}
const queryClient = useQueryClient()
@@ -1053,6 +1067,11 @@ const { data: serversData } = useQuery({
queryFn: () => client.archon.servers_v0.list(),
})
const { data: serverFullList } = useQuery({
queryKey: ['servers', 'v1'],
queryFn: () => client.archon.servers_v1.list(),
})
const midasProduct = ref(products.find((x) => x.metadata?.type === 'midas'))
const midasSubscription = computed(() =>
subscriptions.value?.find(
@@ -1082,16 +1101,38 @@ const pyroSubscriptions = computed(() => {
const pyroSubs = subscriptions.value?.filter((s) => s?.metadata?.type === 'pyro') || []
const servers = serversData.value?.servers || []
return pyroSubs.map((subscription) => {
const server = servers.find((s) => s.server_id === subscription.metadata.id)
return {
...subscription,
serverInfo: server,
}
})
return pyroSubs
.map((subscription) => {
const server = servers.find((s) => s.server_id === subscription.metadata.id)
const charge = getPyroCharge(subscription)
return {
...subscription,
serverInfo: {
...server,
isProvisioning:
subscription.status === 'unprovisioned' &&
(charge?.status === 'processing' || charge?.status === 'open'),
},
}
})
.filter((subscription) => {
// files expire 30 days after cancellation
const cancellationDate = getCancellationDate(subscription)
if (
!cancellationDate ||
subscription.serverInfo?.status !== 'suspended' ||
subscription.serverInfo?.suspension_reason !== 'cancelled'
)
return true
const cancellation = new Date(cancellationDate)
const thirtyDaysLater = new Date(cancellation.getTime() + 30 * 24 * 60 * 60 * 1000)
return new Date() <= thirtyDaysLater
})
})
const midasPurchaseModal = ref()
const pyroResubscribeModal = ref()
const country = useUserCountry()
const price = computed(() =>
midasProduct.value?.prices?.find((x) => x.currency_code === getCurrency(country.value)),
@@ -1201,13 +1242,20 @@ const getProductFromPriceId = (priceId) => {
return productsData.value.find((p) => p.prices?.some((x) => x.id === priceId))
}
const getPyroCharge = (subscription) => {
function getPyroCharge(subscription) {
if (!subscription || !charges.value) return null
return charges.value.find(
(charge) => charge.subscription_id === subscription.id && charge.status !== 'succeeded',
)
}
function getCancellationDate(subscription) {
const charge = getPyroCharge(subscription)
if (!charge) return null
if (charge.status === 'cancelled') return charge.due
return null
}
const getProductSize = (product) => {
if (!product || !product.metadata) return formatMessage(commonMessages.planUnknownLabel)
const ramSize = product.metadata.ram
@@ -1243,12 +1291,38 @@ const showPyroUpgradeModal = (subscription) => {
upgradeModal.value?.open(subscription?.metadata?.id)
}
const CHARGE_POLL_INTERVAL_MS = 20_000
const hasProvisioningSubscription = computed(() =>
pyroSubscriptions.value?.some((s) => s.serverInfo?.isProvisioning),
)
const { pause: pauseChargePoll, resume: resumeChargePoll } = useIntervalFn(
() => {
queryClient.invalidateQueries({ queryKey: ['billing', 'payments'] })
queryClient.invalidateQueries({ queryKey: ['billing', 'subscriptions'] })
},
CHARGE_POLL_INTERVAL_MS,
{ immediate: false },
)
watch(
hasProvisioningSubscription,
(isProvisioning) => {
if (isProvisioning) {
resumeChargePoll()
} else {
pauseChargePoll()
}
},
{ immediate: true },
)
const resubscribePyro = async (subscriptionId, wasSuspended) => {
try {
await client.labrinth.billing_internal.editSubscription(subscriptionId, {
cancelled: false,
})
await refresh()
if (wasSuspended) {
addNotification({
title: formatMessage(messages.pyroResubscribeRequestSubmittedTitle),
@@ -1271,6 +1345,35 @@ const resubscribePyro = async (subscriptionId, wasSuspended) => {
}
}
function openPyroResubscribeModal(subscription) {
const charge = getPyroCharge(subscription)
const product = getPyroProduct(subscription)
const interval = charge?.subscription_interval || subscription?.interval
const productPrice = getProductPrice(product, interval)
pyroResubscribeModal.value?.show({
subscriptionId: subscription?.id ?? '',
wasSuspended: charge?.due ? new Date(charge.due).getTime() < Date.now() : false,
serverName: subscription?.serverInfo?.name ?? 'this server',
planName: `${getProductSize(product)} plan`,
ramGb: product?.metadata?.ram ? product.metadata.ram / 1024 : undefined,
storageGb: product?.metadata?.storage ? product.metadata.storage / 1024 : undefined,
sharedCpus: product?.metadata?.cpu ? product.metadata.cpu / 2 : undefined,
priceCents: charge?.amount ?? productPrice?.prices?.intervals?.[interval],
currencyCode: charge?.currency_code ?? productPrice?.currency_code,
interval,
nextChargeDate: charge?.due,
})
}
function handlePyroResubscribeConfirm({ subscriptionId, wasSuspended }) {
return resubscribePyro(subscriptionId, wasSuspended)
}
function getBackupDownloadForServer(serverInfo) {
return getLatestBackupDownload(serverInfo.server_id, serverFullList.value)
}
const refresh = async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['billing'] }),