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 86e284c4bdd6fa42c3c8fbaf1efbec41f4d1c6d2.

* 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 15:38:08 -06:00
committed by GitHub
parent a2a97d1313
commit 693a371d61
278 changed files with 15974 additions and 12608 deletions

View File

@@ -0,0 +1,351 @@
import { createGlobalState } from '@vueuse/core'
import { type Ref, shallowRef, triggerRef } from 'vue'
import { detectLogLevel } from '../layouts/shared/console/composables/log-level'
import type { Log4jEvent, LogLevel, LogLine } from '../layouts/shared/console/types'
// Flip to true during development to enable console perf logging.
// Uses a plain constant to avoid turbo env-var declarations.
const DEBUG_PERF = false
// TODO: for true unbounded history, consider IndexedDB or similar
const ARCHIVE_CAPACITY = 500_000
const ENTRY_START_RE = /^\[\d{2}:\d{2}:\d{2}\]/
/**
* Reorders a batch of log lines so that continuation lines (lines without a
* timestamp prefix) stay grouped with their parent error/warn entry, even when
* unrelated timestamped lines arrive between them from the server.
*/
function groupContinuations(lines: LogLine[]): LogLine[] {
if (lines.length <= 1) return lines
const groups: LogLine[][] = []
for (const line of lines) {
if (ENTRY_START_RE.test(line.text)) {
groups.push([line])
} else if (groups.length > 0) {
let target = groups.length - 1
const lastEntry = groups[target][0]
if (lastEntry.level !== 'error' && lastEntry.level !== 'warn') {
if (line.level === 'error' || line.level === null) {
for (let i = groups.length - 2; i >= 0; i--) {
if (groups[i][0].level === 'error' || groups[i][0].level === 'warn') {
target = i
break
}
}
}
}
groups[target].push(line)
} else {
groups.push([line])
}
}
return groups.flat()
}
const batchTimeout = 300
const initialBatchSize = 256
const LogLevelCode = {
None: 0,
Trace: 1,
Debug: 2,
Info: 3,
Warn: 4,
Error: 5,
} as const
type LogLevelCode = (typeof LogLevelCode)[keyof typeof LogLevelCode]
function encodeLevel(level: LogLevel | null): LogLevelCode {
if (!level) return LogLevelCode.None
switch (level) {
case 'trace':
return LogLevelCode.Trace
case 'debug':
return LogLevelCode.Debug
case 'info':
return LogLevelCode.Info
case 'warn':
return LogLevelCode.Warn
case 'error':
return LogLevelCode.Error
}
}
function decodeLevel(code: LogLevelCode): LogLevel | null {
switch (code) {
case LogLevelCode.Trace:
return 'trace'
case LogLevelCode.Debug:
return 'debug'
case LogLevelCode.Info:
return 'info'
case LogLevelCode.Warn:
return 'warn'
case LogLevelCode.Error:
return 'error'
default:
return null
}
}
// Columnar ring buffer: stores text and level in parallel arrays instead of
// LogLine objects, eliminating ~40 bytes of object header per line (~20MB
// saved at 500k lines). Lines are stored by value — get(i) returns a fresh
// LogLine each call, so consumers must not rely on reference identity.
class ColumnarRingBuffer {
texts: (string | undefined)[]
levels: Uint8Array
private head = 0
private _size = 0
constructor(readonly capacity: number) {
this.texts = new Array(capacity)
this.levels = new Uint8Array(capacity)
}
get size(): number {
return this._size
}
push(text: string, level: LogLevel | null): boolean {
const wrapped = this._size === this.capacity
this.texts[this.head] = text
this.levels[this.head] = encodeLevel(level)
this.head = (this.head + 1) % this.capacity
if (!wrapped) this._size++
return wrapped
}
get(index: number): LogLine {
if (index < 0 || index >= this._size) {
throw new RangeError(`Index ${index} out of bounds [0, ${this._size})`)
}
const start = this._size === this.capacity ? this.head : 0
const physical = (start + index) % this.capacity
return {
text: this.texts[physical] as string,
level: decodeLevel(this.levels[physical] as LogLevelCode),
}
}
toArray(): LogLine[] {
if (this._size === 0) return []
const start = this._size === this.capacity ? this.head : 0
const result = new Array<LogLine>(this._size)
for (let i = 0; i < this._size; i++) {
const physical = (start + i) % this.capacity
result[i] = {
text: this.texts[physical] as string,
level: decodeLevel(this.levels[physical] as LogLevelCode),
}
}
return result
}
clear(): void {
this.texts = new Array(this.capacity)
this.levels = new Uint8Array(this.capacity)
this.head = 0
this._size = 0
}
}
function mapLog4jLevel(level?: string): LogLevel | null {
if (!level) return null
switch (level.toUpperCase()) {
case 'FATAL':
case 'ERROR':
return 'error'
case 'WARN':
return 'warn'
case 'INFO':
return 'info'
case 'DEBUG':
return 'debug'
case 'TRACE':
return 'trace'
default:
return null
}
}
function formatTimestamp(millis?: number): string {
if (!millis) return ''
const date = new Date(millis)
const h = String(date.getHours()).padStart(2, '0')
const m = String(date.getMinutes()).padStart(2, '0')
const s = String(date.getSeconds()).padStart(2, '0')
return `[${h}:${m}:${s}]`
}
function formatLog4jLines(event: Log4jEvent): LogLine[] {
const level = mapLog4jLevel(event.level)
const time = formatTimestamp(event.timestamp_millis)
const thread = event.thread_name ?? ''
const levelStr = event.level ?? ''
const message = event.message?.trim() ?? ''
const prefix = time ? `${time} [${thread}/${levelStr}]: ` : `[${thread}/${levelStr}]: `
const messageLines = message.split(/\r?\n/)
const lines: LogLine[] = [{ text: prefix + messageLines[0], level }]
for (let i = 1; i < messageLines.length; i++) {
if (!messageLines[i].trim()) continue
lines.push({ text: messageLines[i], level })
}
if (event.throwable) {
for (const line of event.throwable.split(/\r?\n/)) {
if (!line.trim()) continue
lines.push({ text: line, level: 'error' })
}
}
return lines
}
function textToLogLine(text: string): LogLine {
return { text, level: detectLogLevel(text) }
}
export function createConsoleState() {
const archive = new ColumnarRingBuffer(ARCHIVE_CAPACITY)
const output: Ref<LogLine[]> = shallowRef<LogLine[]>([])
let lineBuffer: LogLine[] = []
let batchTimer: NodeJS.Timeout | null = null
let wrapCount = 0
let lastFlushMs = 0
const flushBuffer = (): void => {
if (lineBuffer.length === 0) return
const t0 = DEBUG_PERF ? performance.now() : 0
const arr = output.value
const lines = groupContinuations(lineBuffer)
const flushedCount = lines.length
let didWrap = false
for (const line of lines) {
if (archive.push(line.text, line.level)) didWrap = true
arr.push(line)
}
if (didWrap) {
const evictedCount = Math.max(0, arr.length - archive.size)
if (evictedCount > 0) {
arr.splice(0, evictedCount)
}
wrapCount++
}
lineBuffer = []
batchTimer = null
triggerRef(output)
if (DEBUG_PERF) {
lastFlushMs = performance.now() - t0
if (arr.length !== archive.size) {
console.error(
`[mr-console] drift: output.length=${arr.length} !== archive.size=${archive.size}`,
)
}
console.debug(
`[mr-console] flush: ${flushedCount} lines in ${lastFlushMs.toFixed(2)}ms` +
` | buffer: ${archive.size} | wrap: ${didWrap}`,
)
}
}
const addLines = (lines: LogLine[]): void => {
if (output.value.length === 0 && lines.length >= initialBatchSize) {
lineBuffer = lines
flushBuffer()
return
}
lineBuffer.push(...lines)
if (!batchTimer) {
batchTimer = setTimeout(flushBuffer, batchTimeout)
}
}
const addLog4jEvent = (event: Log4jEvent): void => {
addLines(formatLog4jLines(event))
}
const addLegacyLog = (message: string): void => {
const logLines = message
.split('\n')
.filter((l) => l.trim())
.map(textToLogLine)
let parentLevel: LogLevel | null = null
for (const line of logLines) {
if (ENTRY_START_RE.test(line.text)) {
parentLevel = line.level
} else if (line.level === null && parentLevel !== null) {
line.level = parentLevel
}
}
addLines(logLines)
}
const clear = (): void => {
const t0 = DEBUG_PERF ? performance.now() : 0
archive.clear()
output.value = []
lineBuffer = []
wrapCount = 0
if (batchTimer) {
clearTimeout(batchTimer)
batchTimer = null
}
if (DEBUG_PERF) {
console.debug(`[mr-console] clear in ${(performance.now() - t0).toFixed(2)}ms`)
}
}
const __debugStats = ():
| { enabled: false }
| {
enabled: true
bufferSize: number
heapEstimate: number
recentFlushMs: number
wrapCount: number
} => {
if (!DEBUG_PERF) return { enabled: false }
const heapEstimate =
archive.texts.reduce<number>((a, s) => a + (s?.length ?? 0) * 2, 0) +
archive.levels.byteLength
return {
enabled: true,
bufferSize: archive.size,
heapEstimate,
recentFlushMs: lastFlushMs,
wrapCount,
}
}
return {
output,
addLines,
addLog4jEvent,
addLegacyLog,
clear,
__debugStats,
}
}
export const useModrinthServersConsole = createGlobalState(createConsoleState)