Complete private authentication, server integrations and activity reporting

Add Fabric/Forge version builds and Paper integration, preserve permanent player identities across renames, harden session authorization, and surface privacy-conscious launcher/server activity in the panel.
This commit is contained in:
SCOPEDD committed 2026-09-28 13:31:17 -04:00
commit a1f19e86c6
74 files changed
+2108 -95

No files matched your search

+3
View File
@@ -11,6 +11,7 @@
import Servers from './pages/Servers.svelte';
import ServerDetail from './pages/ServerDetail.svelte';
import Capes from './pages/Capes.svelte';
import Activity from './pages/Activity.svelte';
import { get } from './lib/api';
import { route } from './lib/router.svelte';
import { logout, session, type Me } from './lib/session.svelte';
@@ -64,6 +65,8 @@
<Servers />
{:else if route.name === 'capes'}
<Capes />
{:else if route.name === 'activity'}
<Activity />
{:else if route.name === 'users'}
<Users />
{:else if route.name === 'branding'}
+1
View File
@@ -11,6 +11,7 @@
{ id: 'instances', label: 'Instances', icon: Boxes },
{ id: 'users', label: 'Players', icon: Users },
{ id: 'servers', label: 'Servers', icon: Server },
{ id: 'activity', label: 'Activity', icon: LayoutDashboard },
{ id: 'capes', label: 'Capes', icon: Flag },
{ id: 'branding', label: 'Launcher design', icon: Palette },
{ id: 'settings', label: 'Settings', icon: Settings },
+1 -1
View File
@@ -22,7 +22,7 @@
<ol class="steps">
<li>
<strong>Install the SCOPENET integration</strong>
<span class="muted small">The plugin for Paper, Purpur or Spigot, or the mod for Fabric and Forge servers.</span>
<span class="muted small">Use scopenet-paper for Paper, Purpur or Spigot. Fabric and Forge need the JAR for the exact Minecraft version: 1.20.1, 1.21.1, 26.1.2, 26.2 or 26.3.</span>
<a class="dl" href="https://github.com/scopeddlol/SCOPENET-MC/releases/latest" target="_blank" rel="noreferrer"><Download size={14} /> Download from Releases</a>
</li>
<li>
+52
View File
@@ -0,0 +1,52 @@
<script lang="ts">
import { onMount } from 'svelte';
import { get } from '../lib/api';
type Entry = { id: number; source: string; server: string | null; uuid: string | null; name: string | null; kind: string; detail: string | null; created_at: string };
let rows = $state<Entry[]>([]);
let source = $state('');
let player = $state('');
let offset = $state(0);
let busy = $state(false);
let error = $state('');
async function load(reset = false) {
if (reset) offset = 0;
busy = true; error = '';
try { rows = await get<Entry[]>('/api/admin/activity?' + new URLSearchParams({ source, player: player.trim(), offset: String(offset) })); }
catch (e) { error = String(e); }
finally { busy = false; }
}
onMount(() => { void load(); });
</script>
<div class="page">
<header><h1>Activity</h1><p class="muted">Server gameplay, launcher activity and account changes. Players keep the same UUID when renamed.</p></header>
<form class="filters" onsubmit={(e) => { e.preventDefault(); load(true); }}>
<label>Source<select bind:value={source}><option value="">All sources</option><option value="server">Servers</option><option value="launcher">Launcher</option><option value="auth">Sign-ins</option><option value="panel">Account & admin changes</option></select></label>
<label>Player<input bind:value={player} placeholder="Exact username or permanent UUID" /></label>
<button disabled={busy}>Refresh</button>
</form>
<p class="muted small">Chat counts only. Command arguments and passwords are never included. Repeated gameplay actions are grouped by reporting interval.</p>
{#if error}<p role="alert">{error}</p>{/if}
<div class="table-wrap">
<table>
<thead><tr><th>Time</th><th>Source</th><th>Player</th><th>Action</th><th>Details</th></tr></thead>
<tbody>
{#each rows as row (row.source + ':' + row.id)}
<tr><td>{new Date(row.created_at).toLocaleString()}</td><td>{row.server ?? row.source}</td><td title={row.uuid ?? ''}>{row.name ?? 'Server'}</td><td>{row.kind.replaceAll('_', ' ')}</td><td>{row.detail ?? '—'}</td></tr>
{:else}<tr><td colspan="5">{busy ? 'Loading…' : 'No matching activity yet.'}</td></tr>{/each}
</tbody>
</table>
</div>
<div class="filters"><button class="ghost" disabled={busy || offset === 0} onclick={() => { offset = Math.max(0, offset - 100); load(); }}>Newer</button><span class="muted">Page {offset / 100 + 1}</span><button class="ghost" disabled={busy || rows.length < 100} onclick={() => { offset += 100; load(); }}>Older</button></div>
</div>
<style>
.page { padding: 32px; max-width: 1500px; margin: auto; }
header { margin-bottom: 24px; }
.filters { display: flex; gap: 16px; align-items: end; margin: 20px 0; flex-wrap: wrap; }
label { display: flex; flex-direction: column; gap: 6px; }
input { min-width: 300px; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; border-bottom: 1px solid var(--line); text-align: left; }
td:last-child { overflow-wrap: anywhere; }
</style>