refactor: Huge pyro servers composable cleanup (#3745)

* refactor: start refactor of pyro servers module-based class

* refactor: finish modules

* refactor: start on type checking + matching api

* refactor: finish pyro servers composable refactor

* refactor: pyro -> modrinth

* fix: import not refactored

* fix: broken power action enums

* fix: remove pyro mentions

* fix: lint

* refactor: fix option pages

* fix: error renames

* remove empty pyro-servers.ts file

---------

Signed-off-by: IMB11 <hendersoncal117@gmail.com>
Co-authored-by: Prospector <prospectordev@gmail.com>
This commit is contained in:
IMB11 2025-06-11 23:32:39 +01:00 committed by GitHub
parent 6955731def
commit 1b1d41605b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 1791 additions and 2513 deletions

6
.idea/vcs.xml generated
View File

@ -1,5 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CommitMessageInspectionProfile">
<profile version="1.0">
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>

View File

@ -45,9 +45,11 @@
import { ref, nextTick, computed } from "vue";
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets";
import { ModrinthServersFetchError, type ServerBackup } from "@modrinth/utils";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const modal = ref<InstanceType<typeof NewModal>>();
@ -64,7 +66,7 @@ const trimmedName = computed(() => backupName.value.trim());
const nameExists = computed(() => {
if (!props.server.backups?.data) return false;
return props.server.backups.data.some(
(backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
(backup: ServerBackup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
);
});
@ -98,7 +100,7 @@ const createBackup = async () => {
hideModal();
await props.server.refresh();
} catch (error) {
if (error instanceof PyroFetchError && error.statusCode === 429) {
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
isRateLimited.value = true;
addNotification({
type: "error",

View File

@ -20,13 +20,9 @@
<script setup lang="ts">
import { ref } from "vue";
import { ConfirmModal } from "@modrinth/ui";
import type { Server } from "~/composables/pyroServers";
import type { Backup } from "@modrinth/utils";
import BackupItem from "~/components/ui/servers/BackupItem.vue";
defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
const emit = defineEmits<{
(e: "delete", backup: Backup | undefined): void;
}>();

View File

@ -17,7 +17,7 @@ import {
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
import { defineMessages, useVIntl } from "@vintl/vintl";
import { ref } from "vue";
import type { Backup } from "~/composables/pyroServers.ts";
import type { Backup } from "@modrinth/utils";
const flags = useFeatureFlags();
const { formatMessage } = useVIntl();

View File

@ -48,10 +48,11 @@
import { ref, nextTick, computed } from "vue";
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { SpinnerIcon, SaveIcon, XIcon, IssuesIcon } from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
import type { Backup } from "@modrinth/utils";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const modal = ref<InstanceType<typeof NewModal>>();
@ -70,7 +71,7 @@ const nameExists = computed(() => {
}
return props.server.backups.data.some(
(backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
(backup: Backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
);
});

View File

@ -19,11 +19,12 @@
<script setup lang="ts">
import { ref } from "vue";
import { ConfirmModal, NewModal } from "@modrinth/ui";
import type { Server } from "~/composables/pyroServers";
import type { Backup } from "@modrinth/utils";
import BackupItem from "~/components/ui/servers/BackupItem.vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const modal = ref<InstanceType<typeof NewModal>>();

View File

@ -59,10 +59,10 @@
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { XIcon, SaveIcon } from "@modrinth/assets";
import { ref, computed } from "vue";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["backups"]>;
server: ModrinthServer;
}>();
const modal = ref<InstanceType<typeof NewModal>>();

View File

@ -239,7 +239,7 @@ import {
import { Admonition, ButtonStyled, NewModal } from "@modrinth/ui";
import TagItem from "@modrinth/ui/src/components/base/TagItem.vue";
import { ref, computed } from "vue";
import { formatCategory, formatVersionsForDisplay, type Version } from "@modrinth/utils";
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from "@modrinth/utils";
import Accordion from "~/components/ui/Accordion.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
import ContentVersionFilter, {

View File

@ -99,7 +99,7 @@
import { FolderOpenIcon, CheckCircleIcon, XCircleIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
import { ref, computed, watch, nextTick } from "vue";
import type { FSModule } from "~/composables/pyroServers.ts";
import { FSModule } from "~/composables/servers/modules/fs.ts";
interface UploadItem {
file: File;

View File

@ -75,13 +75,14 @@
<script setup lang="ts">
import { ExternalIcon, SpinnerIcon, DownloadIcon, XIcon } from "@modrinth/assets";
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
import { ModrinthServersFetchError } from "@modrinth/utils";
import { ref, computed, nextTick } from "vue";
import { handleError, type Server } from "~/composables/pyroServers.ts";
import { handleError, ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const cf = ref(false);
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const modal = ref<typeof NewModal>();
@ -110,24 +111,18 @@ const handleSubmit = async () => {
if (!error.value) {
// hide();
try {
const dry = await props.server.fs?.extractFile(trimmedUrl.value, true, true);
const dry = await props.server.fs.extractFile(trimmedUrl.value, true, true);
if (!cf.value || dry.modpack_name) {
await props.server.fs?.extractFile(trimmedUrl.value, true, false, true);
await props.server.fs.extractFile(trimmedUrl.value, true, false, true);
hide();
} else {
submitted.value = false;
handleError(
new ServersError(
new ModrinthServersFetchError(
"Could not find CurseForge modpack at that URL.",
undefined,
undefined,
undefined,
{
context: "Error installing modpack",
error: `url: ${url.value}`,
description: "Could not find CurseForge modpack at that URL.",
},
404,
new Error(`No modpack found at ${url.value}`),
),
);
}

View File

@ -59,7 +59,7 @@
<template v-else>
<ButtonStyled v-if="showStopButton" type="transparent">
<button :disabled="!canTakeAction" @click="initiateAction('stop')">
<button :disabled="!canTakeAction" @click="initiateAction('Stop')">
<div class="flex gap-1">
<StopCircleIcon class="h-5 w-5" />
<span>{{ isStoppingState ? "Stopping..." : "Stop" }}</span>
@ -120,14 +120,12 @@ import {
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { useRouter } from "vue-router";
import { useStorage } from "@vueuse/core";
type ServerAction = "start" | "stop" | "restart" | "kill";
type ServerState = "stopped" | "starting" | "running" | "stopping" | "restarting";
import type { PowerAction as ServerPowerAction, ServerState } from "@modrinth/utils";
const flags = useFeatureFlags();
interface PowerAction {
action: ServerAction;
action: ServerPowerAction;
nextState: ServerState;
}
@ -142,7 +140,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: "action", action: ServerAction): void;
(e: "action", action: ServerPowerAction): void;
}>();
const router = useRouter();
@ -170,7 +168,7 @@ const isStoppingState = computed(() => serverState.value === "stopping");
const showStopButton = computed(() => isRunning.value || isStoppingState.value);
const primaryActionText = computed(() => {
const states: Record<ServerState, string> = {
const states: Partial<Record<ServerState, string>> = {
starting: "Starting...",
restarting: "Restarting...",
running: "Restart",
@ -193,7 +191,7 @@ const menuOptions = computed(() => [
id: "kill",
label: "Kill server",
icon: SlashIcon,
action: () => initiateAction("kill"),
action: () => initiateAction("Kill"),
},
]),
{
@ -221,17 +219,17 @@ async function copyId() {
await navigator.clipboard.writeText(serverId as string);
}
function initiateAction(action: ServerAction) {
function initiateAction(action: ServerPowerAction) {
if (!canTakeAction.value) return;
const stateMap: Record<ServerAction, ServerState> = {
start: "starting",
stop: "stopping",
restart: "restarting",
kill: "stopping",
const stateMap: Record<ServerPowerAction, ServerState> = {
Start: "starting",
Stop: "stopping",
Restart: "restarting",
Kill: "stopping",
};
if (action === "start") {
if (action === "Start") {
emit("action", action);
serverState.value = stateMap[action];
startingDelay.value = true;
@ -249,7 +247,7 @@ function initiateAction(action: ServerAction) {
}
function handlePrimaryAction() {
initiateAction(isRunning.value ? "restart" : "start");
initiateAction(isRunning.value ? "Restart" : "Start");
}
function executePowerAction() {
@ -263,7 +261,7 @@ function executePowerAction() {
userPreferences.value.powerDontAskAgain = true;
}
if (action === "start") {
if (action === "Start") {
startingDelay.value = true;
setTimeout(() => (startingDelay.value = false), 5000);
}

View File

@ -40,7 +40,7 @@
<script setup lang="ts">
import { ref } from "vue";
import type { ServerState } from "~/types/servers";
import type { ServerState } from "@modrinth/utils";
const STATUS_CLASSES = {
running: { main: "bg-brand", bg: "bg-bg-green" },
@ -49,7 +49,7 @@ const STATUS_CLASSES = {
unknown: { main: "", bg: "" },
} as const;
const STATUS_TEXTS = {
const STATUS_TEXTS: Partial<Record<ServerState, string>> = {
running: "Running",
stopped: "",
crashed: "Crashed",
@ -63,7 +63,10 @@ defineProps<{
const isExpanded = ref(false);
function getStatusClass(state: ServerState) {
return STATUS_CLASSES[state] ?? STATUS_CLASSES.unknown;
if (state in STATUS_CLASSES) {
return STATUS_CLASSES[state as keyof typeof STATUS_CLASSES];
}
return STATUS_CLASSES.unknown;
}
function getStatusText(state: ServerState) {

View File

@ -301,7 +301,7 @@ import { useDebounceFn } from "@vueuse/core";
import { NewModal } from "@modrinth/ui";
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
import DOMPurify from "dompurify";
import { usePyroConsole } from "~/store/console.ts";
import { useModrinthServersConsole } from "~/store/console.ts";
const { $cosmetics } = useNuxtApp();
const cosmetics = $cosmetics;
@ -318,7 +318,7 @@ const SEPARATOR_HEIGHT = 32;
const SCROLL_END_DELAY = 150;
const progressiveBlurIterations = ref(8);
const pyroConsole = usePyroConsole();
const pyroConsole = useModrinthServersConsole();
const consoleOutput = computed(() => (props.loading ? [] : pyroConsole.output.value));
const scrollContainer = ref<HTMLElement | null>(null);

View File

@ -69,10 +69,11 @@
<script setup lang="ts">
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { DownloadIcon, XIcon } from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServersFetchError } from "@modrinth/utils";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
project: any;
versions: any[];
currentVersion?: any;
@ -98,8 +99,7 @@ const handleReinstall = async () => {
try {
const versionId = props.versions.find((v) => v.version_number === selectedVersion.value)?.id;
await props.server.general?.reinstall(
props.server.serverId,
await props.server.general.reinstall(
false,
props.project.id,
versionId,
@ -110,7 +110,7 @@ const handleReinstall = async () => {
emit("reinstall");
hide();
} catch (error) {
if (error instanceof PyroFetchError && error.statusCode === 429) {
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
addNotification({
group: "server",
title: "Cannot reinstall server",

View File

@ -116,11 +116,12 @@
<script setup lang="ts">
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
import { UploadIcon, RightArrowIcon, XIcon, ServerIcon } from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServersFetchError } from "@modrinth/utils";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
backupInProgress?: BackupInProgressReason;
}>();
@ -175,7 +176,7 @@ const handleReinstall = async () => {
window.scrollTo(0, 0);
hide();
} catch (error) {
if (error instanceof PyroFetchError && error.statusCode === 429) {
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
addNotification({
group: "server",
title: "Cannot reinstall server",

View File

@ -200,9 +200,9 @@
import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets";
import { $fetch } from "ofetch";
import type { Server } from "~/composables/pyroServers";
import type { Loaders } from "~/types/servers";
import { type Loaders, ModrinthServersFetchError } from "@modrinth/utils";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const { formatMessage } = useVIntl();
@ -220,7 +220,7 @@ type VersionMap = Record<string, LoaderVersion[]>;
type VersionCache = Record<string, any>;
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
currentLoader: Loaders | undefined;
backupInProgress?: BackupInProgressReason;
initialSetup?: boolean;
@ -458,7 +458,6 @@ const handleReinstall = async () => {
try {
await props.server.general?.reinstall(
props.server.serverId,
true,
selectedLoader.value,
selectedMCVersion.value,
@ -474,7 +473,7 @@ const handleReinstall = async () => {
hide();
} catch (error) {
if (error instanceof PyroFetchError && error.statusCode === 429) {
if (error instanceof ModrinthServersFetchError && (error as any)?.statusCode === 429) {
addNotification({
group: "server",
title: "Cannot reinstall server",

View File

@ -31,7 +31,7 @@
<script setup lang="ts">
import { ButtonStyled } from "@modrinth/ui";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
isUpdating: boolean;
@ -39,7 +39,7 @@ const props = defineProps<{
save: () => void;
reset: () => void;
isVisible: boolean;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const saveAndRestart = async () => {

View File

@ -160,14 +160,14 @@
<script setup lang="ts">
import { ButtonStyled, NewProjectCard } from "@modrinth/ui";
import { TransferIcon, UploadIcon, InfoIcon, CompassIcon, SettingsIcon } from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
import type { Loaders } from "~/types/servers";
import type { Loaders } from "@modrinth/utils";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const { formatMessage } = useVIntl();
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
ignoreCurrentInstallation?: boolean;
backupInProgress?: BackupInProgressReason;
}>();

View File

@ -81,12 +81,13 @@
<script setup lang="ts">
import { ChevronRightIcon, LockIcon, SparklesIcon } from "@modrinth/assets";
import type { Project, Server } from "~/types/servers";
import type { Project, Server } from "@modrinth/utils";
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<Partial<Server>>();
if (props.server_id) {
await usePyroServer(props.server_id, ["general"]);
await useModrinthServers(props.server_id, ["general"]);
}
const showGameLabel = computed(() => !!props.game);
@ -109,7 +110,7 @@ if (props.upstream) {
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined);
if (import.meta.server && projectData.value?.icon_url) {
await usePyroServer(props.server_id!, ["general"]);
await useModrinthServers(props.server_id!, ["general"]);
}
const iconUrl = computed(() => projectData.value?.icon_url || undefined);

View File

@ -34,15 +34,15 @@
<script setup lang="ts">
import { RightArrowIcon } from "@modrinth/assets";
import type { RouteLocationNormalized } from "vue-router";
import type { Server } from "~/composables/pyroServers";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const emit = defineEmits(["reinstall"]);
defineProps<{
navLinks: { label: string; href: string; icon: Component; external?: boolean }[];
route: RouteLocationNormalized;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
backupInProgress?: BackupInProgressReason;
}>();

View File

@ -72,7 +72,7 @@
import { ref, computed, shallowRef } from "vue";
import { FolderOpenIcon, CPUIcon, DatabaseIcon, IssuesIcon } from "@modrinth/assets";
import { useStorage } from "@vueuse/core";
import type { Stats } from "~/types/servers";
import type { Stats } from "@modrinth/utils";
const route = useNativeRoute();
const serverId = route.params.id;

View File

@ -224,7 +224,7 @@
<script setup lang="ts">
import { LoaderIcon } from "@modrinth/assets";
import type { Loaders } from "~/types/servers";
import type { Loaders } from "@modrinth/utils";
defineProps<{
loader: Loaders;

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ButtonStyled, ServersSpecs } from "@modrinth/ui";
import type { MessageDescriptor } from "@vintl/vintl";
import { formatPrice } from "../../../../../../../packages/utils";
import { formatPrice } from "@modrinth/utils";
const { formatMessage, locale } = useVIntl();

View File

@ -3,7 +3,7 @@ import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from "@modri
import { PlusIcon, XIcon } from "@modrinth/assets";
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { ref } from "vue";
import { usePyroFetch } from "~/composables/pyroFetch.ts";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
const app = useNuxtApp() as unknown as { $notify: any };
@ -23,7 +23,7 @@ const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === "no
const inputField = ref("");
async function refresh() {
await usePyroFetch("notices").then((res) => {
await useServersFetch("notices").then((res) => {
const notices = res as ServerNoticeType[];
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? [];
});
@ -33,9 +33,12 @@ async function assign(server: boolean = true) {
const input = inputField.value.trim();
if (input !== "" && notice.value) {
await usePyroFetch(`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`, {
method: "PUT",
}).catch((err) => {
await useServersFetch(
`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`,
{
method: "PUT",
},
).catch((err) => {
app.$notify({
group: "main",
title: "Error assigning notice",
@ -75,9 +78,12 @@ async function unassignDetect() {
async function unassign(id: string, server: boolean = true) {
if (notice.value) {
await usePyroFetch(`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`, {
method: "PUT",
}).catch((err) => {
await useServersFetch(
`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`,
{
method: "PUT",
},
).catch((err) => {
app.$notify({
group: "main",
title: "Error unassigning notice",

View File

@ -2,13 +2,8 @@
import dayjs from "dayjs";
import { ButtonStyled, commonMessages, CopyCode, ServerNotice, TagItem } from "@modrinth/ui";
import { EditIcon, SettingsIcon, TrashIcon } from "@modrinth/assets";
import { ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { useRelativeTime } from "@modrinth/ui";
import {
DISMISSABLE,
getDismissableMetadata,
NOTICE_LEVELS,
} from "@modrinth/ui/src/utils/notices.ts";
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { useRelativeTime, getDismissableMetadata, NOTICE_LEVELS } from "@modrinth/ui";
import { useVIntl } from "@vintl/vintl";
const { formatMessage } = useVIntl();

View File

@ -1,112 +0,0 @@
import { $fetch, FetchError } from "ofetch";
interface PyroFetchOptions {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
contentType?: string;
body?: Record<string, any>;
version?: number;
override?: {
url?: string;
token?: string;
};
retry?: boolean;
bypassAuth?: boolean;
}
export class PyroFetchError extends Error {
constructor(
message: string,
public statusCode?: number,
public originalError?: Error,
) {
super(message);
this.name = "PyroFetchError";
}
}
export async function usePyroFetch<T>(path: string, options: PyroFetchOptions = {}): Promise<T> {
const config = useRuntimeConfig();
const auth = await useAuth();
const authToken = auth.value?.token;
if (!authToken && !options.bypassAuth) {
throw new PyroFetchError("Cannot pyrofetch without auth", 10000);
}
const { method = "GET", contentType = "application/json", body, version = 0, override } = options;
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
/\/$/,
"",
);
if (!base) {
throw new PyroFetchError(
"Cannot pyrofetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
10001,
);
}
const fullUrl = override?.url
? `https://${override.url}/${path.replace(/^\//, "")}`
: version === 0
? `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`
: `${base}/v${version}/${path.replace(/^\//, "")}`;
type HeadersRecord = Record<string, string>;
const authHeader: HeadersRecord = options.bypassAuth
? {}
: {
Authorization: `Bearer ${override?.token ?? authToken}`,
"Access-Control-Allow-Headers": "Authorization",
};
const headers: HeadersRecord = {
...authHeader,
"User-Agent": "Pyro/1.0 (https://pyro.host)",
Vary: "Accept, Origin",
"Content-Type": contentType,
};
if (import.meta.client && typeof window !== "undefined") {
headers.Origin = window.location.origin;
}
try {
const response = await $fetch<T>(fullUrl, {
method,
headers,
body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
timeout: 10000,
retry: options.retry !== false ? (method === "GET" ? 3 : 0) : 0,
});
return response;
} catch (error) {
console.error("Fetch error:", error);
if (error instanceof FetchError) {
const statusCode = error.response?.status;
const statusText = error.response?.statusText || "Unknown error";
const errorMessages: { [key: number]: string } = {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
429: "Too Many Requests",
500: "Internal Server Error",
502: "Bad Gateway",
};
const message =
statusCode && statusCode in errorMessages
? errorMessages[statusCode]
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
throw new PyroFetchError(`[PYROFETCH][PYRO] ${message}`, statusCode, error);
}
throw new PyroFetchError(
"[PYROFETCH][PYRO] An unexpected error occurred during the fetch operation.",
undefined,
error as Error,
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,265 @@
import { ModrinthServerError } from "@modrinth/utils";
import type { JWTAuth, ModuleError, ModuleName } from "@modrinth/utils";
import { useServersFetch } from "./servers-fetch.ts";
import {
GeneralModule,
ContentModule,
BackupsModule,
NetworkModule,
StartupModule,
WSModule,
FSModule,
} from "./modules/index.ts";
export function handleError(err: any) {
if (err instanceof ModrinthServerError && err.v1Error) {
addNotification({
title: err.v1Error?.context ?? `An error occurred`,
type: "error",
text: err.v1Error.description,
errorCode: err.v1Error.error,
});
} else {
addNotification({
title: "An error occurred",
type: "error",
text: err.message ?? (err.data ? err.data.description : err),
});
}
}
export class ModrinthServer {
readonly serverId: string;
private errors: Partial<Record<ModuleName, ModuleError>> = {};
readonly general: GeneralModule;
readonly content: ContentModule;
readonly backups: BackupsModule;
readonly network: NetworkModule;
readonly startup: StartupModule;
readonly ws: WSModule;
readonly fs: FSModule;
constructor(serverId: string) {
this.serverId = serverId;
this.general = new GeneralModule(this);
this.content = new ContentModule(this);
this.backups = new BackupsModule(this);
this.network = new NetworkModule(this);
this.startup = new StartupModule(this);
this.ws = new WSModule(this);
this.fs = new FSModule(this);
}
async createMissingFolders(path: string): Promise<void> {
if (path.startsWith("/")) {
path = path.substring(1);
}
const folders = path.split("/");
let currentPath = "";
for (const folder of folders) {
currentPath += "/" + folder;
try {
await this.fs.createFileOrFolder(currentPath, "directory");
} catch {
// Folder might already exist, ignore error
}
}
}
async fetchConfigFile(fileName: string): Promise<any> {
return await useServersFetch(`servers/${this.serverId}/config/${fileName}`);
}
constructServerProperties(properties: any): string {
let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`;
for (const [key, value] of Object.entries(properties)) {
if (typeof value === "object") {
fileContent += `${key}=${JSON.stringify(value)}\n`;
} else if (typeof value === "boolean") {
fileContent += `${key}=${value ? "true" : "false"}\n`;
} else {
fileContent += `${key}=${value}\n`;
}
}
return fileContent;
}
async processImage(iconUrl: string | undefined): Promise<string | undefined> {
const sharedImage = useState<string | undefined>(`server-icon-${this.serverId}`);
if (sharedImage.value) {
return sharedImage.value;
}
try {
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
try {
const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
override: auth,
retry: false,
});
if (fileData instanceof Blob && import.meta.client) {
const dataURL = await new Promise<string>((resolve) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = () => {
canvas.width = 512;
canvas.height = 512;
ctx?.drawImage(img, 0, 0, 512, 512);
const dataURL = canvas.toDataURL("image/png");
sharedImage.value = dataURL;
resolve(dataURL);
URL.revokeObjectURL(img.src);
};
img.src = URL.createObjectURL(fileData);
});
return dataURL;
}
} catch (error) {
if (error instanceof ModrinthServerError && error.statusCode === 404 && iconUrl) {
// Handle external icon processing
try {
const response = await fetch(iconUrl);
if (!response.ok) throw new Error("Failed to fetch icon");
const file = await response.blob();
const originalFile = new File([file], "server-icon-original.png", {
type: "image/png",
});
if (import.meta.client) {
const dataURL = await new Promise<string>((resolve) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = () => {
canvas.width = 64;
canvas.height = 64;
ctx?.drawImage(img, 0, 0, 64, 64);
canvas.toBlob(async (blob) => {
if (blob) {
const scaledFile = new File([blob], "server-icon.png", { type: "image/png" });
await useServersFetch(`/create?path=/server-icon.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: scaledFile,
override: auth,
});
await useServersFetch(`/create?path=/server-icon-original.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: originalFile,
override: auth,
});
}
}, "image/png");
const dataURL = canvas.toDataURL("image/png");
sharedImage.value = dataURL;
resolve(dataURL);
URL.revokeObjectURL(img.src);
};
img.src = URL.createObjectURL(file);
});
return dataURL;
}
} catch (error) {
console.error("Failed to process external icon:", error);
}
}
}
} catch (error) {
console.error("Failed to process server icon:", error);
}
sharedImage.value = undefined;
return undefined;
}
async refresh(
modules: ModuleName[] = [],
options?: {
preserveConnection?: boolean;
preserveInstallState?: boolean;
},
): Promise<void> {
const modulesToRefresh =
modules.length > 0
? modules
: (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]);
for (const module of modulesToRefresh) {
try {
switch (module) {
case "general": {
if (options?.preserveConnection) {
const currentImage = this.general.image;
const currentMotd = this.general.motd;
const currentStatus = this.general.status;
await this.general.fetch();
if (currentImage) {
this.general.image = currentImage;
}
if (currentMotd) {
this.general.motd = currentMotd;
}
if (options.preserveInstallState && currentStatus === "installing") {
this.general.status = "installing";
}
} else {
await this.general.fetch();
}
break;
}
case "content":
await this.content.fetch();
break;
case "backups":
await this.backups.fetch();
break;
case "network":
await this.network.fetch();
break;
case "startup":
await this.startup.fetch();
break;
case "ws":
await this.ws.fetch();
break;
case "fs":
await this.fs.fetch();
break;
}
} catch (error) {
this.errors[module] = {
error:
error instanceof ModrinthServerError
? error
: new ModrinthServerError("Unknown error", undefined, error as Error),
timestamp: Date.now(),
};
}
}
}
get moduleErrors() {
return this.errors;
}
}
export const useModrinthServers = async (
serverId: string,
includedModules: ModuleName[] = ["general"],
) => {
const server = new ModrinthServer(serverId);
await server.refresh(includedModules);
return reactive(server);
};

View File

@ -0,0 +1,79 @@
import type { Backup, AutoBackupSettings } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
export class BackupsModule extends ServerModule {
data: Backup[] = [];
async fetch(): Promise<void> {
this.data = await useServersFetch<Backup[]>(`servers/${this.serverId}/backups`, {}, "backups");
}
async create(backupName: string): Promise<string> {
const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
method: "POST",
body: { name: backupName },
});
await this.fetch(); // Refresh this module
return response.id;
}
async rename(backupId: string, newName: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/rename`, {
method: "POST",
body: { name: newName },
});
await this.fetch(); // Refresh this module
}
async delete(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, {
method: "DELETE",
});
await this.fetch(); // Refresh this module
}
async restore(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
method: "POST",
});
await this.fetch(); // Refresh this module
}
async prepare(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/prepare-download`, {
method: "POST",
});
}
async lock(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
method: "POST",
});
await this.fetch(); // Refresh this module
}
async unlock(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, {
method: "POST",
});
await this.fetch(); // Refresh this module
}
async retry(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/retry`, {
method: "POST",
});
}
async updateAutoBackup(autoBackup: "enable" | "disable", interval: number): Promise<void> {
await useServersFetch(`servers/${this.serverId}/autobackup`, {
method: "POST",
body: { set: autoBackup, interval },
});
}
async getAutoBackup(): Promise<AutoBackupSettings> {
return await useServersFetch(`servers/${this.serverId}/autobackup`);
}
}

View File

@ -0,0 +1,15 @@
import type { ModrinthServer } from "../modrinth-servers.ts";
export abstract class ServerModule {
protected server: ModrinthServer;
constructor(server: ModrinthServer) {
this.server = server;
}
protected get serverId(): string {
return this.server.serverId;
}
abstract fetch(): Promise<void>;
}

View File

@ -0,0 +1,36 @@
import type { Mod, ContentType } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
export class ContentModule extends ServerModule {
data: Mod[] = [];
async fetch(): Promise<void> {
const mods = await useServersFetch<Mod[]>(`servers/${this.serverId}/mods`, {}, "content");
this.data = mods.sort((a, b) => (a?.name ?? "").localeCompare(b?.name ?? ""));
}
async install(contentType: ContentType, projectId: string, versionId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/mods`, {
method: "POST",
body: {
rinth_ids: { project_id: projectId, version_id: versionId },
install_as: contentType,
},
});
}
async remove(path: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/deleteMod`, {
method: "POST",
body: { path },
});
}
async reinstall(replace: string, projectId: string, versionId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/mods/update`, {
method: "POST",
body: { replace, project_id: projectId, version_id: versionId },
});
}
}

View File

@ -0,0 +1,224 @@
import type {
FileUploadQuery,
JWTAuth,
DirectoryResponse,
FilesystemOp,
FSQueuedOp,
} from "@modrinth/utils";
import { ModrinthServerError } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
export class FSModule extends ServerModule {
auth!: JWTAuth;
ops: FilesystemOp[] = [];
queuedOps: FSQueuedOp[] = [];
opsQueuedForModification: string[] = [];
async fetch(): Promise<void> {
this.auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`, {}, "fs");
this.ops = [];
this.queuedOps = [];
this.opsQueuedForModification = [];
}
private async retryWithAuth<T>(requestFn: () => Promise<T>): Promise<T> {
try {
return await requestFn();
} catch (error) {
if (error instanceof ModrinthServerError && error.statusCode === 401) {
await this.fetch(); // Refresh auth
return await requestFn();
}
throw error;
}
}
listDirContents(path: string, page: number, pageSize: number): Promise<DirectoryResponse> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
override: this.auth,
retry: false,
});
});
}
createFileOrFolder(path: string, type: "file" | "directory"): Promise<void> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
await useServersFetch(`/create?path=${encodedPath}&type=${type}`, {
method: "POST",
contentType: "application/octet-stream",
override: this.auth,
});
});
}
uploadFile(path: string, file: File): FileUploadQuery {
const encodedPath = encodeURIComponent(path);
const progressSubject = new EventTarget();
const abortController = new AbortController();
const uploadPromise = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
const progress = (e.loaded / e.total) * 100;
progressSubject.dispatchEvent(
new CustomEvent("progress", {
detail: { loaded: e.loaded, total: e.total, progress },
}),
);
}
});
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(new Error(`Upload failed with status ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error("Upload failed"));
xhr.onabort = () => reject(new Error("Upload cancelled"));
xhr.open("POST", `https://${this.auth.url}/create?path=${encodedPath}&type=file`);
xhr.setRequestHeader("Authorization", `Bearer ${this.auth.token}`);
xhr.setRequestHeader("Content-Type", "application/octet-stream");
xhr.send(file);
abortController.signal.addEventListener("abort", () => xhr.abort());
});
return {
promise: uploadPromise,
onProgress: (
callback: (progress: { loaded: number; total: number; progress: number }) => void,
) => {
progressSubject.addEventListener("progress", ((e: CustomEvent) => {
callback(e.detail);
}) as EventListener);
},
cancel: () => abortController.abort(),
} as FileUploadQuery;
}
renameFileOrFolder(path: string, name: string): Promise<void> {
const pathName = path.split("/").slice(0, -1).join("/") + "/" + name;
return this.retryWithAuth(async () => {
await useServersFetch(`/move`, {
method: "POST",
override: this.auth,
body: { source: path, destination: pathName },
});
});
}
updateFile(path: string, content: string): Promise<void> {
const octetStream = new Blob([content], { type: "application/octet-stream" });
return this.retryWithAuth(async () => {
await useServersFetch(`/update?path=${path}`, {
method: "PUT",
contentType: "application/octet-stream",
body: octetStream,
override: this.auth,
});
});
}
moveFileOrFolder(path: string, newPath: string): Promise<void> {
return this.retryWithAuth(async () => {
await this.server.createMissingFolders(newPath.substring(0, newPath.lastIndexOf("/")));
await useServersFetch(`/move`, {
method: "POST",
override: this.auth,
body: { source: path, destination: newPath },
});
});
}
deleteFileOrFolder(path: string, recursive: boolean): Promise<void> {
const encodedPath = encodeURIComponent(path);
return this.retryWithAuth(async () => {
await useServersFetch(`/delete?path=${encodedPath}&recursive=${recursive}`, {
method: "DELETE",
override: this.auth,
});
});
}
downloadFile(path: string, raw?: boolean): Promise<any> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
const fileData = await useServersFetch(`/download?path=${encodedPath}`, {
override: this.auth,
});
if (fileData instanceof Blob) {
return raw ? fileData : await fileData.text();
}
return fileData;
});
}
extractFile(
path: string,
override = true,
dry = false,
silentQueue = false,
): Promise<{ modpack_name: string | null; conflicting_files: string[] }> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
if (!silentQueue) {
this.queuedOps.push({ op: "unarchive", src: path });
setTimeout(() => this.removeQueuedOp("unarchive", path), 4000);
}
try {
return await useServersFetch(
`/unarchive?src=${encodedPath}&trg=/&override=${override}&dry=${dry}`,
{
method: "POST",
override: this.auth,
version: 1,
},
undefined,
"Error extracting file",
);
} catch (err) {
this.removeQueuedOp("unarchive", path);
throw err;
}
});
}
modifyOp(id: string, action: "dismiss" | "cancel"): Promise<void> {
return this.retryWithAuth(async () => {
await useServersFetch(
`/ops/${action}?id=${id}`,
{
method: "POST",
override: this.auth,
version: 1,
},
undefined,
`Error ${action === "dismiss" ? "dismissing" : "cancelling"} filesystem operation`,
);
this.opsQueuedForModification = this.opsQueuedForModification.filter((x: string) => x !== id);
this.ops = this.ops.filter((x: FilesystemOp) => x.id !== id);
});
}
removeQueuedOp(op: FSQueuedOp["op"], src: string): void {
this.queuedOps = this.queuedOps.filter((x: FSQueuedOp) => x.op !== op || x.src !== src);
}
clearQueuedOps(): void {
this.queuedOps = [];
}
}

View File

@ -0,0 +1,173 @@
import { $fetch } from "ofetch";
import type { ServerGeneral, Project, PowerAction, JWTAuth } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
export class GeneralModule extends ServerModule implements ServerGeneral {
server_id!: string;
name!: string;
net!: { ip: string; port: number; domain: string };
game!: string;
backup_quota!: number;
used_backup_quota!: number;
status!: string;
suspension_reason!: string;
loader!: string;
loader_version!: string;
mc_version!: string;
upstream!: {
kind: "modpack" | "mod" | "resourcepack";
version_id: string;
project_id: string;
} | null;
motd?: string;
image?: string;
project?: Project;
sftp_username!: string;
sftp_password!: string;
sftp_host!: string;
datacenter?: string;
notices?: any[];
node!: { token: string; instance: string };
flows?: { intro?: boolean };
async fetch(): Promise<void> {
const data = await useServersFetch<ServerGeneral>(`servers/${this.serverId}`, {}, "general");
if (data.upstream?.project_id) {
const project = await $fetch(
`https://api.modrinth.com/v2/project/${data.upstream.project_id}`,
);
data.project = project as Project;
}
if (import.meta.client) {
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined;
}
const motd = await this.getMotd();
if (motd === "A Minecraft Server") {
await this.setMotd(
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
);
}
data.motd = motd;
// Copy data to this module
Object.assign(this, data);
}
async updateName(newName: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/name`, {
method: "POST",
body: { name: newName },
});
}
async power(action: PowerAction): Promise<void> {
await useServersFetch(`servers/${this.serverId}/power`, {
method: "POST",
body: { action },
});
await new Promise((resolve) => setTimeout(resolve, 1000));
await this.fetch(); // Refresh this module
}
async reinstall(
loader: boolean,
projectId: string,
versionId?: string,
loaderVersionId?: string,
hardReset: boolean = false,
): Promise<void> {
const hardResetParam = hardReset ? "true" : "false";
if (loader) {
if (projectId.toLowerCase() === "neoforge") {
projectId = "NeoForge";
}
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
method: "POST",
body: { loader: projectId, loader_version: loaderVersionId, game_version: versionId },
});
} else {
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
method: "POST",
body: { project_id: projectId, version_id: versionId },
});
}
}
async reinstallFromMrpack(mrpack: File, hardReset: boolean = false): Promise<void> {
const hardResetParam = hardReset ? "true" : "false";
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/reinstallFromMrpack`);
const formData = new FormData();
formData.append("file", mrpack);
const response = await fetch(
`https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${auth.token}`,
},
body: formData,
signal: AbortSignal.timeout(30 * 60 * 1000),
},
);
if (!response.ok) {
throw new Error(`[pyroservers] native fetch err status: ${response.status}`);
}
}
async suspend(status: boolean): Promise<void> {
await useServersFetch(`servers/${this.serverId}/suspend`, {
method: "POST",
body: { suspended: status },
});
}
async endIntro(): Promise<void> {
await useServersFetch(`servers/${this.serverId}/flows/intro`, {
method: "DELETE",
version: 1,
});
await this.fetch(); // Refresh this module
}
async getMotd(): Promise<string | undefined> {
try {
const props = await this.server.fs.downloadFile("/server.properties");
if (props) {
const lines = props.split("\n");
for (const line of lines) {
if (line.startsWith("motd=")) {
return line.slice(5);
}
}
}
} catch {
return undefined;
}
return undefined;
}
async setMotd(motd: string): Promise<void> {
const props = (await this.server.fetchConfigFile("ServerProperties")) as any;
if (props) {
props.motd = motd;
const newProps = this.server.constructServerProperties(props);
const octetStream = new Blob([newProps], { type: "application/octet-stream" });
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
await useServersFetch(`/update?path=/server.properties`, {
method: "PUT",
contentType: "application/octet-stream",
body: octetStream,
override: auth,
});
}
}
}

View File

@ -0,0 +1,8 @@
export * from "./base.ts";
export * from "./backups.ts";
export * from "./content.ts";
export * from "./fs.ts";
export * from "./general.ts";
export * from "./network.ts";
export * from "./startup.ts";
export * from "./ws.ts";

View File

@ -0,0 +1,47 @@
import type { Allocation } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
export class NetworkModule extends ServerModule {
allocations: Allocation[] = [];
async fetch(): Promise<void> {
this.allocations = await useServersFetch<Allocation[]>(
`servers/${this.serverId}/allocations`,
{},
"network",
);
}
async reserveAllocation(name: string): Promise<Allocation> {
return await useServersFetch<Allocation>(`servers/${this.serverId}/allocations?name=${name}`, {
method: "POST",
});
}
async updateAllocation(port: number, name: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/allocations/${port}?name=${name}`, {
method: "PUT",
});
}
async deleteAllocation(port: number): Promise<void> {
await useServersFetch(`servers/${this.serverId}/allocations/${port}`, {
method: "DELETE",
});
}
async checkSubdomainAvailability(subdomain: string): Promise<boolean> {
const result = (await useServersFetch(`subdomains/${subdomain}/isavailable`)) as {
available: boolean;
};
return result.available;
}
async changeSubdomain(subdomain: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/subdomain`, {
method: "POST",
body: { subdomain },
});
}
}

View File

@ -0,0 +1,26 @@
import type { Startup, JDKVersion, JDKBuild } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
export class StartupModule extends ServerModule implements Startup {
invocation!: string;
original_invocation!: string;
jdk_version!: JDKVersion;
jdk_build!: JDKBuild;
async fetch(): Promise<void> {
const data = await useServersFetch<Startup>(`servers/${this.serverId}/startup`, {}, "startup");
Object.assign(this, data);
}
async update(invocation: string, jdkVersion: JDKVersion, jdkBuild: JDKBuild): Promise<void> {
await useServersFetch(`servers/${this.serverId}/startup`, {
method: "POST",
body: {
invocation: invocation || null,
jdk_version: jdkVersion || null,
jdk_build: jdkBuild || null,
},
});
}
}

View File

@ -0,0 +1,13 @@
import type { JWTAuth } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
export class WSModule extends ServerModule implements JWTAuth {
url!: string;
token!: string;
async fetch(): Promise<void> {
const data = await useServersFetch<JWTAuth>(`servers/${this.serverId}/ws`, {}, "ws");
Object.assign(this, data);
}
}

View File

@ -0,0 +1,188 @@
import { $fetch, FetchError } from "ofetch";
import { ModrinthServerError, ModrinthServersFetchError } from "@modrinth/utils";
import type { V1ErrorInfo } from "@modrinth/utils";
export interface ServersFetchOptions {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
contentType?: string;
body?: Record<string, any>;
version?: number;
override?: {
url?: string;
token?: string;
};
retry?: number | boolean;
bypassAuth?: boolean;
}
export async function useServersFetch<T>(
path: string,
options: ServersFetchOptions = {},
module?: string,
errorContext?: string,
): Promise<T> {
const config = useRuntimeConfig();
const auth = await useAuth();
const authToken = auth.value?.token;
if (!authToken && !options.bypassAuth) {
const error = new ModrinthServersFetchError(
"[Modrinth Servers] Cannot fetch without auth",
10000,
);
throw new ModrinthServerError("Missing auth token", 401, error, module);
}
const {
method = "GET",
contentType = "application/json",
body,
version = 0,
override,
retry = method === "GET" ? 3 : 0,
} = options;
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
/\/$/,
"",
);
if (!base) {
const error = new ModrinthServersFetchError(
"[Modrinth Servers] Cannot fetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
10001,
);
throw new ModrinthServerError("Configuration error: Missing PYRO_BASE_URL", 500, error, module);
}
const versionString = `v${version}`;
let newOverrideUrl = override?.url;
if (newOverrideUrl && newOverrideUrl.includes("v0") && version !== 0) {
newOverrideUrl = newOverrideUrl.replace("v0", versionString);
}
const fullUrl = newOverrideUrl
? `https://${newOverrideUrl}/${path.replace(/^\//, "")}`
: version === 0
? `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`
: `${base}/v${version}/${path.replace(/^\//, "")}`;
const headers: Record<string, string> = {
"User-Agent": "Modrinth/1.0 (https://modrinth.com)",
Vary: "Accept, Origin",
};
if (!options.bypassAuth) {
headers.Authorization = `Bearer ${override?.token ?? authToken}`;
headers["Access-Control-Allow-Headers"] = "Authorization";
}
if (contentType !== "none") {
headers["Content-Type"] = contentType;
}
if (import.meta.client && typeof window !== "undefined") {
headers.Origin = window.location.origin;
}
let attempts = 0;
const maxAttempts = (typeof retry === "boolean" ? (retry ? 3 : 1) : retry) + 1;
let lastError: Error | null = null;
while (attempts < maxAttempts) {
try {
const response = await $fetch<T>(fullUrl, {
method,
headers,
body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
timeout: 10000,
});
return response;
} catch (error) {
lastError = error as Error;
attempts++;
if (error instanceof FetchError) {
const statusCode = error.response?.status;
const statusText = error.response?.statusText || "Unknown error";
let v1Error: V1ErrorInfo | undefined;
if (error.data?.error && error.data?.description) {
v1Error = {
context: errorContext,
...error.data,
};
}
const errorMessages: { [key: number]: string } = {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
408: "Request Timeout",
429: "Too Many Requests",
500: "Internal Server Error",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
};
const message =
statusCode && statusCode in errorMessages
? errorMessages[statusCode]
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true;
if (!isRetryable || attempts >= maxAttempts) {
console.error("Fetch error:", error);
const fetchError = new ModrinthServersFetchError(
`[Modrinth Servers] ${message}`,
statusCode,
error,
);
throw new ModrinthServerError(error.message, statusCode, fetchError, module, v1Error);
}
const delay = Math.min(1000 * Math.pow(2, attempts - 1) + Math.random() * 1000, 10000);
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
console.error("Unexpected fetch error:", error);
const fetchError = new ModrinthServersFetchError(
"[Modrinth Servers] An unexpected error occurred during the fetch operation.",
undefined,
error as Error,
);
throw new ModrinthServerError(
"Unexpected error during fetch operation",
undefined,
fetchError,
module,
);
}
}
console.error("All retry attempts failed:", lastError);
if (lastError instanceof FetchError) {
const statusCode = lastError.response?.status;
const pyroError = new ModrinthServersFetchError(
"Maximum retry attempts reached",
statusCode,
lastError,
);
throw new ModrinthServerError("Maximum retry attempts reached", statusCode, pyroError, module);
}
const fetchError = new ModrinthServersFetchError(
"Maximum retry attempts reached",
undefined,
lastError || undefined,
);
throw new ModrinthServerError("Maximum retry attempts reached", undefined, fetchError, module);
}

View File

@ -275,7 +275,7 @@ import { useVIntl } from "@vintl/vintl";
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { computed } from "vue";
import { NOTICE_LEVELS } from "@modrinth/ui/src/utils/notices.ts";
import { usePyroFetch } from "~/composables/pyroFetch.ts";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import AssignNoticeModal from "~/components/ui/servers/notice/AssignNoticeModal.vue";
const { formatMessage } = useVIntl();
@ -290,7 +290,7 @@ const assignNoticeModal = ref<InstanceType<typeof AssignNoticeModal>>();
await refreshNotices();
async function refreshNotices() {
await usePyroFetch("notices").then((res) => {
await useServersFetch("notices").then((res) => {
notices.value = res as ServerNoticeType[];
notices.value.sort((a, b) => {
const dateDiff = dayjs(b.announce_at).diff(dayjs(a.announce_at));
@ -347,7 +347,7 @@ function startEditing(notice: ServerNoticeType, assignments: boolean = false) {
}
async function deleteNotice(notice: ServerNoticeType) {
await usePyroFetch(`notices/${notice.id}`, {
await useServersFetch(`notices/${notice.id}`, {
method: "DELETE",
})
.then(() => {
@ -401,7 +401,7 @@ async function saveChanges() {
return;
}
await usePyroFetch(`notices/${editingNotice.value?.id}`, {
await useServersFetch(`notices/${editingNotice.value?.id}`, {
method: "PATCH",
body: {
message: newNoticeMessage.value,
@ -432,7 +432,7 @@ async function createNotice() {
return;
}
await usePyroFetch("notices", {
await useServersFetch("notices", {
method: "POST",
body: {
message: newNoticeMessage.value,

View File

@ -623,6 +623,7 @@ import {
ServerIcon,
} from "@modrinth/assets";
import { products } from "~/generated/state.json";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
import OptionGroup from "~/components/ui/OptionGroup.vue";
@ -674,7 +675,7 @@ const outOfStockUrl = "https://discord.modrinth.com";
const { data: hasServers } = await useAsyncData("ServerListCountCheck", async () => {
try {
if (!auth.value.user) return false;
const response = await usePyroFetch("servers");
const response = await useServersFetch("servers");
return response.servers && response.servers.length > 0;
} catch {
return false;
@ -682,7 +683,7 @@ const { data: hasServers } = await useAsyncData("ServerListCountCheck", async ()
});
function fetchStock(region, request) {
return usePyroFetch(`stock?region=${region.shortcode}`, {
return useServersFetch(`stock?region=${region.shortcode}`, {
method: "POST",
body: {
...request,
@ -702,7 +703,7 @@ async function fetchCapacityStatuses(customProduct = null) {
),
];
const capacityChecks = productsToCheck.map((product) =>
usePyroFetch("stock", {
useServersFetch("stock", {
method: "POST",
body: {
cpu: product.metadata.cpu,
@ -892,7 +893,7 @@ const regions = ref([]);
const regionPings = ref([]);
function pingRegions() {
usePyroFetch("regions", {
useServersFetch("regions", {
method: "GET",
version: 1,
bypassAuth: true,

View File

@ -63,8 +63,8 @@
</div>
<div
v-else-if="
server.general?.error?.error.statusCode === 403 ||
server.general?.error?.error.statusCode === 404
server.moduleErrors?.general?.error.statusCode === 403 ||
server.moduleErrors?.general?.error.statusCode === 404
"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
@ -81,7 +81,7 @@
is an error, please contact Modrinth Support.
</p>
</div>
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
<UiCopyCode :text="JSON.stringify(server.moduleErrors?.general?.error)" />
<ButtonStyled size="large" color="brand" @click="() => router.push('/servers/manage')">
<button class="mt-6 !w-full">Go back to all servers</button>
@ -89,7 +89,7 @@
</div>
</div>
<div
v-else-if="server.general?.error?.error.statusCode === 503"
v-else-if="server.moduleErrors?.general?.error.statusCode === 503"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
@ -141,7 +141,7 @@
</div>
</div>
<div
v-else-if="server.general?.error"
v-else-if="server.moduleErrors?.general?.error"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
@ -162,7 +162,7 @@
temporary network issue. You'll be reconnected automatically.
</p>
</div>
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
<UiCopyCode :text="JSON.stringify(server.moduleErrors?.general?.error)" />
<ButtonStyled
:disabled="formattedTime !== '00'"
size="large"
@ -252,7 +252,7 @@
</h2>
<ServerInstallation
:server="server"
:server="server as ModrinthServer"
:backup-in-progress="backupInProgress"
ignore-current-installation
@reinstall="onReinstall"
@ -419,7 +419,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { ref, computed, onMounted, onUnmounted, watch, type Reactive } from "vue";
import {
SettingsIcon,
CopyIcon,
@ -434,12 +434,19 @@ import {
import DOMPurify from "dompurify";
import { ButtonStyled, ServerNotice } from "@modrinth/ui";
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
import { reloadNuxtApp, navigateTo } from "#app";
import type { MessageDescriptor } from "@vintl/vintl";
import type { ServerState, Stats, WSEvent, WSInstallationResultEvent } from "~/types/servers";
import { usePyroConsole } from "~/store/console.ts";
import { type Backup } from "~/composables/pyroServers.ts";
import { usePyroFetch } from "~/composables/pyroFetch.ts";
import type {
ServerState,
Stats,
WSEvent,
WSInstallationResultEvent,
Backup,
PowerAction,
} from "@modrinth/utils";
import { reloadNuxtApp, navigateTo } from "#app";
import { useModrinthServersConsole } from "~/store/console.ts";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import { ModrinthServer, useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
const app = useNuxtApp() as unknown as { $notify: any };
@ -467,19 +474,19 @@ const route = useNativeRoute();
const router = useRouter();
const serverId = route.params.id as string;
const server = await usePyroServer(serverId, ["general", "ws"]);
const server: Reactive<ModrinthServer> = await useModrinthServers(serverId, ["general", "ws"]);
const loadModulesPromise = Promise.resolve().then(() => {
if (server.general?.status === "suspended") {
return;
}
return server.loadModules(["content", "backups", "network", "startup", "fs"]);
return server.refresh(["content", "backups", "network", "startup", "fs"]);
});
provide("modulesLoaded", loadModulesPromise);
watch(
() => [server.general?.error, server.ws?.error],
() => [server.moduleErrors?.general, server.moduleErrors?.ws],
([generalError, wsError]) => {
if (server.general?.status === "suspended") return;
@ -497,7 +504,7 @@ const errorLogFile = ref("");
const serverData = computed(() => server.general);
const isConnected = ref(false);
const isWSAuthIncorrect = ref(false);
const pyroConsole = usePyroConsole();
const modrinthServersConsole = useModrinthServersConsole();
const cpuData = ref<number[]>([]);
const ramData = ref<number[]>([]);
const isActioning = ref(false);
@ -671,7 +678,7 @@ const connectWebSocket = () => {
return;
}
pyroConsole.clear();
modrinthServersConsole.clear();
socket.value?.send(JSON.stringify({ event: "auth", jwt: wsAuth.value?.token }));
isConnected.value = true;
isReconnecting.value = false;
@ -679,7 +686,7 @@ const connectWebSocket = () => {
if (firstConnect.value) {
for (let i = 0; i < initialConsoleMessage.length; i++) {
pyroConsole.addLine(initialConsoleMessage[i]);
modrinthServersConsole.addLine(initialConsoleMessage[i]);
}
}
@ -702,7 +709,9 @@ const connectWebSocket = () => {
socket.value.onclose = () => {
if (isMounted.value) {
pyroConsole.addLine("\nSomething went wrong with the connection, we're reconnecting...");
modrinthServersConsole.addLine(
"\nSomething went wrong with the connection, we're reconnecting...",
);
isConnected.value = false;
scheduleReconnect();
}
@ -760,7 +769,7 @@ const handleWebSocketMessage = (data: WSEvent) => {
case "log":
// eslint-disable-next-line no-case-declarations
const log = data.message.split("\n").filter((l) => l.trim());
pyroConsole.addLines(log);
modrinthServersConsole.addLines(log);
break;
case "stats":
updateStats(data);
@ -1021,11 +1030,11 @@ const toAdverb = (word: string) => {
return word + "ing";
};
const sendPowerAction = async (action: "restart" | "start" | "stop" | "kill") => {
const sendPowerAction = async (action: PowerAction) => {
const actionName = action.charAt(0).toUpperCase() + action.slice(1);
try {
isActioning.value = true;
await server.general?.power(actionName);
await server.general?.power(action);
} catch (error) {
console.error(`Error ${toAdverb(actionName)} server:`, error);
notifyError(
@ -1158,7 +1167,7 @@ const cleanup = () => {
};
async function dismissNotice(noticeId: number) {
await usePyroFetch(`servers/${serverId}/notices/${noticeId}/dismiss`, {
await useServersFetch(`servers/${serverId}/notices/${noticeId}/dismiss`, {
method: "POST",
}).catch((err) => {
app.$notify({
@ -1177,8 +1186,8 @@ onMounted(() => {
isLoading.value = false;
return;
}
if (server.error) {
if (!server.error.message.includes("Forbidden")) {
if (server.moduleErrors.general?.error) {
if (!server.moduleErrors.general?.error?.message?.includes("Forbidden")) {
startPolling();
}
} else {

View File

@ -1,6 +1,6 @@
<template>
<div
v-if="server.backups?.error"
v-if="server.moduleErrors.backups"
class="flex w-full flex-col items-center justify-center gap-4 p-4"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
@ -15,7 +15,9 @@
We couldn't load your server's backups. Here's what went wrong:
</p>
<p>
<span class="break-all font-mono">{{ JSON.stringify(server.backups.error) }}</span>
<span class="break-all font-mono">{{
JSON.stringify(server.moduleErrors.backups.error)
}}</span>
</p>
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['backups'])">
<button class="mt-6 !w-full">Retry</button>
@ -152,16 +154,17 @@ import { ButtonStyled, TagItem } from "@modrinth/ui";
import { useStorage } from "@vueuse/core";
import { SpinnerIcon, PlusIcon, DownloadIcon, SettingsIcon, IssuesIcon } from "@modrinth/assets";
import { ref, computed } from "vue";
import type { Server } from "~/composables/pyroServers";
import type { Backup } from "@modrinth/utils";
import BackupItem from "~/components/ui/servers/BackupItem.vue";
import BackupRenameModal from "~/components/ui/servers/BackupRenameModal.vue";
import BackupCreateModal from "~/components/ui/servers/BackupCreateModal.vue";
import BackupRestoreModal from "~/components/ui/servers/BackupRestoreModal.vue";
import BackupDeleteModal from "~/components/ui/servers/BackupDeleteModal.vue";
import BackupSettingsModal from "~/components/ui/servers/BackupSettingsModal.vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
isServerRunning: boolean;
}>();

View File

@ -5,12 +5,12 @@
</template>
<script setup lang="ts">
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const route = useNativeRoute();
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const data = computed(() => props.server.general);

View File

@ -11,7 +11,7 @@
/>
<div
v-if="server.content?.error"
v-if="server.moduleErrors.content"
class="flex w-full flex-col items-center justify-center gap-4 p-4"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
@ -24,7 +24,9 @@
</div>
<p class="text-lg text-secondary">
We couldn't load your server's {{ type.toLowerCase() }}s. Here's what we know:
<span class="break-all font-mono">{{ JSON.stringify(server.content.error) }}</span>
<span class="break-all font-mono">{{
JSON.stringify(server.moduleErrors.content.error)
}}</span>
</p>
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['content'])">
<button class="mt-6 !w-full">Retry</button>
@ -349,13 +351,14 @@ import {
} from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import type { Mod } from "@modrinth/utils";
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
import type { Server } from "~/composables/pyroServers";
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const type = computed(() => {

View File

@ -278,16 +278,11 @@ import {
} from "@modrinth/assets";
import { computed } from "vue";
import { ButtonStyled, ProgressBar } from "@modrinth/ui";
import { formatBytes } from "@modrinth/utils";
import {
type DirectoryResponse,
type DirectoryItem,
type Server,
handleError,
} from "~/composables/pyroServers.ts";
import { formatBytes, ModrinthServersFetchError } from "@modrinth/utils";
import type { FilesystemOp, FSQueuedOp, DirectoryItem, DirectoryResponse } from "@modrinth/utils";
import { handleError, ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
import type { FilesystemOp, FSQueuedOp } from "~/types/servers.ts";
import FilesUploadZipUrlModal from "~/components/ui/servers/FilesUploadZipUrlModal.vue";
import FilesUploadConflictModal from "~/components/ui/servers/FilesUploadConflictModal.vue";
@ -316,7 +311,7 @@ interface RenameOperation extends BaseOperation {
type Operation = MoveOperation | RenameOperation;
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const modulesLoaded = inject<Promise<void>>("modulesLoaded");
@ -402,7 +397,7 @@ const fetchDirectoryContents = async (): Promise<DirectoryResponse> => {
};
} catch (error) {
console.error("Error fetching directory contents:", error);
if (error instanceof PyroFetchError && error.statusCode === 400) {
if (error instanceof ModrinthServersFetchError && error.statusCode === 400) {
return directoryData.value || { items: [], total: 0 };
}
throw error;
@ -561,7 +556,7 @@ const handleRenameItem = async (newName: string) => {
});
} catch (error) {
console.error("Error renaming item:", error);
if (error instanceof PyroFetchError) {
if (error instanceof ModrinthServersFetchError) {
if (error.statusCode === 400) {
addNotification({
group: "files",
@ -719,7 +714,7 @@ const showDeleteModal = (item: any) => {
const handleCreateError = (error: any) => {
console.error("Error creating item:", error);
if (error instanceof PyroFetchError) {
if (error instanceof ModrinthServersFetchError) {
if (error.statusCode === 400) {
addNotification({
group: "files",

View File

@ -189,8 +189,8 @@
<script setup lang="ts">
import { TerminalSquareIcon, XIcon, IssuesIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
import type { ServerState, Stats } from "~/types/servers";
import type { Server } from "~/composables/pyroServers";
import type { ServerState, Stats } from "@modrinth/utils";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
type ServerProps = {
socket: WebSocket | null;
@ -203,7 +203,7 @@ type ServerProps = {
exit_code?: number;
};
isServerRunning: boolean;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
};
const props = defineProps<ServerProps>();

View File

@ -17,14 +17,14 @@ import {
UserIcon,
WrenchIcon,
} from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
const route = useRoute();
const serverId = route.params.id as string;
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
backupInProgress?: BackupInProgressReason;
}>();

View File

@ -114,11 +114,10 @@
<script setup lang="ts">
import { EditIcon, TransferIcon } from "@modrinth/assets";
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const data = computed(() => props.server.general);

View File

@ -117,13 +117,13 @@
<script setup lang="ts">
import { ButtonStyled } from "@modrinth/ui";
import { CopyIcon, ExternalIcon, EyeIcon, EyeOffIcon } from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const route = useNativeRoute();
const serverId = route.params.id as string;
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const data = computed(() => props.server.general);

View File

@ -7,12 +7,12 @@
</template>
<script setup lang="ts">
import type { Server } from "~/composables/pyroServers";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
backupInProgress?: BackupInProgressReason;
}>();

View File

@ -60,7 +60,7 @@
<div class="relative h-full w-full overflow-y-auto">
<div
v-if="server.network?.error"
v-if="server.moduleErrors.network"
class="flex w-full flex-col items-center justify-center gap-4 p-4"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
@ -73,7 +73,9 @@
</div>
<p class="text-lg text-secondary">
We couldn't load your server's network settings. Here's what we know:
<span class="break-all font-mono">{{ JSON.stringify(server.network.error) }}</span>
<span class="break-all font-mono">{{
JSON.stringify(server.moduleErrors.network.error)
}}</span>
</p>
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['network'])">
<button class="mt-6 !w-full">Retry</button>
@ -273,10 +275,10 @@ import {
} from "@modrinth/assets";
import { ButtonStyled, NewModal, ConfirmModal } from "@modrinth/ui";
import { ref, computed, nextTick } from "vue";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const isUpdating = ref(false);

View File

@ -43,13 +43,13 @@
<script setup lang="ts">
import { useStorage } from "@vueuse/core";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const route = useNativeRoute();
const serverId = route.params.id as string;
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const preferences = {

View File

@ -1,6 +1,9 @@
<template>
<div class="relative h-full w-full select-none overflow-y-auto">
<div v-if="server.fs?.error" class="flex w-full flex-col items-center justify-center gap-4 p-4">
<div
v-if="server.moduleErrors.fs"
class="flex w-full flex-col items-center justify-center gap-4 p-4"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
<div class="flex flex-col items-center text-center">
<div class="flex flex-col items-center gap-4">
@ -11,7 +14,9 @@
</div>
<p class="text-lg text-secondary">
We couldn't access your server's properties. Here's what we know:
<span class="break-all font-mono">{{ JSON.stringify(server.fs.error) }}</span>
<span class="break-all font-mono">{{
JSON.stringify(server.moduleErrors.fs.error)
}}</span>
</p>
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['fs'])">
<button class="mt-6 !w-full">Retry</button>
@ -141,10 +146,10 @@
import { ref, watch, computed, inject } from "vue";
import { EyeIcon, SearchIcon, IssuesIcon } from "@modrinth/assets";
import Fuse from "fuse.js";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const tags = useTags();

View File

@ -1,7 +1,7 @@
<template>
<div class="relative h-full w-full">
<div
v-if="server.startup?.error"
v-if="server.moduleErrors.startup"
class="flex w-full flex-col items-center justify-center gap-4 p-4"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
@ -16,7 +16,9 @@
We couldn't load your server's startup settings. Here's what we know:
</p>
<p>
<span class="break-all font-mono">{{ JSON.stringify(server.startup.error) }}</span>
<span class="break-all font-mono">{{
JSON.stringify(server.moduleErrors.startup.error)
}}</span>
</p>
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['startup'])">
<button class="mt-6 !w-full">Retry</button>
@ -112,10 +114,10 @@
<script setup lang="ts">
import { UpdatedIcon, IssuesIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
import type { Server } from "~/composables/pyroServers";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
server: ModrinthServer;
}>();
const data = computed(() => props.server.general);

View File

@ -36,7 +36,7 @@
<li v-if="fetchError" class="text-red">
<p>Error details:</p>
<UiCopyCode
:text="(fetchError as PyroFetchError).message || 'Unknown error'"
:text="(fetchError as ModrinthServersFetchError).message || 'Unknown error'"
:copyable="false"
:selectable="false"
:language="'json'"
@ -121,9 +121,9 @@ import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import Fuse from "fuse.js";
import { HammerIcon, PlusIcon, SearchIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
import type { Server, ModrinthServersFetchError } from "@modrinth/utils";
import { reloadNuxtApp } from "#app";
import type { PyroFetchError } from "~/composables/pyroFetch";
import type { Server } from "~/types/servers";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
definePageMeta({
middleware: "auth",
@ -146,7 +146,9 @@ const {
data: serverResponse,
error: fetchError,
refresh,
} = await useAsyncData<ServerResponse>("ServerList", () => usePyroFetch<ServerResponse>("servers"));
} = await useAsyncData<ServerResponse>("ServerList", () =>
useServersFetch<ServerResponse>("servers"),
);
watch([fetchError, serverResponse], ([error, response]) => {
hasError.value = !!error || !response;

View File

@ -592,6 +592,7 @@ import {
} from "@modrinth/assets";
import { calculateSavings, formatPrice, getCurrency } from "@modrinth/utils";
import { ref, computed } from "vue";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import { products } from "~/generated/state.json";
definePageMeta({
@ -744,7 +745,7 @@ const [
useBaseFetch("billing/subscriptions", { internal: true }),
),
useAsyncData("billing/products", () => useBaseFetch("billing/products", { internal: true })),
useAsyncData("servers", () => usePyroFetch("servers")),
useAsyncData("servers", () => useServersFetch("servers")),
]);
const midasProduct = ref(products.find((x) => x.metadata?.type === "midas"));
@ -984,7 +985,7 @@ async function fetchCapacityStatuses(serverId, product) {
if (product) {
try {
return {
custom: await usePyroFetch(`servers/${serverId}/upgrade-stock`, {
custom: await useServersFetch(`servers/${serverId}/upgrade-stock`, {
method: "POST",
body: {
cpu: product.metadata.cpu,

View File

@ -18,7 +18,7 @@ const initialBatchSize = 256;
* @property {function(string): void} addConsoleOutput - Method to add a new console output line
* @property {function(): void} clear - Method to clear all console output
*/
export const usePyroConsole = createGlobalState(() => {
export const useModrinthServersConsole = createGlobalState(() => {
/**
* Reactive array storing console output lines
* @type {Ref<string[]>}

View File

@ -1,284 +0,0 @@
// export interface Mod {
// id: string;
// filename: string;
// modrinth_ids: {
// project_id: string;
// version_id: string;
// };
// }
interface License {
id: string;
name: string;
url: string;
}
interface DonationUrl {
id: string;
platform: string;
url: string;
}
interface GalleryItem {
url: string;
featured: boolean;
title: string;
description: string;
created: string;
ordering: number;
}
export interface Project {
slug: string;
title: string;
description: string;
categories: string[];
client_side: "required" | "optional";
server_side: "required" | "optional";
body: string;
status: "approved" | "pending" | "rejected";
requested_status: "approved" | "pending" | "rejected";
additional_categories: string[];
issues_url: string;
source_url: string;
wiki_url: string;
discord_url: string;
donation_urls: DonationUrl[];
project_type: "mod" | "resourcepack" | "map" | "plugin";
downloads: number;
icon_url: string;
color: number;
thread_id: string;
monetization_status: "monetized" | "non-monetized";
id: string;
team: string;
body_url: string | null;
moderator_message: string | null;
published: string;
updated: string;
approved: string;
queued: string;
followers: number;
license: License;
versions: string[];
game_versions: string[];
loaders: string[];
gallery: GalleryItem[];
}
export interface ServerBackup {
id: string;
name: string;
created_at: string;
}
export interface Allocation {
name: string;
port: number;
}
export interface Server {
server_id: string;
name: string;
status: string;
net: {
ip: string;
port: number;
domain: string;
allocations: Allocation[];
};
game: string;
loader: string | null;
loader_version: string | null;
mc_version: string | null;
backup_quota: number;
used_backup_quota: number;
backups: ServerBackup[];
mods: Mod[];
project: Project | null;
suspension_reason: string | null;
image: string | null;
upstream?: {
kind: "modpack";
project_id: string;
version_id: string;
};
motd: string;
flows: {
intro?: boolean;
};
}
export interface Stats {
current: {
cpu_percent: number;
ram_usage_bytes: number;
ram_total_bytes: number;
storage_usage_bytes: number;
storage_total_bytes: number;
};
past: {
cpu_percent: number;
ram_usage_bytes: number;
ram_total_bytes: number;
storage_usage_bytes: number;
storage_total_bytes: number;
};
graph: {
cpu: number[];
ram: number[];
};
}
export interface WSAuth {
url: string;
token: string;
}
export type ServerState = "running" | "stopped" | "crashed";
// export type WebsocketEventType =
// | "log"
// | "auth"
// | "stats"
// | "power-state"
// | "auth-expiring"
// | "auth-incorrect"
// | "installation-result"
// | (string & {});
// export interface WSEvent {
// event: WebsocketEventType;
// message: string;
// state: ServerState;
// }
export type Loaders =
| "Fabric"
| "Quilt"
| "Forge"
| "NeoForge"
| "Paper"
| "Spigot"
| "Bukkit"
| "Vanilla"
| "Purpur";
export interface WSLogEvent {
event: "log";
message: string;
}
type CurrentStats = Stats["current"];
export interface WSStatsEvent extends CurrentStats {
event: "stats";
}
export interface WSAuthExpiringEvent {
event: "auth-expiring";
}
export interface WSPowerStateEvent {
event: "power-state";
state: ServerState;
// if state "crashed"
oom_killed?: boolean;
exit_code?: number;
}
export interface WSAuthIncorrectEvent {
event: "auth-incorrect";
}
export interface WSInstallationResultOkEvent {
event: "installation-result";
result: "ok";
}
export interface WSInstallationResultErrEvent {
event: "installation-result";
result: "err";
reason: string;
}
export type WSInstallationResultEvent = WSInstallationResultOkEvent | WSInstallationResultErrEvent;
export interface WSAuthOkEvent {
event: "auth-ok";
}
export interface WSUptimeEvent {
event: "uptime";
uptime: number; // seconds
}
export interface WSNewModEvent {
event: "new-mod";
}
export type WSBackupTask = "file" | "create" | "restore";
export type WSBackupState = "ongoing" | "done" | "failed" | "cancelled" | "unchanged";
export interface WSBackupProgressEvent {
event: "backup-progress";
task: WSBackupTask;
id: string;
progress: number; // percentage
state: WSBackupState;
ready: boolean;
}
export type FSQueuedOpUnarchive = {
op: "unarchive";
src: string;
};
export type FSQueuedOp = FSQueuedOpUnarchive;
export type FSOpUnarchive = {
op: "unarchive";
progress: number; // Note: 1 does not mean it's done
id: string; // UUID
mime: string;
src: string;
state:
| "queued"
| "ongoing"
| "cancelled"
| "done"
| "failed-corrupted"
| "failed-invalid-path"
| "failed-cf-no-serverpack"
| "failed-cf-not-available"
| "failed-not-reachable";
current_file: string | null;
failed_path?: string;
bytes_processed: number;
files_processed: number;
started: string;
};
export type FilesystemOp = FSOpUnarchive;
export interface WSFilesystemOpsEvent {
event: "filesystem-ops";
all: FilesystemOp[];
}
export type WSEvent =
| WSLogEvent
| WSStatsEvent
| WSPowerStateEvent
| WSAuthExpiringEvent
| WSAuthIncorrectEvent
| WSInstallationResultEvent
| WSAuthOkEvent
| WSUptimeEvent
| WSNewModEvent
| WSBackupProgressEvent
| WSFilesystemOpsEvent;
export interface Servers {
servers: Server[];
}

View File

@ -46,7 +46,7 @@ export const NOTICE_LEVELS: Record<
},
}
const DISMISSABLE = {
export const DISMISSABLE = {
name: defineMessage({
id: 'servers.notice.dismissable',
defaultMessage: 'Dismissable',
@ -57,7 +57,7 @@ const DISMISSABLE = {
},
}
const UNDISMISSABLE = {
export const UNDISMISSABLE = {
name: defineMessage({
id: 'servers.notice.undismissable',
defaultMessage: 'Undismissable',

View File

@ -7,3 +7,4 @@ export * from './projects'
export * from './types'
export * from './users'
export * from './utils'
export * from './servers'

View File

@ -23,6 +23,7 @@
"dayjs": "^1.11.10",
"highlight.js": "^11.9.0",
"markdown-it": "^14.1.0",
"ofetch": "^1.3.4",
"xss": "^1.0.14"
}
}

View File

@ -0,0 +1,3 @@
export * from './modrinth-servers-multi-error'
export * from './modrinth-servers-fetch-error'
export * from './modrinth-server-error'

View File

@ -0,0 +1,59 @@
import { FetchError } from 'ofetch'
import { V1ErrorInfo } from '../types'
export class ModrinthServerError extends Error {
constructor(
message: string,
public readonly statusCode?: number,
public readonly originalError?: Error,
public readonly module?: string,
public readonly v1Error?: V1ErrorInfo,
) {
let errorMessage = message
let method = 'GET'
let path = ''
if (originalError instanceof FetchError) {
const matches = message.match(/\[([A-Z]+)\]\s+"([^"]+)":/)
if (matches) {
method = matches[1]
path = matches[2].replace(/https?:\/\/[^/]+\/[^/]+\/v\d+\//, '')
}
const statusMessage = (() => {
if (!statusCode) return 'Unknown Error'
switch (statusCode) {
case 400:
return 'Bad Request'
case 401:
return 'Unauthorized'
case 403:
return 'Forbidden'
case 404:
return 'Not Found'
case 408:
return 'Request Timeout'
case 429:
return 'Too Many Requests'
case 500:
return 'Internal Server Error'
case 502:
return 'Bad Gateway'
case 503:
return 'Service Unavailable'
case 504:
return 'Gateway Timeout'
default:
return `HTTP ${statusCode}`
}
})()
errorMessage = `[${method}] ${statusMessage} (${statusCode}) while fetching ${path}${module ? ` in ${module}` : ''}`
} else {
errorMessage = `${message}${statusCode ? ` (${statusCode})` : ''}${module ? ` in ${module}` : ''}`
}
super(errorMessage)
this.name = 'PyroServersFetchError'
}
}

View File

@ -0,0 +1,10 @@
export class ModrinthServersFetchError extends Error {
constructor(
message: string,
public statusCode?: number,
public originalError?: Error,
) {
super(message)
this.name = 'PyroFetchError'
}
}

View File

@ -0,0 +1,27 @@
export class ModrinthServersMultiError extends Error {
public readonly errors: Map<string, Error> = new Map()
public readonly timestamp: number = Date.now()
constructor(message?: string) {
super(message || 'Multiple errors occurred')
this.name = 'MultipleErrors'
}
addError(module: string, error: Error) {
this.errors.set(module, error)
this.message = this.buildErrorMessage()
}
hasErrors() {
return this.errors.size > 0
}
private buildErrorMessage(): string {
return (
Array.from(this.errors.entries())
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(([_module, error]) => error.message)
.join('\n')
)
}
}

View File

@ -0,0 +1,2 @@
export * from './errors'
export * from './types'

View File

@ -0,0 +1,19 @@
import { ModrinthServerError } from '../errors'
export interface V1ErrorInfo {
context?: string
error: string
description: string
}
export interface JWTAuth {
url: string
token: string
}
export interface ModuleError {
error: ModrinthServerError
timestamp: number
}
export type ModuleName = 'general' | 'content' | 'backups' | 'network' | 'startup' | 'ws' | 'fs'

View File

@ -0,0 +1,28 @@
import type { WSBackupTask, WSBackupState } from './websocket'
export interface Backup {
id: string
name: string
created_at: string
locked: boolean
automated: boolean
interrupted: boolean
ongoing: boolean
task: {
[K in WSBackupTask]?: {
progress: number
state: WSBackupState
}
}
}
export interface AutoBackupSettings {
enabled: boolean
interval: number
}
export interface ServerBackup {
id: string
name: string
created_at: string
}

View File

@ -0,0 +1,59 @@
import type { Project } from '../../types'
import { Allocation } from './server'
import { ServerBackup } from './backup'
import { Mod } from './content'
export type ServerNotice = {
id: number
message: string
title?: string
level: 'info' | 'warn' | 'critical' | 'survey'
dismissable: boolean
announce_at: string
expires: string
assigned: {
kind: 'server' | 'node'
id: string
name: string
}[]
dismissed_by: {
server: string
dismissed_on: string
}[]
}
export interface Server {
server_id: string
name: string
status: string
net: {
ip: string
port: number
domain: string
allocations: Allocation[]
}
game: string
loader: string | null
loader_version: string | null
mc_version: string | null
backup_quota: number
used_backup_quota: number
backups: ServerBackup[]
mods: Mod[]
project: Project | null
suspension_reason: string | null
image: string | null
upstream?: {
kind: 'modpack'
project_id: string
version_id: string
}
motd: string
flows: {
intro?: boolean
}
}
export interface Servers {
servers: Server[]
}

View File

@ -0,0 +1,13 @@
export interface Mod {
filename: string
project_id: string | undefined
version_id: string | undefined
name: string | undefined
version_number: string | undefined
icon_url: string | undefined
owner: string | undefined
disabled: boolean
installing: boolean
}
export type ContentType = 'mod' | 'plugin'

View File

@ -0,0 +1,33 @@
import type { FSQueuedOp, FilesystemOp } from './websocket'
import { JWTAuth } from './api'
export interface DirectoryItem {
name: string
type: 'directory' | 'file'
count?: number
modified: number
created: number
path: string
}
export interface FileUploadQuery {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
promise: Promise<any>
onProgress: (
callback: (progress: { loaded: number; total: number; progress: number }) => void,
) => void
cancel: () => void
}
export interface DirectoryResponse {
items: DirectoryItem[]
total: number
current?: number
}
export interface FSModule {
auth: JWTAuth
ops: FilesystemOp[]
queuedOps: FSQueuedOp[]
opsQueuedForModification: string[]
}

View File

@ -0,0 +1,8 @@
export * from './api'
export * from './content'
export * from './server'
export * from './backup'
export * from './filesystem'
export * from './websocket'
export * from './stats'
export * from './common'

View File

@ -0,0 +1,76 @@
import type { Project } from '../../types'
import type { ServerNotice } from './common'
export interface ServerGeneral {
server_id: string
name: string
net: {
ip: string
port: number
domain: string
}
game: string
backup_quota: number
used_backup_quota: number
status: string
suspension_reason: 'moderated' | 'paymentfailed' | 'cancelled' | 'upgrading' | 'other' | string
loader: string
loader_version: string
mc_version: string
upstream: {
kind: 'modpack' | 'mod' | 'resourcepack'
version_id: string
project_id: string
} | null
motd?: string
image?: string
project?: Project
sftp_username: string
sftp_password: string
sftp_host: string
datacenter?: string
notices?: ServerNotice[]
node: {
token: string
instance: string
}
flows?: {
intro?: boolean
}
}
export interface Allocation {
port: number
name: string
}
export interface Startup {
invocation: string
original_invocation: string
jdk_version: 'lts8' | 'lts11' | 'lts17' | 'lts21'
jdk_build: 'corretto' | 'temurin' | 'graal'
}
export type PowerAction = 'Start' | 'Stop' | 'Restart' | 'Kill'
export type JDKVersion = 'lts8' | 'lts11' | 'lts17' | 'lts21'
export type JDKBuild = 'corretto' | 'temurin' | 'graal'
export type Loaders =
| 'Fabric'
| 'Quilt'
| 'Forge'
| 'NeoForge'
| 'Paper'
| 'Spigot'
| 'Bukkit'
| 'Vanilla'
| 'Purpur'
export type ServerState =
| 'starting'
| 'running'
| 'restarting'
| 'stopping'
| 'stopped'
| 'crashed'
| 'unknown'

View File

@ -0,0 +1,20 @@
export interface Stats {
current: {
cpu_percent: number
ram_usage_bytes: number
ram_total_bytes: number
storage_usage_bytes: number
storage_total_bytes: number
}
past: {
cpu_percent: number
ram_usage_bytes: number
ram_total_bytes: number
storage_usage_bytes: number
storage_total_bytes: number
}
graph: {
cpu: number[]
ram: number[]
}
}

View File

@ -0,0 +1,124 @@
import type { Stats } from './stats'
import type { ServerState } from './server'
export interface WSAuth {
url: string
token: string
}
export interface WSLogEvent {
event: 'log'
message: string
}
type CurrentStats = Stats['current']
export interface WSStatsEvent extends CurrentStats {
event: 'stats'
}
export interface WSAuthExpiringEvent {
event: 'auth-expiring'
}
export interface WSPowerStateEvent {
event: 'power-state'
state: ServerState
// if state "crashed"
oom_killed?: boolean
exit_code?: number
}
export interface WSAuthIncorrectEvent {
event: 'auth-incorrect'
}
export interface WSInstallationResultOkEvent {
event: 'installation-result'
result: 'ok'
}
export interface WSInstallationResultErrEvent {
event: 'installation-result'
result: 'err'
reason: string
}
export type WSInstallationResultEvent = WSInstallationResultOkEvent | WSInstallationResultErrEvent
export interface WSAuthOkEvent {
event: 'auth-ok'
}
export interface WSUptimeEvent {
event: 'uptime'
uptime: number // seconds
}
export interface WSNewModEvent {
event: 'new-mod'
}
export type WSBackupTask = 'file' | 'create' | 'restore'
export type WSBackupState = 'ongoing' | 'done' | 'failed' | 'cancelled' | 'unchanged'
export interface WSBackupProgressEvent {
event: 'backup-progress'
task: WSBackupTask
id: string
progress: number // percentage
state: WSBackupState
ready: boolean
}
export type FSQueuedOpUnarchive = {
op: 'unarchive'
src: string
}
export type FSQueuedOp = FSQueuedOpUnarchive
export type FSOpUnarchive = {
op: 'unarchive'
progress: number // Note: 1 does not mean it's done
id: string // UUID
mime: string
src: string
state:
| 'queued'
| 'ongoing'
| 'cancelled'
| 'done'
| 'failed-corrupted'
| 'failed-invalid-path'
| 'failed-cf-no-serverpack'
| 'failed-cf-not-available'
| 'failed-not-reachable'
current_file: string | null
failed_path?: string
bytes_processed: number
files_processed: number
started: string
}
export type FilesystemOp = FSOpUnarchive
export interface WSFilesystemOpsEvent {
event: 'filesystem-ops'
all: FilesystemOp[]
}
export type WSEvent =
| WSLogEvent
| WSStatsEvent
| WSPowerStateEvent
| WSAuthExpiringEvent
| WSAuthIncorrectEvent
| WSInstallationResultEvent
| WSAuthOkEvent
| WSUptimeEvent
| WSNewModEvent
| WSBackupProgressEvent
| WSFilesystemOpsEvent

View File

@ -294,22 +294,3 @@ export type Report = {
created: string
body: string
}
export type ServerNotice = {
id: number
message: string
title?: string
level: 'info' | 'warn' | 'critical' | 'survey'
dismissable: boolean
announce_at: string
expires: string
assigned: {
kind: 'server' | 'node'
id: string
name: string
}[]
dismissed_by: {
server: string
dismissed_on: string
}[]
}

31
pnpm-lock.yaml generated
View File

@ -520,6 +520,9 @@ importers:
markdown-it:
specifier: ^14.1.0
version: 14.1.0
ofetch:
specifier: ^1.3.4
version: 1.4.1
xss:
specifier: ^1.0.14
version: 1.0.15
@ -9456,7 +9459,7 @@ snapshots:
'@nuxtjs/eslint-config-typescript@12.1.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)':
dependencies:
'@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
'@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)
'@typescript-eslint/parser': 6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)
eslint: 9.13.0(jiti@2.4.1)
@ -9469,10 +9472,10 @@ snapshots:
- supports-color
- typescript
'@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))':
'@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))':
dependencies:
eslint: 9.13.0(jiti@2.4.1)
eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-n: 15.7.0(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-node: 11.1.0(eslint@9.13.0(jiti@2.4.1))
@ -12075,10 +12078,10 @@ snapshots:
dependencies:
eslint: 9.13.0(jiti@2.4.1)
eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1)):
eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1)):
dependencies:
eslint: 9.13.0(jiti@2.4.1)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-n: 15.7.0(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-promise: 6.4.0(eslint@9.13.0(jiti@2.4.1))
@ -12104,7 +12107,7 @@ snapshots:
debug: 4.4.0(supports-color@9.4.0)
enhanced-resolve: 5.17.1
eslint: 9.13.0(jiti@2.4.1)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint@9.13.0(jiti@2.4.1))
fast-glob: 3.3.2
get-tsconfig: 4.7.5
@ -12116,7 +12119,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1)):
eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
@ -12127,6 +12130,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.13.0(jiti@2.4.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)
eslint: 9.13.0(jiti@2.4.1)
eslint-import-resolver-node: 0.3.9
transitivePeerDependencies:
- supports-color
eslint-plugin-es@3.0.1(eslint@9.13.0(jiti@2.4.1)):
dependencies:
eslint: 9.13.0(jiti@2.4.1)
@ -12166,7 +12179,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.13.0(jiti@2.4.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.13.0(jiti@2.4.1))
hasown: 2.0.2
is-core-module: 2.15.0
is-glob: 4.0.3
@ -12193,7 +12206,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.13.0(jiti@2.4.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.13.0(jiti@2.4.1))
hasown: 2.0.2
is-core-module: 2.15.0
is-glob: 4.0.3