mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 10:34:53 +00:00
feat: hosting access tab (#5995)
* feat: implement access tab with dummy data * fix: spacing * feat: qa * feat: implement backend * qa: qa pass * feat: fix user "search" * fix: lint * feat: change to bitfield * feat: fix fields * fix: lint * fix: lint * feat: hook up api * feat: fix permissions * feat: audit log table event start * feat: better mobile mode for audit log table * feat: i18n * feat: qa * feat: enforce permissions * feat: email template start * feat: qa * fix: tooltip bug * feat: qa * impl: sse support in api-client * feat: sse impl * fix: desync path * feat: time frame picker from analytics * feat: QA * fix: spacing * fix: permisison audit log entries * fix: hosting manage page shared server detection * fix: lint * feat: qa + lint * feat: audit log table sort by time * feat: finish frontend panel stuff * fix: lint * fix: backend alignment * fix: lint * fix: supress friend errors * feat: qa * fix: qa * fix: lint * fix: utils barrel * fix: safari cookies in dev * fix: pin nuxt * feat: fixes + notif fix * fix: notifications * feat: qa * fix: notification sync not happening immediately * fix: qa * fix: qa * feat: qa * blog + prepr * feat: toast shit * blog images * thumbnail update one last time * prepr * feat: use reinvite route * update images * fix: reinvite stuff * fix: lint * fix: alignment of save bar * fix: notif sizing * fix: split up access * fix: lint * fix: lint * fix: link --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -176,6 +176,17 @@ export const WithSubLabel: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const LongSelectedLabel: Story = {
|
||||
args: {
|
||||
modelValue: 'long',
|
||||
options: [
|
||||
{ value: 'short', label: 'Short label' },
|
||||
{ value: 'long', label: 'A much longer selected value that should truncate cleanly' },
|
||||
],
|
||||
triggerClass: 'w-64',
|
||||
},
|
||||
}
|
||||
|
||||
export const MixedSubLabels: Story = {
|
||||
args: {
|
||||
options: [
|
||||
@@ -203,6 +214,35 @@ export const SearchableNoFilter: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const SearchableMinimumLength: Story = {
|
||||
args: {
|
||||
options: [
|
||||
{ value: 'fetch', label: 'Fetch' },
|
||||
{ value: 'emma', label: 'Emma' },
|
||||
{ value: 'boris', label: 'Boris' },
|
||||
{ value: 'coolbot', label: 'Coolbot' },
|
||||
],
|
||||
searchable: true,
|
||||
searchPlaceholder: 'Enter a username or email',
|
||||
minSearchLengthToOpen: 2,
|
||||
noOptionsMessage: 'No matching users found.',
|
||||
},
|
||||
}
|
||||
|
||||
export const SearchableModpacks: Story = {
|
||||
args: {
|
||||
options: [
|
||||
{ value: 'download', label: 'Download', icon: DownloadIcon },
|
||||
{ value: 'share', label: 'Share', icon: ShareIcon },
|
||||
{ value: 'favorite', label: 'Add to favorites', icon: HeartIcon },
|
||||
{ value: 'settings', label: 'Settings', icon: SettingsIcon },
|
||||
],
|
||||
searchable: true,
|
||||
searchPlaceholder: 'Search modpacks...',
|
||||
noOptionsMessage: 'No modpacks found',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithDropdownFooter: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Combobox, EyeIcon, EyeOffIcon },
|
||||
|
||||
@@ -114,6 +114,37 @@ export const Range: Story = {
|
||||
}),
|
||||
}
|
||||
|
||||
export const RangeWithTime: Story = {
|
||||
render: () => ({
|
||||
components: { DatePicker },
|
||||
setup() {
|
||||
const value = ref(['2026-04-13 09:00', '2026-04-29 17:30'])
|
||||
const datePicker = ref<InstanceType<typeof DatePicker> | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
datePicker.value?.open()
|
||||
})
|
||||
|
||||
return { datePicker, value }
|
||||
},
|
||||
template: /* html */ `
|
||||
<div class="flex h-[500px] max-w-sm flex-col gap-2">
|
||||
<DatePicker
|
||||
ref="datePicker"
|
||||
v-model="value"
|
||||
wrapperClass="w-[350px]"
|
||||
mode="range"
|
||||
enable-time
|
||||
default-view-date="2026-04-01"
|
||||
placeholder="Select a date and time range..."
|
||||
/>
|
||||
<p class="text-sm text-secondary">Selected value: {{ value?.join(' to ') || 'None' }}</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const SameDayRange: Story = {
|
||||
render: () => ({
|
||||
components: { DatePicker },
|
||||
|
||||
@@ -183,6 +183,29 @@ export const WithAppliedFilters: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const ImmediateApply: Story = {
|
||||
render: () => ({
|
||||
components: { DropdownFilterBar },
|
||||
setup() {
|
||||
const selected = ref<Record<string, string[]>>({})
|
||||
return { categories: defaultCategories, selected }
|
||||
},
|
||||
template: /* html */ `
|
||||
<div class="flex flex-col items-start gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<DropdownFilterBar v-model="selected" :categories="categories" apply-immediately />
|
||||
</div>
|
||||
<pre class="m-0 rounded-lg bg-surface-2 px-3 py-2 text-sm text-secondary">{{ selected }}</pre>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
modelValue: {},
|
||||
categories: defaultCategories,
|
||||
applyImmediately: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithRightCheckmarks: Story = {
|
||||
render: () => ({
|
||||
components: { DropdownFilterBar },
|
||||
|
||||
@@ -199,6 +199,13 @@ export const WithSelectionIds: StoryObj = {
|
||||
|
||||
export const WithSorting: StoryObj = {
|
||||
args: {},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Sortable header hover and click targets are scoped to the label and sort icon.',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: () => ({
|
||||
components: { Table },
|
||||
setup() {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { MinecraftServerIcon } from '@modrinth/assets'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import { NotificationToast } from '../../components/notifications'
|
||||
|
||||
const avatarUrl =
|
||||
'https://cdn.modrinth.com/user/6Qo4A5QT/9d81be1a9fb1afd163b7f2f05a791955e7693c90.png'
|
||||
|
||||
const meta = {
|
||||
title: 'Notifications/Toasts',
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template:
|
||||
'<div class="dark-mode flex w-[436px] flex-col gap-4 bg-surface-1 p-2"><story /></div>',
|
||||
}),
|
||||
],
|
||||
} satisfies Meta
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const noop = () => console.log('Notification action')
|
||||
|
||||
export const FigmaExamples: Story = {
|
||||
render: () => ({
|
||||
components: { NotificationToast },
|
||||
setup() {
|
||||
return {
|
||||
avatarUrl,
|
||||
instanceIconUrl: MinecraftServerIcon,
|
||||
noop,
|
||||
}
|
||||
},
|
||||
template: /* html */ `
|
||||
<div class="flex flex-col gap-4">
|
||||
<NotificationToast
|
||||
type="friend-request"
|
||||
actor-name="Fetch"
|
||||
:actor-avatar-url="avatarUrl"
|
||||
@accept="noop"
|
||||
@decline="noop"
|
||||
@dismiss="noop"
|
||||
/>
|
||||
<NotificationToast
|
||||
type="server-invite"
|
||||
actor-name="Fetch"
|
||||
:actor-avatar-url="avatarUrl"
|
||||
entity-name="Modrinth SMP"
|
||||
@accept="noop"
|
||||
@decline="noop"
|
||||
@dismiss="noop"
|
||||
@open-actor="noop"
|
||||
/>
|
||||
<NotificationToast
|
||||
type="instance-invite"
|
||||
actor-name="Fetch"
|
||||
:actor-avatar-url="avatarUrl"
|
||||
entity-name="New Creation"
|
||||
:entity-icon-url="instanceIconUrl"
|
||||
@accept="noop"
|
||||
@decline="noop"
|
||||
@dismiss="noop"
|
||||
@open-actor="noop"
|
||||
/>
|
||||
<NotificationToast
|
||||
type="instance-download"
|
||||
entity-name="New Creation"
|
||||
:entity-icon-url="instanceIconUrl"
|
||||
status-text="Downloading Minecraft..."
|
||||
:progress="0.5"
|
||||
@dismiss="noop"
|
||||
/>
|
||||
<NotificationToast
|
||||
type="instance-ready"
|
||||
entity-name="New Creation"
|
||||
:entity-icon-url="instanceIconUrl"
|
||||
:progress="0.75"
|
||||
@launch="noop"
|
||||
@open-instance="noop"
|
||||
@dismiss="noop"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const ServerInvite: Story = {
|
||||
render: () => ({
|
||||
components: { NotificationToast },
|
||||
setup() {
|
||||
return {
|
||||
avatarUrl,
|
||||
instanceIconUrl: MinecraftServerIcon,
|
||||
noop,
|
||||
}
|
||||
},
|
||||
template: /* html */ `
|
||||
<NotificationToast
|
||||
type="server-invite"
|
||||
actor-name="Fetch"
|
||||
:actor-avatar-url="avatarUrl"
|
||||
entity-name="Modrinth SMP"
|
||||
@accept="noop"
|
||||
@decline="noop"
|
||||
@dismiss="noop"
|
||||
@open-actor="noop"
|
||||
/>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const MissingAvatarFallback: Story = {
|
||||
render: () => ({
|
||||
components: { NotificationToast },
|
||||
setup() {
|
||||
return {
|
||||
instanceIconUrl: MinecraftServerIcon,
|
||||
noop,
|
||||
}
|
||||
},
|
||||
template: /* html */ `
|
||||
<NotificationToast
|
||||
type="server-invite"
|
||||
actor-name="Fetch"
|
||||
entity-name="Modrinth SMP"
|
||||
@accept="noop"
|
||||
@decline="noop"
|
||||
@dismiss="noop"
|
||||
@open-actor="noop"
|
||||
/>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WaitingDownload: Story = {
|
||||
render: () => ({
|
||||
components: { NotificationToast },
|
||||
setup() {
|
||||
return {
|
||||
instanceIconUrl: MinecraftServerIcon,
|
||||
noop,
|
||||
}
|
||||
},
|
||||
template: /* html */ `
|
||||
<NotificationToast
|
||||
type="instance-download"
|
||||
entity-name="New Creation"
|
||||
:entity-icon-url="instanceIconUrl"
|
||||
status-text="Preparing files..."
|
||||
waiting
|
||||
@dismiss="noop"
|
||||
/>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import AccessTable from '../../components/servers/access/AccessTable.vue'
|
||||
import type {
|
||||
ServerAccessMember,
|
||||
ServerAccessRole,
|
||||
ServerAccessRoleOption,
|
||||
} from '../../components/servers/access/types'
|
||||
|
||||
const roleOptions: ServerAccessRoleOption[] = [
|
||||
{
|
||||
value: 'owner',
|
||||
label: 'Owner',
|
||||
description: 'Full access including billing, members, and destructive actions.',
|
||||
},
|
||||
{
|
||||
value: 'editor',
|
||||
label: 'Editor',
|
||||
description: 'Manage instance content, files, backups, and other settings.',
|
||||
},
|
||||
{
|
||||
value: 'viewer',
|
||||
label: 'Limited',
|
||||
description: 'Start, stop, and view the server without making changes.',
|
||||
},
|
||||
]
|
||||
|
||||
const members: ServerAccessMember[] = [
|
||||
{
|
||||
id: 'owner',
|
||||
user: { id: 'prospector', username: 'Prospector' },
|
||||
role: 'owner',
|
||||
joinedAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
isOwner: true,
|
||||
},
|
||||
{
|
||||
id: 'editor',
|
||||
user: { id: 'geometrically', username: 'Geometrically' },
|
||||
role: 'editor',
|
||||
joinedAt: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'pending',
|
||||
user: { id: 'imb', username: 'IMB' },
|
||||
role: 'viewer',
|
||||
joinedAt: null,
|
||||
pending: true,
|
||||
},
|
||||
]
|
||||
|
||||
const meta = {
|
||||
title: 'Servers/AccessTable',
|
||||
component: AccessTable,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
},
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template: '<div style="max-width: 960px;"><story /></div>',
|
||||
}),
|
||||
],
|
||||
} satisfies Meta<typeof AccessTable>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { AccessTable },
|
||||
setup() {
|
||||
const rows = ref([...members])
|
||||
function updateRole(member: ServerAccessMember, role: ServerAccessRole) {
|
||||
rows.value = rows.value.map((row) => (row.id === member.id ? { ...row, role } : row))
|
||||
}
|
||||
function removeMember(member: ServerAccessMember) {
|
||||
rows.value = rows.value.filter((row) => row.id !== member.id)
|
||||
}
|
||||
return { rows, roleOptions, updateRole, removeMember }
|
||||
},
|
||||
template: /* html */ `
|
||||
<AccessTable
|
||||
:members="rows"
|
||||
:roles="roleOptions"
|
||||
@update-role="updateRole"
|
||||
@resend-invite="() => {}"
|
||||
@cancel-invite="removeMember"
|
||||
@remove-member="removeMember"
|
||||
/>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const PendingInvite: Story = {
|
||||
args: {
|
||||
members: [members[2]],
|
||||
roles: roleOptions,
|
||||
},
|
||||
}
|
||||
|
||||
export const PendingInviteCooldown: Story = {
|
||||
args: {
|
||||
members: [
|
||||
{
|
||||
...members[2],
|
||||
inviteResendAvailableAt: new Date(Date.now() + 45 * 1000).toISOString(),
|
||||
},
|
||||
],
|
||||
roles: roleOptions,
|
||||
},
|
||||
}
|
||||
|
||||
export const OwnerFixed: Story = {
|
||||
args: {
|
||||
members: [members[0]],
|
||||
roles: roleOptions,
|
||||
},
|
||||
}
|
||||
|
||||
export const MobileCompact: Story = {
|
||||
render: () => ({
|
||||
components: { AccessTable },
|
||||
setup() {
|
||||
const rows = ref([...members])
|
||||
function updateRole(member: ServerAccessMember, role: ServerAccessRole) {
|
||||
rows.value = rows.value.map((row) => (row.id === member.id ? { ...row, role } : row))
|
||||
}
|
||||
function removeMember(member: ServerAccessMember) {
|
||||
rows.value = rows.value.filter((row) => row.id !== member.id)
|
||||
}
|
||||
return { rows, roleOptions, updateRole, removeMember }
|
||||
},
|
||||
template: /* html */ `
|
||||
<div style="max-width: 390px;">
|
||||
<AccessTable
|
||||
:members="rows"
|
||||
:roles="roleOptions"
|
||||
@update-role="updateRole"
|
||||
@resend-invite="() => {}"
|
||||
@cancel-invite="removeMember"
|
||||
@remove-member="removeMember"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,929 @@
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import DropdownFilterBar from '../../components/base/DropdownFilterBar.vue'
|
||||
import type {
|
||||
TimeFrameLastUnit,
|
||||
TimeFrameMode,
|
||||
TimeFramePreset,
|
||||
} from '../../components/base/TimeFramePicker.vue'
|
||||
import AuditLogTable from '../../components/servers/access/AuditLogTable.vue'
|
||||
import { type AuditEventLookups, parseAuditEvent } from '../../components/servers/access/events'
|
||||
import type {
|
||||
ServerAuditLogEntry,
|
||||
ServerAuditLogFilters,
|
||||
} from '../../components/servers/access/types'
|
||||
import auditLogEntryExamplesJson from './audit-log-entry-examples.json?raw'
|
||||
|
||||
const serverId = 'story-server'
|
||||
const createWorldId = 'create-smp'
|
||||
const activeWorldId = 'smp-season-4'
|
||||
const backupId = '00000000-0000-4000-8000-000000000001'
|
||||
|
||||
type AuditLogEntryExamplesFixture = Archon.Actions.v1.ActionLogResponse & {
|
||||
server_id: string
|
||||
server: {
|
||||
id: string
|
||||
name: string
|
||||
subdomain: string
|
||||
}
|
||||
worlds: Array<{
|
||||
id: string
|
||||
name: string
|
||||
}>
|
||||
backups: Archon.Backups.v1.Backup[]
|
||||
}
|
||||
|
||||
const auditLogEntryExamples = JSON.parse(auditLogEntryExamplesJson) as AuditLogEntryExamplesFixture
|
||||
|
||||
const userIds = {
|
||||
geometrically: 'MpxzqsyW',
|
||||
modmuss: 'JZA4dW8o',
|
||||
prospector: 'Dc7EYhxG',
|
||||
}
|
||||
|
||||
const filterBarCategories = [
|
||||
{
|
||||
key: 'users',
|
||||
label: 'Users',
|
||||
searchable: true,
|
||||
searchPlaceholder: 'Search users...',
|
||||
options: [
|
||||
{ value: userIds.geometrically, label: 'Geometrically' },
|
||||
{ value: userIds.modmuss, label: 'modmuss50' },
|
||||
{ value: userIds.prospector, label: 'Prospector' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'worlds',
|
||||
label: 'Instances',
|
||||
options: [
|
||||
{ value: createWorldId, label: 'Create SMP' },
|
||||
{ value: activeWorldId, label: 'SMP Season 4' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: 'Actions',
|
||||
options: [
|
||||
{ value: 'server_started', label: 'Started server' },
|
||||
{ value: 'console_command_executed', label: 'Ran console command' },
|
||||
{ value: 'backup_restored', label: 'Restored backup' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const serverFullResponse: Archon.Servers.v1.ServerFull = {
|
||||
id: serverId,
|
||||
name: 'Stoneblock Lab',
|
||||
subdomain: 'stoneblock-lab',
|
||||
specs: {
|
||||
cpu: 4,
|
||||
memory_mb: 8192,
|
||||
storage_mb: 65536,
|
||||
swap_mb: 1024,
|
||||
},
|
||||
sftp_username: 'story-server',
|
||||
sftp_password: 'temporary-password',
|
||||
tags: ['medal'],
|
||||
location: {
|
||||
status: 'assigned',
|
||||
location_metadata: {
|
||||
region: 'us-east',
|
||||
region_should_be_user_displayed: true,
|
||||
hostname: 'ashburn-01',
|
||||
is_decommissioned_node: false,
|
||||
},
|
||||
},
|
||||
worlds: [
|
||||
{
|
||||
id: createWorldId,
|
||||
name: 'Create SMP',
|
||||
created_at: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
is_active: false,
|
||||
backups: [],
|
||||
content: {
|
||||
modloader: 'fabric',
|
||||
modloader_version: '0.16.10',
|
||||
game_version: '1.21.1',
|
||||
java_version: 21,
|
||||
invocation: 'java -Xmx8G -jar server.jar nogui',
|
||||
original_invocation: 'java -jar server.jar nogui',
|
||||
},
|
||||
readiness: {
|
||||
data_synchronized_fetched: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: activeWorldId,
|
||||
name: 'SMP Season 4',
|
||||
created_at: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
is_active: true,
|
||||
backups: [
|
||||
{
|
||||
id: backupId,
|
||||
physical_id: backupId,
|
||||
name: 'Before modpack update',
|
||||
created_at: new Date(Date.now() - 8 * 60 * 60 * 1000).toISOString(),
|
||||
automated: false,
|
||||
status: 'done',
|
||||
interrupted: false,
|
||||
ongoing: false,
|
||||
locked: false,
|
||||
},
|
||||
],
|
||||
content: {
|
||||
modloader: 'fabric',
|
||||
modloader_version: '0.16.10',
|
||||
game_version: '1.21.1',
|
||||
java_version: 21,
|
||||
invocation: 'java -Xmx8G -jar server.jar nogui',
|
||||
original_invocation: 'java -jar server.jar nogui',
|
||||
},
|
||||
readiness: {
|
||||
data_synchronized_fetched: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const auditLogEntryExamplesServerFullResponse: Archon.Servers.v1.ServerFull = {
|
||||
...serverFullResponse,
|
||||
id: auditLogEntryExamples.server_id,
|
||||
name: auditLogEntryExamples.server.name,
|
||||
subdomain: auditLogEntryExamples.server.subdomain,
|
||||
worlds: auditLogEntryExamples.worlds.map((world, index) => ({
|
||||
...serverFullResponse.worlds[Math.min(index, serverFullResponse.worlds.length - 1)],
|
||||
id: world.id,
|
||||
name: world.name,
|
||||
backups: index === 0 ? auditLogEntryExamples.backups : [],
|
||||
})),
|
||||
}
|
||||
|
||||
const actionLogResponse: Archon.Actions.v1.ActionLogResponse = {
|
||||
next_offset: null,
|
||||
users: {
|
||||
[userIds.geometrically]: {
|
||||
username: 'Geometrically',
|
||||
avatar_url:
|
||||
'https://cdn.modrinth.com/data/MpxzqsyW/eb0038489a55e7e7a188a5b50462f0b10dfc1613_96.webp',
|
||||
},
|
||||
[userIds.modmuss]: {
|
||||
username: 'modmuss50',
|
||||
avatar_url: 'https://avatars2.githubusercontent.com/u/4324090?v=4',
|
||||
},
|
||||
[userIds.prospector]: {
|
||||
username: 'Prospector',
|
||||
avatar_url:
|
||||
'https://cdn.modrinth.com/user/Dc7EYhxG/32e8b1f7d18288262d1ed92cbdf43272d21b4fcd.png',
|
||||
},
|
||||
},
|
||||
addons: {
|
||||
oWaK0Q19: {
|
||||
title: 'Create Aeronautics',
|
||||
slug: 'create-aeronautics',
|
||||
icon_url:
|
||||
'https://cdn.modrinth.com/data/oWaK0Q19/f66b5589924884ffd81acb27f3ccb775867a962e_96.webp',
|
||||
version: null,
|
||||
},
|
||||
AANobbMI: {
|
||||
title: 'Sodium',
|
||||
slug: 'sodium',
|
||||
icon_url:
|
||||
'https://cdn.modrinth.com/data/AANobbMI/295862f4724dc3f78df3447ad6072b2dcd3ef0c9_96.webp',
|
||||
version: null,
|
||||
},
|
||||
P7dR8mSH: {
|
||||
title: 'Fabric API',
|
||||
slug: 'fabric-api',
|
||||
icon_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png',
|
||||
version: null,
|
||||
},
|
||||
'project-modpack-001': {
|
||||
title: 'Vault Hunters',
|
||||
slug: 'vault-hunters',
|
||||
icon_url: null,
|
||||
version: null,
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
HY8u0JqC: { name: 'Create Aeronautics 1.0.0', version_number: '1.0.0' },
|
||||
yaoBL9D9: { name: 'Sodium 0.6.0', version_number: '0.6.0' },
|
||||
KZS9tylY: { name: 'Fabric API 0.116.0', version_number: '0.116.0' },
|
||||
'version-modpack-001': { name: 'Vault Hunters 3.15.1', version_number: '3.15.1' },
|
||||
},
|
||||
data: [
|
||||
rawEntry({ action: 'server_created', worldId: null, minutesAgo: 5 }),
|
||||
rawEntry({
|
||||
action: 'changed_server_name',
|
||||
metadata: { name: 'Stoneblock Lab' },
|
||||
worldId: null,
|
||||
minutesAgo: 10,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'changed_server_subdomain',
|
||||
metadata: { subdomain: 'stoneblock-lab' },
|
||||
worldId: null,
|
||||
minutesAgo: 15,
|
||||
}),
|
||||
rawEntry({ action: 'server_reallocated', worldId: null, minutesAgo: 20 }),
|
||||
rawEntry({
|
||||
action: 'server_plan_changed',
|
||||
metadata: { new_specs: { cpu: 4, memory_mb: 8192, storage_mb: 65536, swap_mb: 1024 } },
|
||||
worldId: null,
|
||||
minutesAgo: 25,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'user_invited',
|
||||
metadata: { user_id: userIds.modmuss, permissions: -4611686018427387904 },
|
||||
worldId: null,
|
||||
minutesAgo: 30,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'user_invite_revoked',
|
||||
metadata: { user_id: userIds.modmuss },
|
||||
worldId: null,
|
||||
minutesAgo: 35,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'user_permission_modified',
|
||||
metadata: {
|
||||
user_id: userIds.modmuss,
|
||||
permissions: -288230376151711744,
|
||||
},
|
||||
worldId: null,
|
||||
minutesAgo: 40,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'user_removed',
|
||||
metadata: { user_id: userIds.modmuss },
|
||||
worldId: null,
|
||||
minutesAgo: 45,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'addon_added',
|
||||
metadata: { addons: [{ addon_id: 'oWaK0Q19', version_id: 'HY8u0JqC' }] },
|
||||
minutesAgo: 50,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'addon_uploaded',
|
||||
metadata: { file_names: ['custom-tweaks.jar', 'server-rules.datapack.zip'] },
|
||||
minutesAgo: 55,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'addon_disabled',
|
||||
metadata: { addons: [{ addon_id: 'AANobbMI', version_id: 'yaoBL9D9' }] },
|
||||
minutesAgo: 60,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'addon_enabled',
|
||||
metadata: { addons: [{ addon_id: 'AANobbMI', version_id: 'yaoBL9D9' }] },
|
||||
minutesAgo: 65,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'addon_deleted',
|
||||
metadata: { addons: [{ addon_id: 'P7dR8mSH', version_id: 'KZS9tylY' }] },
|
||||
minutesAgo: 70,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'addon_updated',
|
||||
metadata: {
|
||||
addons: [
|
||||
{ addon_id: 'AANobbMI', version_id: 'YAGZ1cCS' },
|
||||
{ addon_id: 'P7dR8mSH', version_id: 'EW33COvi' },
|
||||
],
|
||||
},
|
||||
minutesAgo: 75,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'modpack_changed',
|
||||
metadata: {
|
||||
spec: {
|
||||
platform: 'modrinth',
|
||||
project_id: 'project-modpack-001',
|
||||
version_id: 'version-modpack-001',
|
||||
},
|
||||
},
|
||||
minutesAgo: 80,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'modpack_unlinked',
|
||||
metadata: {
|
||||
spec: {
|
||||
platform: 'modrinth',
|
||||
project_id: 'project-modpack-001',
|
||||
version_id: 'version-modpack-001',
|
||||
},
|
||||
},
|
||||
minutesAgo: 85,
|
||||
}),
|
||||
rawEntry({ action: 'server_repaired', minutesAgo: 90 }),
|
||||
rawEntry({ action: 'server_reset', minutesAgo: 95 }),
|
||||
rawEntry({ action: 'server_started', minutesAgo: 100 }),
|
||||
rawEntry({ action: 'server_stopped', minutesAgo: 105 }),
|
||||
rawEntry({ action: 'server_restarted', minutesAgo: 110 }),
|
||||
rawEntry({ action: 'server_killed', minutesAgo: 115 }),
|
||||
rawEntry({
|
||||
action: 'port_allocation_added',
|
||||
metadata: { port: 25565 },
|
||||
worldId: null,
|
||||
minutesAgo: 120,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'port_allocation_removed',
|
||||
metadata: { port: 24454 },
|
||||
worldId: null,
|
||||
minutesAgo: 125,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'loader_version_edited',
|
||||
metadata: { new_loader: 'fabric', new_version: '0.16.10' },
|
||||
minutesAgo: 130,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'game_version_edited',
|
||||
metadata: { new_version: '1.21.1' },
|
||||
minutesAgo: 135,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'server_properties_modified',
|
||||
metadata: { properties: { difficulty: 'hard', 'max-players': '20' } },
|
||||
minutesAgo: 140,
|
||||
}),
|
||||
rawEntry({ action: 'file_uploaded', metadata: { path: '/mods/custom.jar' }, minutesAgo: 145 }),
|
||||
rawEntry({ action: 'file_deleted', metadata: { path: '/config/old.toml' }, minutesAgo: 150 }),
|
||||
rawEntry({
|
||||
action: 'file_renamed',
|
||||
metadata: { from: '/world/old.dat', to: '/world/new.dat' },
|
||||
minutesAgo: 155,
|
||||
}),
|
||||
rawEntry({ action: 'file_edited', metadata: { path: '/server.properties' }, minutesAgo: 160 }),
|
||||
rawEntry({ action: 'sftp_login', minutesAgo: 165 }),
|
||||
rawEntry({
|
||||
action: 'console_command_executed',
|
||||
metadata: { command: 'whitelist add Prospector' },
|
||||
minutesAgo: 170,
|
||||
}),
|
||||
rawEntry({ action: 'console_cleared', minutesAgo: 175 }),
|
||||
rawEntry({
|
||||
action: 'backup_created',
|
||||
metadata: { id: backupId },
|
||||
minutesAgo: 180,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'backup_renamed',
|
||||
metadata: {
|
||||
id: backupId,
|
||||
from: 'Manual backup 1',
|
||||
to: 'Before modpack update',
|
||||
},
|
||||
minutesAgo: 185,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'backup_restored',
|
||||
metadata: { id: backupId },
|
||||
minutesAgo: 190,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'backup_deleted',
|
||||
metadata: { id: '00000000-0000-4000-8000-000000000099' },
|
||||
minutesAgo: 195,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'startup_command_modified',
|
||||
metadata: { command: 'java -Xmx8G -jar server.jar nogui' },
|
||||
minutesAgo: 200,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'java_runtime_modified',
|
||||
metadata: { vendor: 'temurin' },
|
||||
minutesAgo: 205,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'java_version_modified',
|
||||
metadata: { version: 21 },
|
||||
minutesAgo: 210,
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
const missingLookupActionLogResponse: Archon.Actions.v1.ActionLogResponse = {
|
||||
next_offset: null,
|
||||
users: {},
|
||||
addons: {},
|
||||
versions: {},
|
||||
data: [
|
||||
rawEntry({
|
||||
action: 'user_permission_modified',
|
||||
metadata: { user_id: 'unknown-user', permissions: -9223372036854775808 },
|
||||
worldId: 'unknown-world',
|
||||
minutesAgo: 5,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'addon_updated',
|
||||
metadata: { addons: [{ addon_id: 'unknown-addon', version_id: 'unknown-version' }] },
|
||||
minutesAgo: 10,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'modpack_changed',
|
||||
metadata: {
|
||||
spec: {
|
||||
platform: 'local_file',
|
||||
filename: 'example-modpack-v1.20.mrpack',
|
||||
},
|
||||
},
|
||||
minutesAgo: 15,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'backup_deleted',
|
||||
metadata: { id: '00000000-0000-4000-8000-000000000099' },
|
||||
minutesAgo: 20,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'changed_server_name',
|
||||
metadata: { bad: true },
|
||||
minutesAgo: 25,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'backend_added_this_later',
|
||||
metadata: { value: true },
|
||||
actor: { type: 'support', user_id: userIds.prospector },
|
||||
worldId: null,
|
||||
minutesAgo: 30,
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
const overflowAddonNames = [
|
||||
"Alpha's Rise",
|
||||
'Bookshelf',
|
||||
'Diagonal Fences',
|
||||
'Dragon Compass',
|
||||
'Dragon Feed',
|
||||
'Dragons of the Cosmos',
|
||||
"Farmer's Delight",
|
||||
'Fastload',
|
||||
'Friendly Fire',
|
||||
'Isle of Berk Addons',
|
||||
'Jade',
|
||||
'Kiwi',
|
||||
"Nature's Compass",
|
||||
'Off the Grid Dragons',
|
||||
'Patchouli',
|
||||
'Puzzles Lib',
|
||||
'Resourceful Config',
|
||||
'Waystones',
|
||||
]
|
||||
|
||||
const overflowActionLogResponse: Archon.Actions.v1.ActionLogResponse = {
|
||||
next_offset: null,
|
||||
users: actionLogResponse.users,
|
||||
addons: {
|
||||
...Object.fromEntries(
|
||||
overflowAddonNames.map((title, index) => [
|
||||
`overflow-addon-${index}`,
|
||||
{
|
||||
title,
|
||||
slug: `overflow-addon-${index}`,
|
||||
icon_url: null,
|
||||
version: null,
|
||||
},
|
||||
]),
|
||||
),
|
||||
'overflow-sodium': {
|
||||
title: 'Sodium',
|
||||
slug: 'sodium',
|
||||
icon_url: null,
|
||||
version: null,
|
||||
},
|
||||
'overflow-iris': {
|
||||
title: 'Iris Shaders',
|
||||
slug: 'iris',
|
||||
icon_url: null,
|
||||
version: null,
|
||||
},
|
||||
} as Record<string, Archon.Actions.v1.AddonResp>,
|
||||
versions: {
|
||||
...Object.fromEntries(
|
||||
overflowAddonNames.map((title, index) => [
|
||||
`overflow-version-${index}`,
|
||||
{
|
||||
name: `${title} 1.${index}.0`,
|
||||
version_number: `1.${index}.0`,
|
||||
},
|
||||
]),
|
||||
),
|
||||
'overflow-sodium-version': {
|
||||
name: 'Sodium mc1.21.1-0.6.13-fabric',
|
||||
version_number: 'mc1.21.1-0.6.13-fabric',
|
||||
},
|
||||
'overflow-iris-version': {
|
||||
name: 'Iris Shaders 1.8.8+1.21.1-fabric',
|
||||
version_number: '1.8.8+1.21.1-fabric',
|
||||
},
|
||||
} as Record<string, Archon.Actions.v1.VersionResp>,
|
||||
data: [
|
||||
rawEntry({
|
||||
action: 'addon_uploaded',
|
||||
metadata: {
|
||||
file_names: ['fabric-api-0.116.12+1.21.1.jar', 'lithium-fabric-0.15.3+mc1.21.1.jar'],
|
||||
},
|
||||
minutesAgo: 1,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'addon_added',
|
||||
metadata: {
|
||||
addons: [
|
||||
{ addon_id: 'overflow-sodium', version_id: 'overflow-sodium-version' },
|
||||
{ addon_id: 'overflow-iris', version_id: 'overflow-iris-version' },
|
||||
],
|
||||
},
|
||||
minutesAgo: 2,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'addon_enabled',
|
||||
metadata: {
|
||||
addons: [
|
||||
{ addon_id: 'overflow-sodium', version_id: 'overflow-sodium-version' },
|
||||
{ addon_id: 'overflow-iris', version_id: 'overflow-iris-version' },
|
||||
],
|
||||
},
|
||||
minutesAgo: 3,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'addon_disabled',
|
||||
metadata: {
|
||||
addons: [
|
||||
{ addon_id: 'overflow-sodium', version_id: 'overflow-sodium-version' },
|
||||
{ addon_id: 'overflow-iris', version_id: 'overflow-iris-version' },
|
||||
],
|
||||
},
|
||||
minutesAgo: 4,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'addon_deleted',
|
||||
metadata: {
|
||||
addons: overflowAddonNames.map((_, index) => ({
|
||||
addon_id: `overflow-addon-${index}`,
|
||||
version_id: `overflow-version-${index}`,
|
||||
})),
|
||||
},
|
||||
minutesAgo: 5,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'startup_command_modified',
|
||||
metadata: {
|
||||
command:
|
||||
'java -Xmx8192M -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -jar fabric-server-launch.jar nogui',
|
||||
},
|
||||
minutesAgo: 6,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'server_properties_modified',
|
||||
metadata: {
|
||||
properties: {
|
||||
difficulty: 'hard',
|
||||
'enforce-whitelist': 'true',
|
||||
'max-players': '20',
|
||||
'resource-pack': 'https://cdn.example.invalid/very-long-resource-pack-name.zip',
|
||||
},
|
||||
},
|
||||
minutesAgo: 7,
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
type RawEntryInput = {
|
||||
action: Archon.Actions.v1.ActionName | string
|
||||
metadata?: unknown
|
||||
actor?: Archon.Actions.v1.ActionUser
|
||||
worldId?: string | null
|
||||
minutesAgo: number
|
||||
}
|
||||
|
||||
function rawEntry(input: RawEntryInput): Archon.Actions.v1.ActionEntry {
|
||||
return {
|
||||
actor: input.actor ?? { type: 'user', user_id: userIds.geometrically },
|
||||
action:
|
||||
input.metadata === undefined
|
||||
? { action: input.action }
|
||||
: { action: input.action, metadata: input.metadata },
|
||||
server_id: serverId,
|
||||
world_id: input.worldId ?? activeWorldId,
|
||||
timestamp: new Date(Date.now() - input.minutesAgo * 60 * 1000).toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
function lookupsFromResponse(
|
||||
response: Archon.Actions.v1.ActionLogResponse,
|
||||
serverFull: Archon.Servers.v1.ServerFull,
|
||||
): AuditEventLookups {
|
||||
const backups = new Map<string, Archon.Backups.v1.Backup>()
|
||||
for (const world of serverFull.worlds) {
|
||||
for (const backup of world.backups) {
|
||||
backups.set(backup.id, backup)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
serverId: serverFull.id,
|
||||
users: response.users,
|
||||
addons: response.addons,
|
||||
worldById: new Map(
|
||||
serverFull.worlds.map((world) => [world.id, { id: world.id, name: world.name }]),
|
||||
),
|
||||
backupById: backups,
|
||||
versions: response.versions,
|
||||
}
|
||||
}
|
||||
|
||||
function toAuditEntries(
|
||||
response: Archon.Actions.v1.ActionLogResponse,
|
||||
serverFull = serverFullResponse,
|
||||
): ServerAuditLogEntry[] {
|
||||
const lookups = lookupsFromResponse(response, serverFull)
|
||||
return response.data.map((entry, index) => {
|
||||
const event = parseAuditEvent(entry, lookups)
|
||||
return {
|
||||
id: `${entry.timestamp}-${entry.actor.type}-${entry.world_id ?? 'server'}-${index}`,
|
||||
actor: event.props.actor,
|
||||
world: event.props.world,
|
||||
event,
|
||||
timestamp: entry.timestamp,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const everyActionEntries = toAuditEntries(
|
||||
auditLogEntryExamples,
|
||||
auditLogEntryExamplesServerFullResponse,
|
||||
)
|
||||
const sampleEntries = toAuditEntries(actionLogResponse)
|
||||
const permissionBadgeEntries = sampleEntries.filter((entry) =>
|
||||
['user_invited', 'user_permission_modified'].includes(entry.event.key),
|
||||
)
|
||||
const loaderVersionEntries = toAuditEntries({
|
||||
...actionLogResponse,
|
||||
data: [
|
||||
rawEntry({
|
||||
action: 'loader_version_edited',
|
||||
metadata: { new_loader: 'fabric', new_version: '0.19.2' },
|
||||
minutesAgo: 1,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'loader_version_edited',
|
||||
metadata: { new_loader: null, new_version: '0.30.0-beta.7' },
|
||||
minutesAgo: 2,
|
||||
}),
|
||||
rawEntry({
|
||||
action: 'loader_version_edited',
|
||||
metadata: { new_loader: null, new_version: null },
|
||||
minutesAgo: 3,
|
||||
}),
|
||||
],
|
||||
})
|
||||
const fallbackEntries = toAuditEntries(missingLookupActionLogResponse, {
|
||||
...serverFullResponse,
|
||||
worlds: [],
|
||||
})
|
||||
const overflowEntries = toAuditEntries(overflowActionLogResponse)
|
||||
|
||||
const meta = {
|
||||
title: 'Servers/AuditLogTable',
|
||||
component: AuditLogTable,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Audit entries render backend action-log events through typed event components.',
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template: '<div style="max-width: 1120px;"><story /></div>',
|
||||
}),
|
||||
],
|
||||
} satisfies Meta<typeof AuditLogTable>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
function createTimeframeState(
|
||||
initial: Partial<{
|
||||
mode: TimeFrameMode
|
||||
preset: TimeFramePreset
|
||||
lastAmount: number
|
||||
lastUnit: TimeFrameLastUnit
|
||||
customStartDate: string
|
||||
customEndDate: string
|
||||
}> = {},
|
||||
) {
|
||||
return {
|
||||
timeframeCustomEndDate: ref(initial.customEndDate ?? ''),
|
||||
timeframeCustomStartDate: ref(initial.customStartDate ?? ''),
|
||||
timeframeLastAmount: ref(initial.lastAmount ?? 30),
|
||||
timeframeLastUnit: ref<TimeFrameLastUnit>(initial.lastUnit ?? 'days'),
|
||||
timeframeMode: ref<TimeFrameMode>(initial.mode ?? 'preset'),
|
||||
timeframePreset: ref<TimeFramePreset>(initial.preset ?? 'all_time'),
|
||||
}
|
||||
}
|
||||
|
||||
function renderStory(entries: ServerAuditLogEntry[], initialQuery = '') {
|
||||
return () => ({
|
||||
components: { AuditLogTable },
|
||||
setup() {
|
||||
const query = ref(initialQuery)
|
||||
const filters = ref<ServerAuditLogFilters>({
|
||||
userId: null,
|
||||
worldId: null,
|
||||
})
|
||||
return { entries, filters, query, ...createTimeframeState() }
|
||||
},
|
||||
template: /* html */ `
|
||||
<AuditLogTable
|
||||
v-model:query="query"
|
||||
v-model:timeframe-mode="timeframeMode"
|
||||
v-model:timeframe-preset="timeframePreset"
|
||||
v-model:timeframe-last-amount="timeframeLastAmount"
|
||||
v-model:timeframe-last-unit="timeframeLastUnit"
|
||||
v-model:timeframe-custom-start-date="timeframeCustomStartDate"
|
||||
v-model:timeframe-custom-end-date="timeframeCustomEndDate"
|
||||
v-model:filters="filters"
|
||||
:entries="entries"
|
||||
/>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
export const AllEvents: Story = {
|
||||
render: renderStory(everyActionEntries),
|
||||
}
|
||||
|
||||
export const PermissionBadges: Story = {
|
||||
render: renderStory(permissionBadgeEntries),
|
||||
}
|
||||
|
||||
export const LoaderVersionEvents: Story = {
|
||||
render: renderStory(loaderVersionEntries),
|
||||
}
|
||||
|
||||
export const LongOverflowTooltip: Story = {
|
||||
render: renderStory(overflowEntries),
|
||||
}
|
||||
|
||||
export const MissingLookupsAndFallbacks: Story = {
|
||||
render: renderStory(fallbackEntries),
|
||||
}
|
||||
|
||||
export const Filtered: Story = {
|
||||
render: renderStory(sampleEntries, 'Create Aeronautics'),
|
||||
}
|
||||
|
||||
export const WithExternalFilterControls: Story = {
|
||||
render: () => ({
|
||||
components: { AuditLogTable, DropdownFilterBar },
|
||||
setup() {
|
||||
const query = ref('')
|
||||
const filters = ref<ServerAuditLogFilters>({
|
||||
userId: null,
|
||||
worldId: null,
|
||||
})
|
||||
const externalFilters = ref<Record<string, string[]>>({
|
||||
users: [userIds.geometrically],
|
||||
worlds: [],
|
||||
actions: [],
|
||||
})
|
||||
return {
|
||||
categories: filterBarCategories,
|
||||
entries: sampleEntries,
|
||||
externalFilters,
|
||||
filters,
|
||||
query,
|
||||
...createTimeframeState(),
|
||||
}
|
||||
},
|
||||
template: /* html */ `
|
||||
<AuditLogTable
|
||||
v-model:query="query"
|
||||
v-model:timeframe-mode="timeframeMode"
|
||||
v-model:timeframe-preset="timeframePreset"
|
||||
v-model:timeframe-last-amount="timeframeLastAmount"
|
||||
v-model:timeframe-last-unit="timeframeLastUnit"
|
||||
v-model:timeframe-custom-start-date="timeframeCustomStartDate"
|
||||
v-model:timeframe-custom-end-date="timeframeCustomEndDate"
|
||||
v-model:filters="filters"
|
||||
:entries="entries"
|
||||
has-active-external-filters
|
||||
>
|
||||
<template #filters>
|
||||
<DropdownFilterBar
|
||||
v-model="externalFilters"
|
||||
:categories="categories"
|
||||
add-label="Add filter"
|
||||
clear-label="Clear"
|
||||
use-filter-icon
|
||||
/>
|
||||
</template>
|
||||
</AuditLogTable>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const EmptyExternalFilters: Story = {
|
||||
render: () => ({
|
||||
components: { AuditLogTable },
|
||||
setup() {
|
||||
const query = ref('')
|
||||
const filters = ref<ServerAuditLogFilters>({
|
||||
userId: null,
|
||||
worldId: null,
|
||||
})
|
||||
return { entries: [], filters, query, ...createTimeframeState() }
|
||||
},
|
||||
template: /* html */ `
|
||||
<AuditLogTable
|
||||
v-model:query="query"
|
||||
v-model:timeframe-mode="timeframeMode"
|
||||
v-model:timeframe-preset="timeframePreset"
|
||||
v-model:timeframe-last-amount="timeframeLastAmount"
|
||||
v-model:timeframe-last-unit="timeframeLastUnit"
|
||||
v-model:timeframe-custom-start-date="timeframeCustomStartDate"
|
||||
v-model:timeframe-custom-end-date="timeframeCustomEndDate"
|
||||
v-model:filters="filters"
|
||||
:entries="entries"
|
||||
has-active-external-filters
|
||||
/>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const MediumAppWindow: Story = {
|
||||
render: () => ({
|
||||
components: { AuditLogTable },
|
||||
setup() {
|
||||
const query = ref('')
|
||||
const filters = ref<ServerAuditLogFilters>({
|
||||
userId: null,
|
||||
worldId: null,
|
||||
})
|
||||
return {
|
||||
entries: everyActionEntries.slice(0, 8),
|
||||
filters,
|
||||
query,
|
||||
...createTimeframeState(),
|
||||
}
|
||||
},
|
||||
template: /* html */ `
|
||||
<div style="max-width: 860px;">
|
||||
<AuditLogTable
|
||||
v-model:query="query"
|
||||
v-model:timeframe-mode="timeframeMode"
|
||||
v-model:timeframe-preset="timeframePreset"
|
||||
v-model:timeframe-last-amount="timeframeLastAmount"
|
||||
v-model:timeframe-last-unit="timeframeLastUnit"
|
||||
v-model:timeframe-custom-start-date="timeframeCustomStartDate"
|
||||
v-model:timeframe-custom-end-date="timeframeCustomEndDate"
|
||||
v-model:filters="filters"
|
||||
:entries="entries"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const MobileCompact: Story = {
|
||||
render: () => ({
|
||||
components: { AuditLogTable },
|
||||
setup() {
|
||||
const query = ref('')
|
||||
const filters = ref<ServerAuditLogFilters>({
|
||||
userId: null,
|
||||
worldId: null,
|
||||
})
|
||||
return {
|
||||
entries: everyActionEntries.slice(0, 8),
|
||||
filters,
|
||||
query,
|
||||
...createTimeframeState(),
|
||||
}
|
||||
},
|
||||
template: /* html */ `
|
||||
<div style="max-width: 390px;">
|
||||
<AuditLogTable
|
||||
v-model:query="query"
|
||||
v-model:timeframe-mode="timeframeMode"
|
||||
v-model:timeframe-preset="timeframePreset"
|
||||
v-model:timeframe-last-amount="timeframeLastAmount"
|
||||
v-model:timeframe-last-unit="timeframeLastUnit"
|
||||
v-model:timeframe-custom-start-date="timeframeCustomStartDate"
|
||||
v-model:timeframe-custom-end-date="timeframeCustomEndDate"
|
||||
v-model:filters="filters"
|
||||
:entries="entries"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { Archon, UploadState } from '@modrinth/api-client'
|
||||
import type { Stats } from '@modrinth/utils'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import EditServerIcon from '../../components/servers/edit-server-icon/EditServerIcon.vue'
|
||||
import { provideModrinthServerContext } from '../../providers'
|
||||
import type { CancelUploadHandler, ModrinthServerContext } from '../../providers/server-context'
|
||||
import type {
|
||||
CancelUploadHandler,
|
||||
ModrinthServerContext,
|
||||
ServerStats,
|
||||
} from '../../providers/server-context'
|
||||
|
||||
const meta = {
|
||||
title: 'Servers/EditServerIcon',
|
||||
@@ -22,7 +25,7 @@ const meta = {
|
||||
upstream: null,
|
||||
} as Archon.Servers.v0.Server)
|
||||
|
||||
const stats = ref<Stats>({
|
||||
const stats = ref<ServerStats>({
|
||||
current: {
|
||||
cpu_percent: 0,
|
||||
ram_usage_bytes: 0,
|
||||
@@ -59,6 +62,8 @@ const meta = {
|
||||
},
|
||||
worldId: ref(null),
|
||||
server,
|
||||
serverFull: computed(() => null),
|
||||
currentUserPermissions: computed(() => 0),
|
||||
isConnected: ref(true),
|
||||
isWsAuthIncorrect: ref(false),
|
||||
powerState: ref('running'),
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||
import GrantAccessModal from '../../components/servers/access/GrantAccessModal.vue'
|
||||
import type {
|
||||
GrantServerAccessPayload,
|
||||
ServerAccessMember,
|
||||
} from '../../components/servers/access/types'
|
||||
|
||||
const meta = {
|
||||
title: 'Servers/GrantAccessModal',
|
||||
component: GrantAccessModal,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Role descriptions use the same instance-focused copy as the access page. The username field searches users asynchronously before showing the empty state.',
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof GrantAccessModal>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { ButtonStyled, GrantAccessModal },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof GrantAccessModal> | null>(null)
|
||||
const lastAddedUser = ref('')
|
||||
const users = [
|
||||
{ id: 'fetch', username: 'Fetch' },
|
||||
{ id: 'emma', username: 'Emma' },
|
||||
]
|
||||
async function searchUsers(query: string) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250))
|
||||
const normalizedQuery = query.trim().toLowerCase()
|
||||
return users.filter((user) => user.username.toLowerCase().startsWith(normalizedQuery))
|
||||
}
|
||||
function handleGrant(payload: GrantServerAccessPayload) {
|
||||
lastAddedUser.value = `${payload.target} as ${payload.role}${
|
||||
payload.addAsFriend ? ' with friend request' : ''
|
||||
}`
|
||||
}
|
||||
return { modalRef, searchUsers, lastAddedUser, handleGrant }
|
||||
},
|
||||
template: /* html */ `
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="modalRef?.show($event)">Add user</button>
|
||||
</ButtonStyled>
|
||||
<p v-if="lastAddedUser" class="m-0 text-sm text-secondary">Last added: {{ lastAddedUser }}</p>
|
||||
<GrantAccessModal ref="modalRef" :search-users="searchUsers" @grant="handleGrant" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const ExistingMember: Story = {
|
||||
render: () => ({
|
||||
components: { ButtonStyled, GrantAccessModal },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof GrantAccessModal> | null>(null)
|
||||
const users = [
|
||||
{ id: 'josh11', username: 'josh11' },
|
||||
{ id: 'emma', username: 'Emma' },
|
||||
]
|
||||
const members: ServerAccessMember[] = [
|
||||
{
|
||||
id: 'story-josh11',
|
||||
user: {
|
||||
id: 'josh11',
|
||||
username: 'josh11',
|
||||
},
|
||||
role: 'editor',
|
||||
joinedAt: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
async function searchUsers(query: string) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250))
|
||||
const normalizedQuery = query.trim().toLowerCase()
|
||||
return users.filter((user) => user.username.toLowerCase().startsWith(normalizedQuery))
|
||||
}
|
||||
return { modalRef, members, searchUsers }
|
||||
},
|
||||
template: /* html */ `
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="modalRef?.show($event)">Add existing user</button>
|
||||
</ButtonStyled>
|
||||
<GrantAccessModal ref="modalRef" :members="members" :search-users="searchUsers" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
@@ -41,6 +41,16 @@ export const Default: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const SharedWithOwner: Story = {
|
||||
args: {
|
||||
...baseMedalServer,
|
||||
name: 'Medal Co-op Server',
|
||||
owner: {
|
||||
username: 'Prospector',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ConfiguringNewServer: Story = {
|
||||
args: {
|
||||
...baseMedalServer,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||
import RemoveAccessModal from '../../components/servers/access/RemoveAccessModal.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Servers/RemoveAccessModal',
|
||||
component: RemoveAccessModal,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies Meta<typeof RemoveAccessModal>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { ButtonStyled, RemoveAccessModal },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof RemoveAccessModal> | null>(null)
|
||||
const removed = ref(false)
|
||||
function handleRemove() {
|
||||
removed.value = true
|
||||
}
|
||||
return { modalRef, removed, handleRemove }
|
||||
},
|
||||
template: /* html */ `
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<ButtonStyled color="red">
|
||||
<button @click="modalRef?.show()">Remove user</button>
|
||||
</ButtonStyled>
|
||||
<p v-if="removed" class="m-0 text-sm text-secondary">User removed</p>
|
||||
<RemoveAccessModal
|
||||
ref="modalRef"
|
||||
username="Geometrically"
|
||||
role="editor"
|
||||
:joined-at="new Date(Date.now() - 21 * 24 * 60 * 60 * 1000).toISOString()"
|
||||
@remove="handleRemove"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const CancelInvite: Story = {
|
||||
render: () => ({
|
||||
components: { ButtonStyled, RemoveAccessModal },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof RemoveAccessModal> | null>(null)
|
||||
const cancelled = ref(false)
|
||||
function handleCancel() {
|
||||
cancelled.value = true
|
||||
}
|
||||
return { modalRef, cancelled, handleCancel }
|
||||
},
|
||||
template: /* html */ `
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<ButtonStyled color="red">
|
||||
<button @click="modalRef?.show()">Cancel invite</button>
|
||||
</ButtonStyled>
|
||||
<p v-if="cancelled" class="m-0 text-sm text-secondary">Invite cancelled</p>
|
||||
<RemoveAccessModal
|
||||
ref="modalRef"
|
||||
username="Geometrically"
|
||||
role="viewer"
|
||||
:joined-at="null"
|
||||
pending
|
||||
should-cancel
|
||||
@remove="handleCancel"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
@@ -50,6 +50,16 @@ export const Default: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const SharedWithOwner: Story = {
|
||||
args: {
|
||||
...baseServer,
|
||||
name: 'Cobbletown',
|
||||
owner: {
|
||||
username: 'Prospector',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ConfiguringNewServer: Story = {
|
||||
args: {
|
||||
...baseServer,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Archon, UploadState } from '@modrinth/api-client'
|
||||
import type { Stats } from '@modrinth/utils'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -8,7 +7,11 @@ import ServerPanelAdmonitions from '../../components/servers/admonitions/ServerP
|
||||
import { defineMessage } from '../../composables/i18n'
|
||||
import type { FileOperation } from '../../layouts/shared/files-tab/types'
|
||||
import { provideModrinthServerContext } from '../../providers'
|
||||
import type { CancelUploadHandler, ModrinthServerContext } from '../../providers/server-context'
|
||||
import type {
|
||||
CancelUploadHandler,
|
||||
ModrinthServerContext,
|
||||
ServerStats,
|
||||
} from '../../providers/server-context'
|
||||
|
||||
const meta = {
|
||||
title: 'Servers/ServerPanelAdmonitions',
|
||||
@@ -31,7 +34,7 @@ const meta = {
|
||||
upstream: null,
|
||||
} as Archon.Servers.v0.Server)
|
||||
|
||||
const stats = ref<Stats>({
|
||||
const stats = ref<ServerStats>({
|
||||
current: {
|
||||
cpu_percent: 0,
|
||||
ram_usage_bytes: 0,
|
||||
@@ -76,6 +79,8 @@ const meta = {
|
||||
},
|
||||
worldId: ref(null),
|
||||
server,
|
||||
serverFull: computed(() => null),
|
||||
currentUserPermissions: computed(() => 0),
|
||||
isConnected: ref(true),
|
||||
isWsAuthIncorrect: ref(false),
|
||||
powerState: ref('running'),
|
||||
|
||||
@@ -0,0 +1,930 @@
|
||||
{
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"server": {
|
||||
"id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"name": "CaffeineMC Fabric SMP",
|
||||
"subdomain": "caffeine-smp"
|
||||
},
|
||||
"worlds": [
|
||||
{
|
||||
"id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"name": "Overworld"
|
||||
},
|
||||
{
|
||||
"id": "ba1a5666-96fc-4de5-9b91-977dd7b58d09",
|
||||
"name": "Creative testing"
|
||||
}
|
||||
],
|
||||
"backups": [
|
||||
{
|
||||
"id": "142f5ee6-a239-4824-bc69-5db0e9564c24",
|
||||
"physical_id": "restic:4f3d5df7/142f5ee6-a239-4824-bc69-5db0e9564c24",
|
||||
"name": "Pre-1.21.1 upgrade",
|
||||
"created_at": "2026-05-20T11:40:00Z",
|
||||
"automated": false,
|
||||
"status": "done",
|
||||
"interrupted": false,
|
||||
"ongoing": false,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "2f9c9471-baba-45ff-a2c1-23c9962095ec",
|
||||
"physical_id": "restic:4f3d5df7/2f9c9471-baba-45ff-a2c1-23c9962095ec",
|
||||
"name": "Before Nether reset",
|
||||
"created_at": "2026-05-20T13:55:00Z",
|
||||
"automated": false,
|
||||
"status": "done",
|
||||
"interrupted": false,
|
||||
"ongoing": false,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "6f2ef8ef-3329-4c77-bb06-8a1b6b1db0f0",
|
||||
"physical_id": "restic:4f3d5df7/6f2ef8ef-3329-4c77-bb06-8a1b6b1db0f0",
|
||||
"name": "Rollback after datapack test",
|
||||
"created_at": "2026-05-20T14:15:00Z",
|
||||
"automated": false,
|
||||
"status": "done",
|
||||
"interrupted": false,
|
||||
"ongoing": false,
|
||||
"locked": true
|
||||
}
|
||||
],
|
||||
"next_offset": null,
|
||||
"data": [
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "server_created"
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T09:00:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "changed_server_name",
|
||||
"metadata": {
|
||||
"name": "CaffeineMC Fabric SMP"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T09:05:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "changed_server_subdomain",
|
||||
"metadata": {
|
||||
"subdomain": "caffeine-smp"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T09:10:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "support",
|
||||
"user_id": "JZA4dW8o"
|
||||
},
|
||||
"action": {
|
||||
"action": "server_reallocated"
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T09:15:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "support",
|
||||
"user_id": "JZA4dW8o"
|
||||
},
|
||||
"action": {
|
||||
"action": "server_plan_changed",
|
||||
"metadata": {
|
||||
"new_specs": {
|
||||
"cpu": 4,
|
||||
"memory_mb": 8192,
|
||||
"storage_mb": 40960,
|
||||
"swap_mb": 1024
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T09:20:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "user_invited",
|
||||
"metadata": {
|
||||
"user_id": "JZA4dW8o",
|
||||
"permissions": "BASE_READ | POWER_ACTIONS"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T09:25:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "user_invite_revoked",
|
||||
"metadata": {
|
||||
"user_id": "DzLrfrbK"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T09:30:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "user_permission_modified",
|
||||
"metadata": {
|
||||
"user_id": "JZA4dW8o",
|
||||
"permissions": "BASE_READ | FILES_WRITE | SETUP | BACKUPS"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T09:35:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "user_removed",
|
||||
"metadata": {
|
||||
"user_id": "DzLrfrbK"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T09:40:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_added",
|
||||
"metadata": {
|
||||
"addons": [
|
||||
{
|
||||
"addon_id": "AANobbMI",
|
||||
"version_id": "u1OEbNKx"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T09:45:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_added",
|
||||
"metadata": {
|
||||
"addons": [
|
||||
{
|
||||
"addon_id": "P7dR8mSH",
|
||||
"version_id": "Lwt6YYHL"
|
||||
},
|
||||
{
|
||||
"addon_id": "gvQqBUqZ",
|
||||
"version_id": "XQJtuOTA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T09:50:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_uploaded",
|
||||
"metadata": {
|
||||
"file_names": ["sodium-fabric-0.6.13+mc1.21.1.jar"]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T09:55:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_uploaded",
|
||||
"metadata": {
|
||||
"file_names": ["fabric-api-0.116.12+1.21.1.jar", "lithium-fabric-0.15.3+mc1.21.1.jar"]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_disabled",
|
||||
"metadata": {
|
||||
"addons": [
|
||||
{
|
||||
"addon_id": "YL57xq9U",
|
||||
"version_id": "zsoi0dso"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:05:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_disabled",
|
||||
"metadata": {
|
||||
"addons": [
|
||||
{
|
||||
"addon_id": "AANobbMI",
|
||||
"version_id": "u1OEbNKx"
|
||||
},
|
||||
{
|
||||
"addon_id": "YL57xq9U",
|
||||
"version_id": "zsoi0dso"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:10:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_enabled",
|
||||
"metadata": {
|
||||
"addons": [
|
||||
{
|
||||
"addon_id": "YL57xq9U",
|
||||
"version_id": "zsoi0dso"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:15:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_enabled",
|
||||
"metadata": {
|
||||
"addons": [
|
||||
{
|
||||
"addon_id": "AANobbMI",
|
||||
"version_id": "u1OEbNKx"
|
||||
},
|
||||
{
|
||||
"addon_id": "YL57xq9U",
|
||||
"version_id": "zsoi0dso"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:20:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_deleted",
|
||||
"metadata": {
|
||||
"addons": [
|
||||
{
|
||||
"addon_id": "YL57xq9U",
|
||||
"version_id": "zsoi0dso"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:25:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_deleted",
|
||||
"metadata": {
|
||||
"addons": [
|
||||
{
|
||||
"addon_id": "AANobbMI",
|
||||
"version_id": "u1OEbNKx"
|
||||
},
|
||||
{
|
||||
"addon_id": "P7dR8mSH",
|
||||
"version_id": "Lwt6YYHL"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_updated",
|
||||
"metadata": {
|
||||
"addons": [
|
||||
{
|
||||
"addon_id": "AANobbMI",
|
||||
"version_id": "u1OEbNKx"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:35:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "addon_updated",
|
||||
"metadata": {
|
||||
"addons": [
|
||||
{
|
||||
"addon_id": "P7dR8mSH",
|
||||
"version_id": "Lwt6YYHL"
|
||||
},
|
||||
{
|
||||
"addon_id": "gvQqBUqZ",
|
||||
"version_id": "XQJtuOTA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:40:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "uk2lIMmm"
|
||||
},
|
||||
"action": {
|
||||
"action": "modpack_changed",
|
||||
"metadata": {
|
||||
"spec": {
|
||||
"platform": "modrinth",
|
||||
"project_id": "BYN9yKrV",
|
||||
"version_id": "w0KHzZ43",
|
||||
"mrpack_sha1": "6b277e2f6e3a9a1acef3342bd154ade25031ad70"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:45:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "uk2lIMmm"
|
||||
},
|
||||
"action": {
|
||||
"action": "modpack_unlinked",
|
||||
"metadata": {
|
||||
"spec": {
|
||||
"platform": "modrinth",
|
||||
"project_id": "BYN9yKrV",
|
||||
"version_id": "w0KHzZ43",
|
||||
"mrpack_sha1": "6b277e2f6e3a9a1acef3342bd154ade25031ad70"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:50:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "support",
|
||||
"user_id": "JZA4dW8o"
|
||||
},
|
||||
"action": {
|
||||
"action": "server_repaired"
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T10:55:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "server_reset"
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "ba1a5666-96fc-4de5-9b91-977dd7b58d09",
|
||||
"timestamp": "2026-05-20T11:00:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "server_started"
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T11:05:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "server_stopped"
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T11:10:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "server_restarted"
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T11:15:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "support",
|
||||
"user_id": "JZA4dW8o"
|
||||
},
|
||||
"action": {
|
||||
"action": "server_killed"
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T11:20:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "port_allocation_added",
|
||||
"metadata": {
|
||||
"port": 25565
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T11:25:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "port_allocation_removed",
|
||||
"metadata": {
|
||||
"port": 24454
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T11:30:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "loader_version_edited",
|
||||
"metadata": {
|
||||
"new_loader": "fabric",
|
||||
"new_version": "0.16.14"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T11:35:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "game_version_edited",
|
||||
"metadata": {
|
||||
"new_version": "1.21.1"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T11:40:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "server_properties_modified",
|
||||
"metadata": {
|
||||
"properties": {
|
||||
"difficulty": "hard"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T11:45:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "server_properties_modified",
|
||||
"metadata": {
|
||||
"properties": {
|
||||
"max-players": "20",
|
||||
"simulation-distance": "8",
|
||||
"view-distance": "10"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T11:50:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "file_uploaded",
|
||||
"metadata": {
|
||||
"path": "/mods/lithium-fabric-0.15.3+mc1.21.1.jar"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T11:55:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "file_deleted",
|
||||
"metadata": {
|
||||
"path": "/mods/old-performance-patch.jar"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T12:00:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "file_renamed",
|
||||
"metadata": {
|
||||
"from": "/world/datapacks/trial-test.zip",
|
||||
"to": "/world/datapacks/trial-test.zip.disabled"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T12:05:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "file_edited",
|
||||
"metadata": {
|
||||
"path": "/server.properties"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T12:10:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "sftp_login"
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": null,
|
||||
"timestamp": "2026-05-20T12:15:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "console_command_executed",
|
||||
"metadata": {
|
||||
"command": "say Maintenance in 5 minutes"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T12:20:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "console_cleared"
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T12:25:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "backup_created",
|
||||
"metadata": {
|
||||
"id": "142f5ee6-a239-4824-bc69-5db0e9564c24"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T12:30:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "backup_renamed",
|
||||
"metadata": {
|
||||
"id": "2f9c9471-baba-45ff-a2c1-23c9962095ec",
|
||||
"from": "Before Nether test",
|
||||
"to": "Before Nether reset"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T12:35:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "backup_restored",
|
||||
"metadata": {
|
||||
"id": "6f2ef8ef-3329-4c77-bb06-8a1b6b1db0f0"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T12:40:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "backup_deleted",
|
||||
"metadata": {
|
||||
"id": "0dd0eb1e-f0e0-46d1-b32f-5245e18bf563"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "ba1a5666-96fc-4de5-9b91-977dd7b58d09",
|
||||
"timestamp": "2026-05-20T12:45:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "startup_command_modified",
|
||||
"metadata": {
|
||||
"command": "java -Xms4G -Xmx8G -jar fabric-server-launch.jar nogui"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T12:50:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "java_runtime_modified",
|
||||
"metadata": {
|
||||
"vendor": "temurin"
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T12:55:00Z"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"type": "user",
|
||||
"user_id": "TEZXhE2U"
|
||||
},
|
||||
"action": {
|
||||
"action": "java_version_modified",
|
||||
"metadata": {
|
||||
"version": 21
|
||||
}
|
||||
},
|
||||
"server_id": "4f3d5df7-4e75-4f08-a1f5-fc472fb7b7bd",
|
||||
"world_id": "7dd2f1a9-2a94-41fb-a0c7-92653ec980ab",
|
||||
"timestamp": "2026-05-20T13:00:00Z"
|
||||
}
|
||||
],
|
||||
"users": {
|
||||
"TEZXhE2U": {
|
||||
"username": "jellysquid3",
|
||||
"avatar_url": "https://cdn.modrinth.com/user/TEZXhE2U/f4705a5f2388c65029ae2e59f1434b3e6e4de23a.png"
|
||||
},
|
||||
"JZA4dW8o": {
|
||||
"username": "modmuss50",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/4324090?v=4"
|
||||
},
|
||||
"DzLrfrbK": {
|
||||
"username": "IMS",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/31803019?v=4"
|
||||
},
|
||||
"uk2lIMmm": {
|
||||
"username": "devin",
|
||||
"avatar_url": "https://cdn.modrinth.com/user/uk2lIMmm/9ceb45272fef68343dcbc01696eb1c6856acb701.png"
|
||||
}
|
||||
},
|
||||
"addons": {
|
||||
"AANobbMI": {
|
||||
"title": "Sodium",
|
||||
"slug": "sodium",
|
||||
"icon_url": "https://cdn.modrinth.com/data/AANobbMI/295862f4724dc3f78df3447ad6072b2dcd3ef0c9_96.webp",
|
||||
"version": null
|
||||
},
|
||||
"P7dR8mSH": {
|
||||
"title": "Fabric API",
|
||||
"slug": "fabric-api",
|
||||
"icon_url": "https://cdn.modrinth.com/data/P7dR8mSH/icon.png",
|
||||
"version": null
|
||||
},
|
||||
"YL57xq9U": {
|
||||
"title": "Iris Shaders",
|
||||
"slug": "iris",
|
||||
"icon_url": "https://cdn.modrinth.com/data/YL57xq9U/18d0e7f076d3d6ed5bedd472b853909aac5da202_96.webp",
|
||||
"version": null
|
||||
},
|
||||
"gvQqBUqZ": {
|
||||
"title": "Lithium",
|
||||
"slug": "lithium",
|
||||
"icon_url": "https://cdn.modrinth.com/data/gvQqBUqZ/bcc8686c13af0143adf4285d741256af824f70b7_96.webp",
|
||||
"version": null
|
||||
},
|
||||
"BYN9yKrV": {
|
||||
"title": "Adrenaline",
|
||||
"slug": "adrenaline",
|
||||
"icon_url": "https://cdn.modrinth.com/data/BYN9yKrV/98f14519960b17418d5ba5ebfdc46c85154ad87b_96.webp",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
"versions": {
|
||||
"u1OEbNKx": {
|
||||
"name": "Sodium 0.6.13 for Fabric 1.21.1",
|
||||
"version_number": "mc1.21.1-0.6.13-fabric"
|
||||
},
|
||||
"Lwt6YYHL": {
|
||||
"name": "[1.21.1] Fabric API 0.116.12+1.21.1",
|
||||
"version_number": "0.116.12+1.21.1"
|
||||
},
|
||||
"zsoi0dso": {
|
||||
"name": "Iris 1.8.8 for Fabric 1.21.1",
|
||||
"version_number": "1.8.8+1.21.1-fabric"
|
||||
},
|
||||
"XQJtuOTA": {
|
||||
"name": "Lithium 0.15.3 for Fabric",
|
||||
"version_number": "mc1.21.1-0.15.3-fabric"
|
||||
},
|
||||
"w0KHzZ43": {
|
||||
"name": "26.1.4+mc1.21.1.fabric",
|
||||
"version_number": "26.1.4+mc1.21.1.fabric"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user