76 lines
6.6 KiB
Svelte
76 lines
6.6 KiB
Svelte
<script lang="ts">
|
||
import { page } from '$app/state';
|
||
let { data, children } = $props();
|
||
const can = (permission: string) => data.permissions.includes('*') || data.permissions.includes(permission);
|
||
const canSeeData = $derived(['data.view', 'data.manage', 'data.propose', 'data.approve'].some(can));
|
||
const contributorEnabled = $derived(Boolean(data.contributorMode?.enabled));
|
||
const guidelinesPage = $derived(page.url.pathname === '/isac/contributor-guidelines');
|
||
const dataReadOnly = $derived(can('data.view') && !can('data.manage') && (!can('data.propose') || !contributorEnabled) && !guidelinesPage);
|
||
const dataMode = $derived(can('*') ? 'ADMIN' : can('data.approve') ? 'OVERSIGHT' : can('data.manage') ? 'DATA MANAGER' : contributorEnabled ? 'CONTRIBUTOR' : 'READ-ONLY');
|
||
const dataModeClass = $derived(dataMode.toLowerCase().replace(' ', '-'));
|
||
let readOnlyBlocked = $state(false);
|
||
const dataSections = [
|
||
{ href: '/isac/attributes', label: 'Attributes', code: 'ATTR', module: 'attributes' },
|
||
{ href: '/isac/talents', label: 'Talents', code: 'TLNT', module: 'talents' },
|
||
{ href: '/isac/brands', label: 'Brand sets', code: 'BRND', module: 'brands' },
|
||
{ href: '/isac/gear', label: 'Gear', code: 'GEAR', module: 'gear' },
|
||
{ href: '/isac/weapons', label: 'Weapons', code: 'WPN', module: 'weapons' },
|
||
{ href: '/isac/protocols', label: 'OS Protocols', code: 'OSPR', module: 'protocols' },
|
||
{ href: '/isac/skillchips', label: 'Skill Chips', code: 'CHIP', module: 'skillchips' }
|
||
];
|
||
const userSections = [
|
||
{ href: '/isac/users', label: 'User operations', code: 'USER', permission: 'users.view' },
|
||
{ href: '/isac/roles', label: 'Roles', code: 'ROLE', permission: 'users.roles' }
|
||
];
|
||
const visibleUserSections = $derived(userSections.filter((section) => can(section.permission)));
|
||
const oversightSections = [
|
||
{ href: '/isac/data-moderation', label: 'Data Oversight', code: 'OVST', permission: 'data.approve' },
|
||
{ href: '/isac/avatar-oversight', label: 'Avatar Oversight', code: 'AVTR', permission: 'avatars.review' },
|
||
{ href: '/isac/audit', label: 'Audit history', code: 'AUDT', permission: 'audit.view' },
|
||
{ href: '/isac/metrics', label: 'System metrics', code: 'MTRX', permission: 'metrics.view' }
|
||
];
|
||
const visibleOversightSections = $derived(oversightSections.filter((section) => can(section.permission)));
|
||
const selectNumericValue = (event: FocusEvent) => {
|
||
const input = event.target;
|
||
if (input instanceof HTMLInputElement && input.type === 'number' && !input.disabled) input.select();
|
||
};
|
||
</script>
|
||
|
||
<main class="admin-page">
|
||
<header class="admin-command">
|
||
<div class="command-title"><p class="eyebrow"><span></span> INTELLIGENT SYSTEM ANALYTIC COMPUTER</p><h1>ISAC</h1><p>SHD.build operations and intelligence control surface</p></div>
|
||
<div class="command-status"><div><i></i><span>SYSTEM</span><strong>ONLINE</strong></div><div><span>ACCESS</span><strong>AUTHORIZED</strong></div><div><span>ENVIRONMENT</span><strong>LOCAL</strong></div><div class="data-mode-status" class:admin={dataModeClass === 'admin'} class:data-manager={dataModeClass === 'data-manager'} class:oversight={dataModeClass === 'oversight'} class:contributor={dataModeClass === 'contributor'} class:read-only={dataModeClass === 'read-only'}><span>DATA MODE</span><strong>{dataMode}</strong></div></div>
|
||
</header>
|
||
<div class="admin-dashboard">
|
||
<aside class="admin-sidebar">
|
||
{#if visibleUserSections.length}
|
||
<div class="sidebar-label"><span>USER OPERATIONS</span><b>{String(visibleUserSections.length).padStart(2, '0')} MODULE{visibleUserSections.length === 1 ? '' : 'S'}</b></div>
|
||
<nav aria-label="User operations">{#each visibleUserSections as section}<a class:active={page.url.pathname.startsWith(section.href)} href={section.href}><small>{section.code}</small><span>{section.label}</span><b>›</b></a>{/each}</nav>
|
||
{/if}
|
||
{#if canSeeData}
|
||
<div class="sidebar-label" class:section-label={visibleUserSections.length > 0}><span>DATA MANAGEMENT</span><b>{String(dataSections.length).padStart(2, '0')} MODULES</b></div>
|
||
<nav aria-label="Data management">
|
||
{#each dataSections as section}<a class:active={page.url.pathname === section.href} class:has-pending={Boolean(data.pendingProposals[section.module])} href={section.href}><small>{section.code}</small><span>{section.label}</span>{#if data.pendingProposals[section.module]}<em aria-label={`${data.pendingProposals[section.module]} pending change proposal${data.pendingProposals[section.module] === 1 ? '' : 's'}`} title="Pending change proposals">{data.pendingProposals[section.module]}</em>{/if}<b>›</b></a>{/each}
|
||
</nav>
|
||
{/if}
|
||
{#if visibleOversightSections.length}
|
||
<div class="sidebar-label section-label"><span>SYSTEM OVERSIGHT</span><b>{String(visibleOversightSections.length).padStart(2, '0')} MODULE{visibleOversightSections.length === 1 ? '' : 'S'}</b></div>
|
||
<nav aria-label="System oversight">{#each visibleOversightSections as section}<a class:active={page.url.pathname.startsWith(section.href)} href={section.href}><small>{section.code}</small><span>{section.label}</span><b>›</b></a>{/each}</nav>
|
||
{/if}
|
||
<div class="sidebar-foot"><span>ISAC NODE</span><strong>NYC-01</strong><small>Access policy synchronized</small></div>
|
||
</aside>
|
||
<section class="admin-content" class:data-readonly={dataReadOnly} onfocusin={selectNumericValue} onsubmit={(event) => {
|
||
if (dataReadOnly && (event.target as HTMLFormElement).method.toLowerCase() === 'post') {
|
||
event.preventDefault();
|
||
readOnlyBlocked = true;
|
||
}
|
||
}}>
|
||
{#if page.url.searchParams.has('proposal')}<div class="proposal-submitted" role="status"><b>CHANGE QUEUED</b><span>Your suggestion is awaiting data moderation and has not changed the live data set.</span></div>{/if}
|
||
{#if dataReadOnly}<div class="data-access-banner"><div><b>READ-ONLY DATA ACCESS</b><span>You can inspect registries without submitting changes.</span></div>{#if can('data.propose')}<a class="mode-toggle" href={`/isac/contributor-guidelines?returnTo=${encodeURIComponent(page.url.pathname + page.url.search)}`}>ENABLE CONTRIBUTOR MODE →</a>{/if}</div>
|
||
{:else if contributorEnabled && !guidelinesPage && can('data.propose') && !can('data.manage')}<div class="data-access-banner contributor"><div><b>CONTRIBUTOR MODE</b><span>Suggestions enter moderation and do not immediately change live data.</span></div><form method="POST" action="/isac/contributor-guidelines?/disable"><input type="hidden" name="returnTo" value={page.url.pathname + page.url.search}/><button class="mode-toggle" type="submit">RETURN TO READ-ONLY</button></form></div>{/if}
|
||
{#if readOnlyBlocked}<p class="error data-access-error" role="alert">This account has read-only game-data access.</p>{/if}
|
||
{@render children()}
|
||
</section>
|
||
</div>
|
||
</main>
|