Show oversight queue counts

This commit is contained in:
JB
2026-07-25 21:27:28 -04:00
parent c5036e7bf0
commit c0a8e05e78
3 changed files with 26 additions and 6 deletions
+7
View File
@@ -735,6 +735,13 @@ grid, because the arrow will wrap to another row. The count includes textual
assistive labeling and strengthens its border and contrast on link hover or assistive labeling and strengthens its border and contrast on link hover or
active state. active state.
The same compact count cell identifies actionable work in System Oversight
navigation. Show it only for queues with unresolved items, such as pending data
proposals or avatar reviews, and calculate it within the viewer's permissions.
Do not add zero badges or counts to informational registries such as Audit
History and System Metrics. Badge text must expose an `awaiting review` label to
assistive technology; color supplements that meaning rather than replacing it.
Pending records may be used as dependencies in later proposals by any Pending records may be used as dependencies in later proposals by any
contributor. Render these choices in the ordinary assignment control with a contributor. Render these choices in the ordinary assignment control with a
restrained orange border/background and the explicit text `PENDING` plus the restrained orange border/background and the explicit text `PENDING` plus the
+12 -2
View File
@@ -12,14 +12,24 @@ export const load: LayoutServerLoad = async ({ cookies }) => {
const canSeeData = permissions.includes('*') || ['data.view','data.manage','data.propose','data.approve'].some((permission) => permissions.includes(permission)); const canSeeData = permissions.includes('*') || ['data.view','data.manage','data.propose','data.approve'].some((permission) => permissions.includes(permission));
const canManageData = permissions.includes('*') || permissions.includes('data.manage'); const canManageData = permissions.includes('*') || permissions.includes('data.manage');
const canProposeData = permissions.includes('*') || permissions.includes('data.propose'); const canProposeData = permissions.includes('*') || permissions.includes('data.propose');
const canApproveData = permissions.includes('*') || permissions.includes('data.approve');
const canReviewAvatars = permissions.includes('*') || permissions.includes('avatars.review');
const contributorMode = !canManageData && canProposeData ? await getContributorMode(user.id) : null; const contributorMode = !canManageData && canProposeData ? await getContributorMode(user.id) : null;
const database = canSeeData || canReviewAvatars ? await getDatabase() : null;
const pendingRows = canSeeData const pendingRows = canSeeData
? await (await (await getDatabase()).prepare("SELECT module,COUNT(*) AS count FROM data_change_proposals WHERE status='pending' GROUP BY module")).all() ? await (await database!.prepare("SELECT module,COUNT(*) AS count FROM data_change_proposals WHERE status='pending' GROUP BY module")).all()
: []; : [];
const pendingProposalTotal = pendingRows.reduce((total, row) => total + Number(row.count), 0);
const pendingAvatarReviews = canReviewAvatars
? Number((await (await database!.prepare("SELECT COUNT(*) AS count FROM avatar_reviews WHERE status='pending'")).get())?.count ?? 0)
: 0;
return { return {
permissions, permissions,
contributorMode, contributorMode,
pendingProposals: Object.fromEntries(pendingRows.map((row) => [String(row.module), Number(row.count)])), pendingProposals: Object.fromEntries(pendingRows.map((row) => [String(row.module), Number(row.count)])),
pendingProposalTotal: pendingRows.reduce((total, row) => total + Number(row.count), 0) oversightReviewCounts: {
data: canApproveData ? pendingProposalTotal : 0,
avatars: pendingAvatarReviews
}
}; };
}; };
+7 -4
View File
@@ -23,9 +23,9 @@
{ href: '/isac/roles', label: 'Roles', code: 'ROLE', permission: 'users.roles' } { href: '/isac/roles', label: 'Roles', code: 'ROLE', permission: 'users.roles' }
]; ];
const visibleUserSections = $derived(userSections.filter((section) => can(section.permission))); const visibleUserSections = $derived(userSections.filter((section) => can(section.permission)));
const oversightSections = [ const oversightSections: Array<{ href: string; label: string; code: string; permission: string; reviewKey?: 'data' | 'avatars' }> = [
{ href: '/isac/data-moderation', label: 'Data Oversight', code: 'OVST', permission: 'data.approve' }, { href: '/isac/data-moderation', label: 'Data Oversight', code: 'OVST', permission: 'data.approve', reviewKey: 'data' },
{ href: '/isac/avatar-oversight', label: 'Avatar Oversight', code: 'AVTR', permission: 'avatars.review' }, { href: '/isac/avatar-oversight', label: 'Avatar Oversight', code: 'AVTR', permission: 'avatars.review', reviewKey: 'avatars' },
{ href: '/isac/audit', label: 'Audit history', code: 'AUDT', permission: 'audit.view' }, { href: '/isac/audit', label: 'Audit history', code: 'AUDT', permission: 'audit.view' },
{ href: '/isac/metrics', label: 'System metrics', code: 'MTRX', permission: 'metrics.view' } { href: '/isac/metrics', label: 'System metrics', code: 'MTRX', permission: 'metrics.view' }
]; ];
@@ -55,7 +55,10 @@
{/if} {/if}
{#if visibleOversightSections.length} {#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> <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> <nav aria-label="System oversight">{#each visibleOversightSections as section}
{@const reviewCount = section.reviewKey ? Number(data.oversightReviewCounts[section.reviewKey] ?? 0) : 0}
<a class:active={page.url.pathname.startsWith(section.href)} class:has-pending={reviewCount > 0} href={section.href}><small>{section.code}</small><span>{section.label}</span>{#if reviewCount}<em aria-label={`${reviewCount} item${reviewCount === 1 ? '' : 's'} awaiting review`} title="Awaiting review">{reviewCount}</em>{/if}<b>›</b></a>
{/each}</nav>
{/if} {/if}
<div class="sidebar-foot"><span>ISAC NODE</span><strong>NYC-01</strong><small>Access policy synchronized</small></div> <div class="sidebar-foot"><span>ISAC NODE</span><strong>NYC-01</strong><small>Access policy synchronized</small></div>
</aside> </aside>