diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 8754ce8..614fa5a 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -435,7 +435,7 @@ Do not communicate state by color alone; include a label, icon, or pattern. ## Adding a new admin module -1. Add the route beneath `src/routes/admin`. +1. Add the route beneath `src/routes/isac`. 2. Register it in the admin layout navigation. 3. Start with the module toolbar and KPI row. 4. Reuse dashboard editor and table classes. diff --git a/docs/architecture.md b/docs/architecture.md index 55a8926..68ab9a8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -181,7 +181,11 @@ specialized workflow rather than inferred database writes. # System metrics -The read-only `/admin/metrics` route requires `metrics.view` and collects a +ISAC uses `/isac` as its canonical route namespace. The server permanently +redirects legacy `/admin` URLs to their matching `/isac` location so old +notifications and external bookmarks remain usable. + +The read-only `/isac/metrics` route requires `metrics.view` and collects a point-in-time process-local health snapshot. Node and operating-system APIs provide process CPU and memory, event-loop utilization and delay, host memory, load average, core count, platform, and uptime. Embedded Turso pragmas provide diff --git a/scripts/seed-test-notifications.mjs b/scripts/seed-test-notifications.mjs index c2227df..ae45a07 100644 --- a/scripts/seed-test-notifications.mjs +++ b/scripts/seed-test-notifications.mjs @@ -12,8 +12,8 @@ if (!user) throw new Error(`No user matched ${targetName}.`); const now = Date.now(); const samples = [ ['isac', 'ISAC AVATAR OVERSIGHT', '[TEST] Custom avatar removed', 'Your custom avatar did not meet SHD.build image requirements and was removed. Oversight reason: The image contained unrelated promotional text.', '/account#avatar', 1], - ['contributions', 'ISAC DATA OVERSIGHT', '[TEST] Contribution approved', 'Quick Draw brand-set data was approved and deployed to the live builder registry.', '/admin/brands', 1], - ['contributions', 'ISAC DATA OVERSIGHT', '[TEST] Contribution needs revision', 'A talent update was not approved. Oversight reason: Tier II effect value does not match the supplied game evidence.', '/admin/talents', 1], + ['contributions', 'ISAC DATA OVERSIGHT', '[TEST] Contribution approved', 'Quick Draw brand-set data was approved and deployed to the live builder registry.', '/isac/brands', 1], + ['contributions', 'ISAC DATA OVERSIGHT', '[TEST] Contribution needs revision', 'A talent update was not approved. Oversight reason: Tier II effect value does not match the supplied game evidence.', '/isac/talents', 1], ['comments', 'SHD COMMUNITY', '[TEST] New comment on your build', 'An agent commented on “First Wave Vector Critical Build”: This setup worked really well in Legendary missions.', '/builds', 0] ]; for (const [index, sample] of samples.entries()) { diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 9a90c7e..4e17f56 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -4,8 +4,12 @@ import { getSessionUser } from '$lib/server/auth/session'; import { DATA_MODULES, queueDataProposal, shouldModerateDataMutation, type DataModule } from '$lib/server/data-moderation'; export const handle: Handle = async ({ event, resolve }) => { + if (event.url.pathname === '/admin' || event.url.pathname.startsWith('/admin/')) { + const location = `/isac${event.url.pathname.slice('/admin'.length)}${event.url.search}`; + return new Response(null, { status: 308, headers: { location } }); + } if (event.request.method === 'POST') { - const match = event.url.pathname.match(/^\/admin\/([^/]+)$/); + const match = event.url.pathname.match(/^\/isac\/([^/]+)$/); const module = match?.[1] as DataModule | undefined; const action = event.url.search.startsWith('?/') ? event.url.search.slice(2) : ''; if (module && DATA_MODULES.includes(module) && action) { diff --git a/src/lib/components/AppShell.svelte b/src/lib/components/AppShell.svelte index 7059809..58dd061 100644 --- a/src/lib/components/AppShell.svelte +++ b/src/lib/components/AppShell.svelte @@ -90,7 +90,7 @@ My Profile {/if} {#if isacAccess} - ISAC + ISAC {/if} {#if user} @@ -120,7 +120,7 @@ {:else} 04Sign In› {/if} - {#if isacAccess}06ISAC›{/if} + {#if isacAccess}06ISAC›{/if} {#if user}
This account has read-only game-data access.
{/if} {@render children()} diff --git a/src/routes/admin/+page.server.ts b/src/routes/isac/+page.server.ts similarity index 64% rename from src/routes/admin/+page.server.ts rename to src/routes/isac/+page.server.ts index 69856d3..3a47789 100644 --- a/src/routes/admin/+page.server.ts +++ b/src/routes/isac/+page.server.ts @@ -5,10 +5,10 @@ import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ cookies }) => { const permissions = await getPermissions(await getSessionUser(cookies)); - if (permissions.includes('*') || permissions.includes('users.view')) redirect(303, '/admin/users'); - if (permissions.includes('users.roles')) redirect(303, '/admin/roles'); - if (permissions.includes('*') || ['data.view','data.manage','data.propose','data.approve'].some((permission) => permissions.includes(permission))) redirect(303, '/admin/attributes'); - if (permissions.includes('audit.view')) redirect(303, '/admin/audit'); - if (permissions.includes('metrics.view')) redirect(303, '/admin/metrics'); + if (permissions.includes('*') || permissions.includes('users.view')) redirect(303, '/isac/users'); + if (permissions.includes('users.roles')) redirect(303, '/isac/roles'); + if (permissions.includes('*') || ['data.view','data.manage','data.propose','data.approve'].some((permission) => permissions.includes(permission))) redirect(303, '/isac/attributes'); + if (permissions.includes('audit.view')) redirect(303, '/isac/audit'); + if (permissions.includes('metrics.view')) redirect(303, '/isac/metrics'); redirect(303, '/account'); }; diff --git a/src/routes/admin/attributes/+page.server.ts b/src/routes/isac/attributes/+page.server.ts similarity index 100% rename from src/routes/admin/attributes/+page.server.ts rename to src/routes/isac/attributes/+page.server.ts diff --git a/src/routes/admin/attributes/+page.svelte b/src/routes/isac/attributes/+page.svelte similarity index 100% rename from src/routes/admin/attributes/+page.svelte rename to src/routes/isac/attributes/+page.svelte diff --git a/src/routes/admin/audit/+page.server.ts b/src/routes/isac/audit/+page.server.ts similarity index 100% rename from src/routes/admin/audit/+page.server.ts rename to src/routes/isac/audit/+page.server.ts diff --git a/src/routes/admin/audit/+page.svelte b/src/routes/isac/audit/+page.svelte similarity index 100% rename from src/routes/admin/audit/+page.svelte rename to src/routes/isac/audit/+page.svelte diff --git a/src/routes/admin/avatar-oversight/+page.server.ts b/src/routes/isac/avatar-oversight/+page.server.ts similarity index 100% rename from src/routes/admin/avatar-oversight/+page.server.ts rename to src/routes/isac/avatar-oversight/+page.server.ts diff --git a/src/routes/admin/avatar-oversight/+page.svelte b/src/routes/isac/avatar-oversight/+page.svelte similarity index 100% rename from src/routes/admin/avatar-oversight/+page.svelte rename to src/routes/isac/avatar-oversight/+page.svelte diff --git a/src/routes/admin/brands/+page.server.ts b/src/routes/isac/brands/+page.server.ts similarity index 100% rename from src/routes/admin/brands/+page.server.ts rename to src/routes/isac/brands/+page.server.ts diff --git a/src/routes/admin/brands/+page.svelte b/src/routes/isac/brands/+page.svelte similarity index 100% rename from src/routes/admin/brands/+page.svelte rename to src/routes/isac/brands/+page.svelte diff --git a/src/routes/admin/contributor-guidelines/+page.server.ts b/src/routes/isac/contributor-guidelines/+page.server.ts similarity index 94% rename from src/routes/admin/contributor-guidelines/+page.server.ts rename to src/routes/isac/contributor-guidelines/+page.server.ts index 9b946e7..93f63da 100644 --- a/src/routes/admin/contributor-guidelines/+page.server.ts +++ b/src/routes/isac/contributor-guidelines/+page.server.ts @@ -8,8 +8,8 @@ import { recordIsacAudit } from '$lib/server/audit'; import type { Actions, PageServerLoad } from './$types'; const safeReturn = (value: FormDataEntryValue | null) => { - const path = String(value ?? '/admin/attributes'); - return path.startsWith('/admin/') && !path.startsWith('//') ? path : '/admin/attributes'; + const path = String(value ?? '/isac/attributes'); + return path.startsWith('/isac/') && !path.startsWith('//') ? path : '/isac/attributes'; }; export const load: PageServerLoad = async ({ cookies, url }) => { @@ -17,7 +17,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => { if (!user) redirect(303, '/sign-in'); const permissions = await getPermissions(user); const eligible = permissions.includes('*') || permissions.includes('data.propose'); - if (!eligible) redirect(303, '/admin/attributes'); + if (!eligible) redirect(303, '/isac/attributes'); return { contributorMode: await getContributorMode(user.id), returnTo: safeReturn(url.searchParams.get('returnTo')) diff --git a/src/routes/admin/contributor-guidelines/+page.svelte b/src/routes/isac/contributor-guidelines/+page.svelte similarity index 100% rename from src/routes/admin/contributor-guidelines/+page.svelte rename to src/routes/isac/contributor-guidelines/+page.svelte diff --git a/src/routes/admin/data-moderation/+page.server.ts b/src/routes/isac/data-moderation/+page.server.ts similarity index 96% rename from src/routes/admin/data-moderation/+page.server.ts rename to src/routes/isac/data-moderation/+page.server.ts index 45b3e14..06fa1cc 100644 --- a/src/routes/admin/data-moderation/+page.server.ts +++ b/src/routes/isac/data-moderation/+page.server.ts @@ -102,7 +102,7 @@ export const actions: Actions = { const names = unresolved.map((item) => `${item.summary} (${item.status})`).join(', '); return fail(409, { moderationError: `Approve this proposal's dependencies first: ${names}.` }); } - const target = new URL(`/admin/${proposal.module}?/${proposal.action}`, url.origin); + const target = new URL(`/isac/${proposal.module}?/${proposal.action}`, url.origin); const proposalForm = deserializeProposal(String(proposal.payload_json)); if (proposal.draft_entity_id && !proposal.entity_id) proposalForm.set('_proposedEntityId', String(proposal.draft_entity_id)); const response = await fetch(target, { method: 'POST', body: proposalForm, headers: { origin: url.origin } }); @@ -111,12 +111,12 @@ export const actions: Actions = { const details = (await response.text()).slice(0, 1000); await (await database.prepare("UPDATE data_change_proposals SET status='failed',reviewed_by=?,review_note=?,reviewed_at=? WHERE id=?")).run(reviewer.id, details || note || 'Canonical validation failed.', now, id); await recordIsacAudit({ actorId: reviewer.id, category: 'data_moderation', action: 'proposal.failed', entityId: id, before: { status: 'pending' }, after: { status: 'failed', responseStatus: response.status } }); - await createNotification({ userId: String(proposal.proposed_by), category: 'contributions', source: 'ISAC DATA OVERSIGHT', title: 'Contribution could not be deployed', body: `${proposal.summary} did not pass current validation. Oversight evidence is available in your notification inbox.`, link: `/admin/${proposal.module}`, mandatory: true }); + await createNotification({ userId: String(proposal.proposed_by), category: 'contributions', source: 'ISAC DATA OVERSIGHT', title: 'Contribution could not be deployed', body: `${proposal.summary} did not pass current validation. Oversight evidence is available in your notification inbox.`, link: `/isac/${proposal.module}`, mandatory: true }); return fail(409, { moderationError: 'The proposal did not pass the current canonical validation and was marked failed.' }); } await (await database.prepare("UPDATE data_change_proposals SET status='approved',applied_entity_id=COALESCE(entity_id,draft_entity_id),reviewed_by=?,review_note=?,reviewed_at=?,applied_at=? WHERE id=?")).run(reviewer.id, note || null, now, now, id); await recordIsacAudit({ actorId: reviewer.id, category: 'data_moderation', action: 'proposal.approved', entityId: id, before: { status: 'pending' }, after: { status: 'approved', module: proposal.module, action: proposal.action, proposedBy: proposal.proposed_by, appliedEntityId: proposal.entity_id ?? proposal.draft_entity_id } }); - await createNotification({ userId: String(proposal.proposed_by), category: 'contributions', source: 'ISAC DATA OVERSIGHT', title: 'Contribution approved', body: `${proposal.summary} was approved and deployed to the live builder data set.`, link: `/admin/${proposal.module}`, mandatory: true }); + await createNotification({ userId: String(proposal.proposed_by), category: 'contributions', source: 'ISAC DATA OVERSIGHT', title: 'Contribution approved', body: `${proposal.summary} was approved and deployed to the live builder data set.`, link: `/isac/${proposal.module}`, mandatory: true }); return { moderationSaved: true, moderationToken: randomUUID() }; }, reject: async ({ cookies, request }) => { @@ -131,7 +131,7 @@ export const actions: Actions = { const now = new Date().toISOString(); await (await database.prepare("UPDATE data_change_proposals SET status='rejected',reviewed_by=?,review_note=?,reviewed_at=? WHERE id=?")).run(reviewer.id, note, now, id); await recordIsacAudit({ actorId: reviewer.id, category: 'data_moderation', action: 'proposal.rejected', entityId: id, before: { status: 'pending' }, after: { status: 'rejected', note, module: proposal.module, action: proposal.action, proposedBy: proposal.proposed_by } }); - await createNotification({ userId: String(proposal.proposed_by), category: 'contributions', source: 'ISAC DATA OVERSIGHT', title: 'Contribution needs revision', body: `${String(proposal.action).replace(/([A-Z])/g, ' $1').trim()} was not approved. Oversight reason: ${note}`, link: `/admin/${proposal.module}`, mandatory: true }); + await createNotification({ userId: String(proposal.proposed_by), category: 'contributions', source: 'ISAC DATA OVERSIGHT', title: 'Contribution needs revision', body: `${String(proposal.action).replace(/([A-Z])/g, ' $1').trim()} was not approved. Oversight reason: ${note}`, link: `/isac/${proposal.module}`, mandatory: true }); return { moderationSaved: true, moderationToken: randomUUID() }; } }; diff --git a/src/routes/admin/data-moderation/+page.svelte b/src/routes/isac/data-moderation/+page.svelte similarity index 100% rename from src/routes/admin/data-moderation/+page.svelte rename to src/routes/isac/data-moderation/+page.svelte diff --git a/src/routes/admin/gear/+page.server.ts b/src/routes/isac/gear/+page.server.ts similarity index 100% rename from src/routes/admin/gear/+page.server.ts rename to src/routes/isac/gear/+page.server.ts diff --git a/src/routes/admin/gear/+page.svelte b/src/routes/isac/gear/+page.svelte similarity index 100% rename from src/routes/admin/gear/+page.svelte rename to src/routes/isac/gear/+page.svelte diff --git a/src/routes/admin/metrics/+page.server.ts b/src/routes/isac/metrics/+page.server.ts similarity index 100% rename from src/routes/admin/metrics/+page.server.ts rename to src/routes/isac/metrics/+page.server.ts diff --git a/src/routes/admin/metrics/+page.svelte b/src/routes/isac/metrics/+page.svelte similarity index 100% rename from src/routes/admin/metrics/+page.svelte rename to src/routes/isac/metrics/+page.svelte diff --git a/src/routes/admin/protocols/+page.server.ts b/src/routes/isac/protocols/+page.server.ts similarity index 100% rename from src/routes/admin/protocols/+page.server.ts rename to src/routes/isac/protocols/+page.server.ts diff --git a/src/routes/admin/protocols/+page.svelte b/src/routes/isac/protocols/+page.svelte similarity index 100% rename from src/routes/admin/protocols/+page.svelte rename to src/routes/isac/protocols/+page.svelte diff --git a/src/routes/admin/roles/+page.server.ts b/src/routes/isac/roles/+page.server.ts similarity index 100% rename from src/routes/admin/roles/+page.server.ts rename to src/routes/isac/roles/+page.server.ts diff --git a/src/routes/admin/roles/+page.svelte b/src/routes/isac/roles/+page.svelte similarity index 100% rename from src/routes/admin/roles/+page.svelte rename to src/routes/isac/roles/+page.svelte diff --git a/src/routes/admin/skillchips/+page.server.ts b/src/routes/isac/skillchips/+page.server.ts similarity index 100% rename from src/routes/admin/skillchips/+page.server.ts rename to src/routes/isac/skillchips/+page.server.ts diff --git a/src/routes/admin/skillchips/+page.svelte b/src/routes/isac/skillchips/+page.svelte similarity index 100% rename from src/routes/admin/skillchips/+page.svelte rename to src/routes/isac/skillchips/+page.svelte diff --git a/src/routes/admin/talents/+page.server.ts b/src/routes/isac/talents/+page.server.ts similarity index 100% rename from src/routes/admin/talents/+page.server.ts rename to src/routes/isac/talents/+page.server.ts diff --git a/src/routes/admin/talents/+page.svelte b/src/routes/isac/talents/+page.svelte similarity index 98% rename from src/routes/admin/talents/+page.svelte rename to src/routes/isac/talents/+page.svelte index 4539423..687cff8 100644 --- a/src/routes/admin/talents/+page.svelte +++ b/src/routes/isac/talents/+page.svelte @@ -98,4 +98,4 @@ {/if}{tierDescription(item.tiers, 'I')}
{tierDescription(item.tiers, 'II')}
ACTIVE{#if item.category === 'Exotic Weapon'}{#if item.ownerWeaponId}EDIT WEAPON{:else}{/if}{:else}{/if}{tierDescription(item.tiers, 'I')}
{tierDescription(item.tiers, 'II')}
ACTIVE{#if item.category === 'Exotic Weapon'}{#if item.ownerWeaponId}EDIT WEAPON{:else}{/if}{:else}{/if}