diff --git a/panel/web/src/components/IntegrationsPanel.svelte b/panel/web/src/components/IntegrationsPanel.svelte new file mode 100644 index 0000000..3c72f09 --- /dev/null +++ b/panel/web/src/components/IntegrationsPanel.svelte @@ -0,0 +1,200 @@ + + +{#if p && p.integrations.length} +
+ {#if lp} +
+
+

LuckPerms ranks

+ v{lp.version} · {timeAgo(lp.updated_at)} + +
+
+ {#each modes as m} + + {/each} +
+

LuckPerms always decides real permissions. Map panel groups to LuckPerms groups under Players → Groups.

+ {#if p.ranks.length} +
+ {#each p.ranks as r (r.uuid)} +
+ +
{r.name}{r.permissions.length} permission{r.permissions.length === 1 ? '' : 's'} shown
+ + {r.display || r.primary} + {#each r.groups.filter((g) => g !== r.primary).slice(0, 4) as g}{g}{/each} + +
+ {/each} +
+ {:else} +

Ranks appear once players are online.

+ {/if} +
+ {/if} + + {#if spark} + {@const tps = spark.data?.tps ?? {}} +
+

Spark

{timeAgo(spark.updated_at)}
+
+
TPS (1m){tps.m1 ?? '–'}
+
MSPT (10s){spark.data?.mspt?.s10?.mean ?? '–'}ms
+
CPU (proc){spark.data?.cpu?.process?.s10 != null ? Math.round(spark.data.cpu.process.s10 * 100) : '–'}%
+
Memory{spark.data?.memory ? fmtMb(spark.data.memory.used_mb) : '–'}
+
+ {#if spark.data?.memory} +
+ of {fmtMb(spark.data.memory.max_mb)} allocated + {/if} +
+ {/if} + + {#if cp} + {@const d = cp.data ?? {}} +
+

CoreProtect

{timeAgo(cp.updated_at)}
+
+
Logged last hour{d.logged_last_hour ?? 0}
+
Since restart{d.logged_total ?? 0}
+
+ {#if d.per_minute?.length} + {@const max = Math.max(1, ...d.per_minute)} + + {/if} +
+ + {#if d.backups?.found} + {d.backups.count} backup{d.backups.count === 1 ? '' : 's'} · {fmtBytes(d.backups.bytes)}{#if d.backups.latest_at} · latest {timeAgo(new Date(d.backups.latest_at).toISOString())}{/if} + {:else} + No backups found in the configured folders. + {/if} +
+ {#if d.top_actors?.length} +
{#each d.top_actors as a}{a.name} · {a.count}{/each}
+ {/if} +
+ {/if} + + {#if wg} +
+
+

WorldGuard regions

+ {wg.data?.regions?.length ?? 0}{wg.data?.truncated ? '+' : ''} · {timeAgo(wg.updated_at)} + +
+ {#if regions.length} +
+ + + + {#each regions.slice(0, 100) as r (r.world + r.id)} + + + + + + + + {/each} + +
RegionWorldAreaPriorityOwners
{r.id}{#if r.id === '__global__'} global{/if}{r.world}{r.id === '__global__' ? 'Everywhere' : spread(r)}{r.priority}{r.owners?.length ? r.owners.join(', ') : r.owner_count ? `${r.owner_count} owners` : 'Server-owned'}
+
+ {:else} +

No regions match.

+ {/if} +
+ {/if} +
+{:else if p} +
+

Plugin integrations

+

LuckPerms, WorldGuard, Spark and CoreProtect appear here automatically once they are installed on this server and it has synced. Turn them off in config.yml under integrations:.

+
+{/if} + + diff --git a/panel/web/src/lib/types.ts b/panel/web/src/lib/types.ts index 5b3585c..5dae903 100644 --- a/panel/web/src/lib/types.ts +++ b/panel/web/src/lib/types.ts @@ -71,7 +71,7 @@ export interface User { last_seen_ingame: string | null; } -export interface Group { id: number; name: string; color: string; members: number } +export interface Group { id: number; name: string; color: string; members: number; luckperms_group?: string } export interface Settings { auth: { diff --git a/panel/web/src/pages/ServerDetail.svelte b/panel/web/src/pages/ServerDetail.svelte index 07497bf..8ba439e 100644 --- a/panel/web/src/pages/ServerDetail.svelte +++ b/panel/web/src/pages/ServerDetail.svelte @@ -4,6 +4,7 @@ CircleDot, Ban, Server, } from '@lucide/svelte'; import Avatar from '../components/Avatar.svelte'; + import IntegrationsPanel from '../components/IntegrationsPanel.svelte'; import LiveMapPanel from '../components/LiveMapPanel.svelte'; import Modal from '../components/Modal.svelte'; import ServerForm from '../components/ServerForm.svelte'; @@ -150,6 +151,8 @@ + +
diff --git a/panel/web/src/pages/Users.svelte b/panel/web/src/pages/Users.svelte index 1bfc469..08e3835 100644 --- a/panel/web/src/pages/Users.svelte +++ b/panel/web/src/pages/Users.svelte @@ -3,7 +3,7 @@ import Avatar from '../components/Avatar.svelte'; import Modal from '../components/Modal.svelte'; import PlayerPanel from '../components/PlayerPanel.svelte'; - import { del, duration, get, patch, post, timeAgo } from '../lib/api'; + import { del, duration, get, patch, post, put, timeAgo } from '../lib/api'; import { session } from '../lib/session.svelte'; import { toast, toastError } from '../lib/toast.svelte'; import type { Cape, Group, User } from '../lib/types'; @@ -100,6 +100,17 @@ } } + async function mapLp(g: Group, value: string) { + try { + await put(`/api/admin/groups/${g.id}/luckperms`, { luckperms_group: value.trim() }); + toast(value.trim() ? `${g.name} follows LuckPerms "${value.trim().toLowerCase()}"` : `${g.name} is no longer mapped`); + refresh(); + } catch (e) { + toastError(e); + refresh(); + } + } + async function removeGroup(g: Group) { try { await del(`/api/admin/groups/${g.id}`); @@ -183,6 +194,8 @@ {g.name} {g.members} member{g.members === 1 ? '' : 's'} + mapLp(g, e.currentTarget.value)} />
{/each} @@ -278,6 +291,8 @@ .head { justify-content: space-between; margin-top: -8px; } .actions { white-space: nowrap; text-align: right; } .group { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; background: var(--bg-2); } + .group { flex-wrap: wrap; } + .group .lp { width: 130px; padding: 4px 8px; font-size: 0.78rem; } .dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } form.row input[type='color'] { width: 42px; flex-shrink: 0; } .two { grid-template-columns: 1fr 1fr; }