Add ISAC registry search controls
This commit is contained in:
@@ -4,7 +4,7 @@ import { getDatabase } from '$lib/server/database';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { getPagination } from '$lib/server/pagination';
|
||||
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
|
||||
import { auditRecord, recordIsacAudit } from '$lib/server/audit';
|
||||
import { getPendingEntities } from '$lib/server/data-moderation';
|
||||
|
||||
@@ -32,21 +32,31 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
SUM(CASE WHEN type='Offensive' THEN 1 ELSE 0 END) AS offensive,
|
||||
SUM(CASE WHEN type='Defensive' THEN 1 ELSE 0 END) AS defensive FROM stat_attributes`)).get();
|
||||
const filter = ['active', 'offensive', 'defensive'].includes(url.searchParams.get('filter') ?? '') ? String(url.searchParams.get('filter')) : '';
|
||||
const where = filter === 'active' ? 'WHERE active=1' : filter === 'offensive' ? "WHERE type='Offensive'" : filter === 'defensive' ? "WHERE type='Defensive'" : '';
|
||||
const visiblePending = pending.filter((stat) => filter === 'offensive' ? String((stat as any).type) === 'Offensive' : filter === 'defensive' ? String((stat as any).type) === 'Defensive' : true);
|
||||
const filtered = await (await database.prepare(`SELECT COUNT(*) AS total FROM stat_attributes ${where}`)).get();
|
||||
const { search, pattern } = getRegistrySearch(url);
|
||||
const conditions = [
|
||||
...(filter === 'active' ? ['active=1'] : filter === 'offensive' ? ["type='Offensive'"] : filter === 'defensive' ? ["type='Defensive'"] : []),
|
||||
...(search ? ["name LIKE ? ESCAPE '\\' COLLATE NOCASE"] : [])
|
||||
];
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const args = search ? [pattern] : [];
|
||||
const visiblePending = pending.filter((stat) =>
|
||||
(filter === 'offensive' ? String((stat as any).type) === 'Offensive' : filter === 'defensive' ? String((stat as any).type) === 'Defensive' : true)
|
||||
&& (!search || String(stat.name).toLocaleLowerCase().includes(search.toLocaleLowerCase()))
|
||||
);
|
||||
const filtered = await (await database.prepare(`SELECT COUNT(*) AS total FROM stat_attributes ${where}`)).get(...args);
|
||||
const pagination = getPagination(url, Number(filtered?.total ?? 0) + visiblePending.length);
|
||||
const pendingOnPage = pagination.page === 1 ? visiblePending.slice(0, pagination.perPage) : [];
|
||||
const databaseOffset = Math.max(0, pagination.offset - visiblePending.length);
|
||||
const databaseLimit = Math.max(0, pagination.perPage - pendingOnPage.length);
|
||||
const stats = databaseLimit
|
||||
? await (await database.prepare(`SELECT * FROM stat_attributes ${where} ORDER BY active DESC, name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(databaseLimit, databaseOffset)
|
||||
? await (await database.prepare(`SELECT * FROM stat_attributes ${where} ORDER BY active DESC, name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(...args, databaseLimit, databaseOffset)
|
||||
: [];
|
||||
return {
|
||||
stats: [...pendingOnPage, ...stats.map((stat) => ({ ...stat, unit: stat.unit === 'flat' ? '' : stat.unit }))],
|
||||
summary: { ...summary, total: Number(summary?.total ?? 0) + pending.length, pending: pending.length },
|
||||
pagination,
|
||||
filter
|
||||
filter,
|
||||
search
|
||||
};
|
||||
};
|
||||
export const actions: Actions = {
|
||||
|
||||
@@ -29,5 +29,5 @@
|
||||
<div class="module-toolbar"><div><p class="eyebrow">ATTRIBUTE REGISTRY</p><h2>ATTRIBUTES</h2><p>Builder values, hidden calculation targets, source ranges, and caps.</p></div><button class="primary" onclick={() => editing=blank()}>ADD ATTRIBUTE <span>+</span></button></div>
|
||||
<KpiFilters active={data.filter} items={[{value:'',label:'TOTAL RECORDS',count:data.summary.total,description:'Show all definitions'},{value:'active',label:'ACTIVE',count:data.summary.active,description:'Available to systems'},{value:'offensive',label:'OFFENSIVE',count:data.summary.offensive,description:'Damage-oriented'},{value:'defensive',label:'DEFENSIVE',count:data.summary.defensive,description:'Survival-oriented'}]}/>
|
||||
{#if editing}<div class="dashboard-editor"><div class="editor-title"><div><p class="eyebrow">{editing.id?'MODIFY RECORD':'NEW RECORD'}</p><h3>{editing.id?editing.name:'ATTRIBUTE DEFINITION'}</h3></div><button aria-label="Close editor" onclick={() => editing=null}>×</button></div><PendingForm action="?/save" class="grid"><input type="hidden" name="id" value={editing.id}/><div class="form-grid"><label class="field">NAME<input name="name" bind:value={editing.name} required /></label><SegmentedChoice legend="TYPE" name="type" options={typeOptions} bind:value={editing.type} required/><SegmentedChoice legend="CATEGORY" name="category" options={categoryOptions} bind:value={editing.category} required/><SegmentedChoice legend="CALCULATION" name="calculationType" options={calculationOptions} bind:value={editing.calculation_type} required/><SegmentedChoice legend="UNIT" name="unit" options={unitOptions} bind:value={editing.unit}/>{#each [['gear','GEAR'],['weapon','WEAPON'],['skill_chip','SKILL CHIP']] as source}<div class="field range-field"><span class="label">{source[1]} RANGE · OPTIONAL</span><div><input name={`${source[0]==='skill_chip'?'chip':source[0]}Min`} type="number" step="any" value={value(editing[`${source[0]}_min`])} placeholder="MIN"/><input name={`${source[0]==='skill_chip'?'chip':source[0]}Max`} type="number" step="any" value={value(editing[`${source[0]}_max`])} placeholder="MAX"/></div></div>{/each}<label class="field">OPTIONAL CAP<input name="capValue" type="number" step="any" value={value(editing.cap_value)}/></label></div>{#if form?.error}<p class="error">{form.error}</p>{/if}<div class="form-actions"><button type="button" class="secondary" onclick={() => editing=null}>CANCEL</button><button class="primary">SAVE ATTRIBUTE →</button></div></PendingForm></div>{/if}
|
||||
<RegistryPagination pagination={data.pagination}/>
|
||||
<RegistryPagination pagination={data.pagination} search={data.search}/>
|
||||
<div class="dashboard-table"><div class="dashboard-row table-heading"><span>ATTRIBUTE</span><span>CLASSIFICATION</span><span>CATEGORY</span><span>CALCULATION</span><span>STATE</span><span></span></div>{#each visibleStats as stat}<div class="dashboard-row" class:archived={Number(stat.active)!==1} class:pending-record={stat.pending}><div><strong>{stat.name}</strong><small>{stat.pending ? `PROPOSED BY ${stat.proposerName}` : stat.unit || 'NO UNIT'}</small></div><span class:type-offensive={stat.type==='Offensive'} class:type-defensive={stat.type==='Defensive'}>{stat.type}</span><span>{stat.category}</span><span>{stat.calculation_type}</span><b class:pending={stat.pending} class="state-dot">{stat.pending?'PENDING':Number(stat.active)===1?'ACTIVE':'ARCHIVED'}</b>{#if stat.pending}<span></span>{:else}<button onclick={() => editing={...stat}}>EDIT</button>{/if}</div>{/each}</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getDatabase } from '$lib/server/database';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { getPagination } from '$lib/server/pagination';
|
||||
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
|
||||
import { auditRecord, recordIsacAudit } from '$lib/server/audit';
|
||||
import { applyIconMutation, gameDataIconMetadata, gameDataIconUrl, GameDataIconValidationError, readIconMutation } from '$lib/server/game-data-icons';
|
||||
import { getPendingEntities } from '$lib/server/data-moderation';
|
||||
@@ -32,16 +32,21 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
SUM(CASE WHEN active=0 THEN 1 ELSE 0 END) AS archived,
|
||||
SUM(CASE WHEN (SELECT COUNT(*) FROM brand_set_effects WHERE brand_set_id=brand_sets.id)=3 THEN 1 ELSE 0 END) AS complete FROM brand_sets`)).get();
|
||||
const filter = ['active', 'archived', 'complete'].includes(url.searchParams.get('filter') ?? '') ? String(url.searchParams.get('filter')) : '';
|
||||
const where = filter === 'active' ? 'WHERE active=1' : filter === 'archived' ? 'WHERE active=0'
|
||||
: filter === 'complete' ? 'WHERE (SELECT COUNT(*) FROM brand_set_effects WHERE brand_set_id=brand_sets.id)=3' : '';
|
||||
const filtered = await (await database.prepare(`SELECT COUNT(*) AS total FROM brand_sets ${where}`)).get();
|
||||
const visiblePending = mappedPendingBrands.filter(() => filter !== 'archived');
|
||||
const { search, pattern } = getRegistrySearch(url);
|
||||
const conditions = [
|
||||
...(filter === 'active' ? ['active=1'] : filter === 'archived' ? ['active=0'] : filter === 'complete' ? ['(SELECT COUNT(*) FROM brand_set_effects WHERE brand_set_id=brand_sets.id)=3'] : []),
|
||||
...(search ? ["name LIKE ? ESCAPE '\\' COLLATE NOCASE"] : [])
|
||||
];
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const args = search ? [pattern] : [];
|
||||
const filtered = await (await database.prepare(`SELECT COUNT(*) AS total FROM brand_sets ${where}`)).get(...args);
|
||||
const visiblePending = mappedPendingBrands.filter((brand) => filter !== 'archived' && (!search || String(brand.name).toLocaleLowerCase().includes(search.toLocaleLowerCase())));
|
||||
const pagination = getPagination(url, Number(filtered?.total ?? 0) + visiblePending.length);
|
||||
const pendingOnPage = pagination.page === 1 ? visiblePending.slice(0, pagination.perPage) : [];
|
||||
const databaseOffset = Math.max(0, pagination.offset - visiblePending.length);
|
||||
const databaseLimit = Math.max(0, pagination.perPage - pendingOnPage.length);
|
||||
const [brands, effects, stats, effectSummary, icons] = await Promise.all([
|
||||
databaseLimit ? (await database.prepare(`SELECT * FROM brand_sets ${where} ORDER BY active DESC, name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(databaseLimit, databaseOffset) : [],
|
||||
databaseLimit ? (await database.prepare(`SELECT * FROM brand_sets ${where} ORDER BY active DESC, name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(...args, databaseLimit, databaseOffset) : [],
|
||||
(await database.prepare('SELECT brand_set_id,pieces_required,stat_attribute_id,value FROM brand_set_effects ORDER BY pieces_required')).all(),
|
||||
(await database.prepare('SELECT id,name,unit FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE')).all(),
|
||||
(await database.prepare('SELECT COUNT(*) AS total FROM brand_set_effects')).get(),
|
||||
@@ -55,7 +60,8 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
stats: [...pendingStats, ...stats],
|
||||
summary: { ...summary, total: Number(summary?.total ?? 0) + pendingBrands.length, pending: pendingBrands.length, effects: Number(effectSummary?.total ?? 0) },
|
||||
pagination,
|
||||
filter
|
||||
filter,
|
||||
search
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</PendingForm>
|
||||
</div>
|
||||
{/if}
|
||||
<RegistryPagination pagination={data.pagination}/>
|
||||
<RegistryPagination pagination={data.pagination} search={data.search}/>
|
||||
<div class="dashboard-table">
|
||||
<div class="dashboard-row brand-set-row table-heading"><span>BRAND SET</span><span>2 PIECES</span><span>3 PIECES</span><span>4+ PIECES</span><span>STATE</span><span></span></div>
|
||||
{#each data.brands as brand}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getDatabase } from '$lib/server/database';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { getPagination } from '$lib/server/pagination';
|
||||
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
|
||||
import { auditRecord, recordIsacAudit } from '$lib/server/audit';
|
||||
import { getPendingEntities } from '$lib/server/data-moderation';
|
||||
import { gameDataIconUrl } from '$lib/server/game-data-icons';
|
||||
@@ -33,10 +33,14 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
SUM(CASE WHEN slot IN ('Gloves','Knee Pads','Body Armor') THEN 1 ELSE 0 END) AS armored,
|
||||
SUM(CASE WHEN EXISTS (SELECT 1 FROM gear_talents WHERE gear_id=gear.id) THEN 1 ELSE 0 END) AS talented FROM gear`)).get();
|
||||
const filter = ['active', 'armored', 'talented'].includes(url.searchParams.get('filter') ?? '') ? String(url.searchParams.get('filter')) : '';
|
||||
const where = filter === 'active' ? 'WHERE active=1' : filter === 'armored' ? "WHERE slot IN ('Gloves','Knee Pads','Body Armor')"
|
||||
: filter === 'talented' ? 'WHERE EXISTS (SELECT 1 FROM gear_talents WHERE gear_id=gear.id)' : '';
|
||||
const filtered = await (await database.prepare(`SELECT COUNT(*) AS total FROM gear ${where}`)).get();
|
||||
const pagination = getPagination(url, Number(filtered?.total ?? 0));
|
||||
const { search, pattern } = getRegistrySearch(url);
|
||||
const conditions = [
|
||||
...(filter === 'active' ? ['active=1'] : filter === 'armored' ? ["slot IN ('Gloves','Knee Pads','Body Armor')"] : filter === 'talented' ? ['EXISTS (SELECT 1 FROM gear_talents WHERE gear_id=gear.id)'] : []),
|
||||
...(search ? ["name LIKE ? ESCAPE '\\' COLLATE NOCASE"] : [])
|
||||
];
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const args = search ? [pattern] : [];
|
||||
const filtered = await (await database.prepare(`SELECT COUNT(*) AS total FROM gear ${where}`)).get(...args);
|
||||
const pendingDefinitions = await getPendingEntities(user.id, [
|
||||
{ module: 'brands', action: 'save', kind: 'brand' },
|
||||
{ module: 'talents', action: 'saveTalent', kind: 'talent', fields: ['category'] },
|
||||
@@ -44,14 +48,15 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
{ module: 'gear', action: 'save', kind: 'gear', fields: ['slot', 'tier', 'healthMin', 'healthMax', 'armorMin', 'armorMax', 'brandIds', 'talentIds'] }
|
||||
]);
|
||||
const pendingGear = pendingDefinitions.filter((item) => item.kind === 'gear').filter((item) =>
|
||||
filter === 'armored' ? armoredSlots.has(String(item.slot)) : filter === 'talented' ? ['Backpack', 'Body Armor'].includes(String(item.slot)) : true
|
||||
(filter === 'armored' ? armoredSlots.has(String(item.slot)) : filter === 'talented' ? ['Backpack', 'Body Armor'].includes(String(item.slot)) : true)
|
||||
&& (!search || String(item.name).toLocaleLowerCase().includes(search.toLocaleLowerCase()))
|
||||
);
|
||||
const adjustedPagination = getPagination(url, Number(filtered?.total ?? 0) + pendingGear.length);
|
||||
const pendingGearOnPage = adjustedPagination.page === 1 ? pendingGear.slice(0, adjustedPagination.perPage) : [];
|
||||
const databaseOffset = Math.max(0, adjustedPagination.offset - pendingGear.length);
|
||||
const databaseLimit = Math.max(0, adjustedPagination.perPage - pendingGearOnPage.length);
|
||||
const [gear, brandLinks, talentLinks, brands, talents, augmentRows, augmentCosts, stats, brandIcons] = await Promise.all([
|
||||
databaseLimit ? (await database.prepare(`SELECT * FROM gear ${where} ORDER BY active DESC, slot, name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(databaseLimit, databaseOffset) : [],
|
||||
databaseLimit ? (await database.prepare(`SELECT * FROM gear ${where} ORDER BY active DESC, slot, name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(...args, databaseLimit, databaseOffset) : [],
|
||||
(await database.prepare('SELECT gear_id,brand_set_id FROM gear_brand_sets')).all(),
|
||||
(await database.prepare('SELECT gear_id,talent_id FROM gear_talents')).all(),
|
||||
(await database.prepare('SELECT id,name FROM brand_sets WHERE active=1 ORDER BY name COLLATE NOCASE')).all(),
|
||||
@@ -95,7 +100,8 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
stats: [...pendingStats, ...stats],
|
||||
summary: { ...summary, total: Number(summary?.total ?? 0) + pendingDefinitions.filter((item) => item.kind === 'gear').length, pending: pendingDefinitions.filter((item) => item.kind === 'gear').length },
|
||||
pagination: adjustedPagination,
|
||||
filter
|
||||
filter,
|
||||
search
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
</PendingForm>
|
||||
</div>
|
||||
{/if}
|
||||
<RegistryPagination pagination={data.pagination}/>
|
||||
<RegistryPagination pagination={data.pagination} search={data.search}/>
|
||||
<div class="dashboard-table">
|
||||
<div class="dashboard-row gear-row table-heading"><span>GEAR</span><span>SLOT</span><span>TIER</span><span>BASE RANGES</span><span>BRAND SETS</span><span>STATE</span><span></span></div>
|
||||
{#each data.gear as item}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { auditRecord, recordIsacAudit } from '$lib/server/audit';
|
||||
import { requireDataView, requirePermission } from '$lib/server/auth/authorization';
|
||||
import { getSessionUser } from '$lib/server/auth/session';
|
||||
import { getDatabase } from '$lib/server/database';
|
||||
import { getPagination } from '$lib/server/pagination';
|
||||
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
@@ -56,6 +56,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
? requestedFilter : '';
|
||||
const conditions: string[] = [];
|
||||
const args: unknown[] = [];
|
||||
const { search, pattern } = getRegistrySearch(url);
|
||||
if (filter === 'active' || filter === 'archived') conditions.push(`active=${filter === 'active' ? 1 : 0}`);
|
||||
else if (qualities.some((quality) => quality.toLowerCase() === filter)) {
|
||||
conditions.push('quality=?');
|
||||
@@ -64,6 +65,10 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
conditions.push('primary_attribute_type=?');
|
||||
args.push(attributeTypes.find((type) => type.toLowerCase() === filter));
|
||||
}
|
||||
if (search) {
|
||||
conditions.push("name LIKE ? ESCAPE '\\' COLLATE NOCASE");
|
||||
args.push(pattern);
|
||||
}
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const [summary, filtered] = await Promise.all([
|
||||
(await database.prepare(`SELECT COUNT(*) AS total,
|
||||
@@ -98,6 +103,8 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
filter === 'archived' ? false
|
||||
: qualities.some((quality) => quality.toLowerCase() === filter) ? String(item.quality).toLowerCase() === filter
|
||||
: attributeTypes.some((type) => type.toLowerCase() === filter) ? String(item.attributeType).toLowerCase() === filter : true
|
||||
).filter((item) =>
|
||||
!search || String(item.name).toLocaleLowerCase().includes(search.toLocaleLowerCase())
|
||||
).map((protocol) => ({
|
||||
...protocol,
|
||||
iconUrl: '',
|
||||
@@ -131,7 +138,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
) ?? { id: '', quality, primary_attribute_type: attributeType, effects: [] })),
|
||||
stats: [...pending.filter((item) => item.kind === 'attribute'), ...stats],
|
||||
triggers: [...pending.filter((item) => item.kind === 'trigger'), ...triggers],
|
||||
summary: { ...summary, total: Number(summary?.total ?? 0) + pending.filter((item) => item.kind === 'protocol').length, pending: pending.filter((item) => item.kind === 'protocol').length }, filter, pagination
|
||||
summary: { ...summary, total: Number(summary?.total ?? 0) + pending.filter((item) => item.kind === 'protocol').length, pending: pending.filter((item) => item.kind === 'protocol').length }, filter, pagination, search
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<RegistryPagination pagination={data.pagination}/>
|
||||
<RegistryPagination pagination={data.pagination} search={data.search}/>
|
||||
<div class="dashboard-table protocol-dashboard-table">
|
||||
<div class="dashboard-row protocol-row table-heading"><span>OS PROTOCOL</span><span>QUALITY</span><span>TYPE</span><span>UNIQUE PRIMARY</span><span>UNIQUE TALENT</span><span>STATE</span><span></span></div>
|
||||
{#each data.protocols as protocol}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getPermissions, requirePermission } from '$lib/server/auth/authorization';
|
||||
import { getSessionUser } from '$lib/server/auth/session';
|
||||
import { getDatabase } from '$lib/server/database';
|
||||
import { getPagination } from '$lib/server/pagination';
|
||||
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
@@ -15,12 +15,17 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
const canManageSystemAdmin = operatorPermissions.includes('*');
|
||||
const database = await getDatabase();
|
||||
const filter = filters.includes(url.searchParams.get('filter') ?? '') ? String(url.searchParams.get('filter')) : '';
|
||||
const { search, pattern } = getRegistrySearch(url);
|
||||
const permissionPrefix = filter === 'isac' ? 'isac.%' : filter === 'data' ? 'data.%' : filter === 'users' ? 'users.%' : filter === 'audit' ? 'audit.%' : '';
|
||||
const where = permissionPrefix ? `WHERE EXISTS (
|
||||
SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id
|
||||
WHERE rp.role_id=roles.id AND p.name LIKE ?
|
||||
)` : '';
|
||||
const filterArgs = permissionPrefix ? [permissionPrefix] : [];
|
||||
const conditions = [
|
||||
...(permissionPrefix ? [`EXISTS (
|
||||
SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id
|
||||
WHERE rp.role_id=roles.id AND p.name LIKE ?
|
||||
)`] : []),
|
||||
...(search ? ["roles.name LIKE ? ESCAPE '\\' COLLATE NOCASE"] : [])
|
||||
];
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const filterArgs = [...(permissionPrefix ? [permissionPrefix] : []), ...(search ? [pattern] : [])];
|
||||
const [summary, filtered, permissions] = await Promise.all([
|
||||
(await database.prepare(`SELECT COUNT(*) AS total,
|
||||
SUM(CASE WHEN EXISTS (SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=roles.id AND p.name LIKE 'isac.%') THEN 1 ELSE 0 END) AS isac,
|
||||
@@ -49,6 +54,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
permissions,
|
||||
summary,
|
||||
filter,
|
||||
search,
|
||||
pagination,
|
||||
canManageSystemAdmin
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<RegistryPagination pagination={data.pagination}/>
|
||||
<RegistryPagination pagination={data.pagination} search={data.search}/>
|
||||
<div class="dashboard-table">
|
||||
<div class="dashboard-row role-row table-heading"><span>ROLE</span><span>ASSIGNED USERS</span><span>DISCRETE PERMISSIONS</span><span>STATE</span><span></span></div>
|
||||
{#each data.roles as role}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getSessionUser } from '$lib/server/auth/session';
|
||||
import { getDatabase } from '$lib/server/database';
|
||||
import { getPendingEntities } from '$lib/server/data-moderation';
|
||||
import { applyIconMutation, gameDataIconMetadata, gameDataIconUrl, GameDataIconValidationError, readIconMutation } from '$lib/server/game-data-icons';
|
||||
import { getPagination } from '$lib/server/pagination';
|
||||
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
@@ -46,24 +46,28 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
const database = await getDatabase();
|
||||
const requestedFilter = url.searchParams.get('filter') ?? '';
|
||||
const filter = ['active', 'archived', 'configured'].includes(requestedFilter) ? requestedFilter : '';
|
||||
const where = filter === 'active' ? 'WHERE sets.active=1'
|
||||
: filter === 'archived' ? 'WHERE sets.active=0'
|
||||
: filter === 'configured' ? `WHERE EXISTS (SELECT 1 FROM skill_chip_two_piece_bonuses bonus WHERE bonus.skill_chip_set_id=sets.id)
|
||||
AND EXISTS (SELECT 1 FROM skill_chip_talents talent WHERE talent.skill_chip_set_id=sets.id)` : '';
|
||||
const { search, pattern } = getRegistrySearch(url);
|
||||
const conditions = [
|
||||
...(filter === 'active' ? ['sets.active=1'] : filter === 'archived' ? ['sets.active=0']
|
||||
: filter === 'configured' ? ['EXISTS (SELECT 1 FROM skill_chip_two_piece_bonuses bonus WHERE bonus.skill_chip_set_id=sets.id)', 'EXISTS (SELECT 1 FROM skill_chip_talents talent WHERE talent.skill_chip_set_id=sets.id)'] : []),
|
||||
...(search ? ["sets.name LIKE ? ESCAPE '\\' COLLATE NOCASE"] : [])
|
||||
];
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const args = search ? [pattern] : [];
|
||||
const [summary, filtered, pendingStats, pendingTriggers, pendingSets] = await Promise.all([
|
||||
(await database.prepare(`SELECT COUNT(*) AS total,
|
||||
SUM(CASE WHEN active=1 THEN 1 ELSE 0 END) AS active,
|
||||
SUM(CASE WHEN active=0 THEN 1 ELSE 0 END) AS archived,
|
||||
SUM(CASE WHEN EXISTS (SELECT 1 FROM skill_chip_talents talent WHERE talent.skill_chip_set_id=skill_chip_sets.id) THEN 1 ELSE 0 END) AS configured
|
||||
FROM skill_chip_sets`)).get(),
|
||||
(await database.prepare(`SELECT COUNT(*) AS total FROM skill_chip_sets sets ${where}`)).get(),
|
||||
(await database.prepare(`SELECT COUNT(*) AS total FROM skill_chip_sets sets ${where}`)).get(...args),
|
||||
getPendingEntities(user.id, [{ module: 'attributes', action: 'save', kind: 'stat', fields: ['unit', 'chipMin', 'chipMax'] }]),
|
||||
getPendingEntities(user.id, [{ module: 'talents', action: 'saveTrigger', kind: 'trigger', fields: ['description'] }]),
|
||||
getPendingEntities(user.id, [{ module: 'skillchips', action: 'save', kind: 'skillchip', fields: ['twoPieceStatId', 'twoPieceMin', 'twoPieceMax', 'primaryStatIds', 'secondaryStatIds', 'talentName', 'descriptionTemplate', 'talentEffects', 'triggerIds'] }])
|
||||
]);
|
||||
const pagination = getPagination(url, Number(filtered?.total ?? 0));
|
||||
const [sets, twoPiece, primaryOptions, secondaryOptions, talents, talentTriggers, talentEffects, stats, triggers, icons] = await Promise.all([
|
||||
(await database.prepare(`SELECT sets.* FROM skill_chip_sets sets ${where} ORDER BY sets.active DESC,sets.name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(pagination.perPage, pagination.offset),
|
||||
(await database.prepare(`SELECT sets.* FROM skill_chip_sets sets ${where} ORDER BY sets.active DESC,sets.name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(...args, pagination.perPage, pagination.offset),
|
||||
(await database.prepare('SELECT * FROM skill_chip_two_piece_bonuses')).all(),
|
||||
(await database.prepare('SELECT * FROM skill_chip_primary_options')).all(),
|
||||
(await database.prepare('SELECT * FROM skill_chip_secondary_options')).all(),
|
||||
@@ -80,7 +84,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
skill_chip_min: item.chipMin === '' ? null : Number(item.chipMin),
|
||||
skill_chip_max: item.chipMax === '' ? null : Number(item.chipMax)
|
||||
})), ...stats];
|
||||
const pendingSetRows = filter === 'archived' ? [] : pendingSets.map((set) => ({
|
||||
const pendingSetRows = filter === 'archived' ? [] : pendingSets.filter((set) => !search || String(set.name).toLocaleLowerCase().includes(search.toLocaleLowerCase())).map((set) => ({
|
||||
...set,
|
||||
iconUrl: '',
|
||||
twoPiece: {
|
||||
@@ -117,7 +121,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
stats: availableStats,
|
||||
rollStats: availableStats.filter((stat) => stat.skill_chip_min !== null && stat.skill_chip_max !== null),
|
||||
triggers: [...pendingTriggers, ...triggers],
|
||||
summary: { ...summary, total: Number(summary?.total ?? 0) + pendingSets.length, pending: pendingSets.length }, filter, pagination
|
||||
summary: { ...summary, total: Number(summary?.total ?? 0) + pendingSets.length, pending: pendingSets.length }, filter, pagination, search
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<RegistryPagination pagination={data.pagination}/>
|
||||
<RegistryPagination pagination={data.pagination} search={data.search}/>
|
||||
<div class="dashboard-table">
|
||||
<div class="dashboard-row skill-chip-row table-heading"><span>SKILL CHIP</span><span>QUALITY</span><span>2-PIECE BONUS</span><span>3-PIECE TALENT</span><span>ROLL POOLS</span><span>STATE</span><span></span></div>
|
||||
{#each data.sets as item}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getDatabase } from '$lib/server/database';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { getPagination } from '$lib/server/pagination';
|
||||
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
|
||||
import { auditRecord, recordIsacAudit } from '$lib/server/audit';
|
||||
import { applyIconMutation, gameDataIconMetadata, gameDataIconUrl, GameDataIconValidationError, readIconMutation } from '$lib/server/game-data-icons';
|
||||
import { getPendingEntities } from '$lib/server/data-moderation';
|
||||
@@ -16,15 +16,20 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
const db = await getDatabase();
|
||||
const totalRow = await (await db.prepare('SELECT COUNT(*) AS total FROM talents')).get();
|
||||
const filter = ['slot1', 'slot2', 'exotic', 'backpack', 'body-armor'].includes(url.searchParams.get('filter') ?? '') ? String(url.searchParams.get('filter')) : '';
|
||||
const where = filter === 'slot1' ? 'WHERE EXISTS (SELECT 1 FROM weapon_talent_slots WHERE talent_id=talents.id AND slot=1)'
|
||||
: filter === 'slot2' ? 'WHERE EXISTS (SELECT 1 FROM weapon_talent_slots WHERE talent_id=talents.id AND slot=2)'
|
||||
: filter === 'exotic' ? "WHERE EXISTS (SELECT 1 FROM talent_weapon_scopes WHERE talent_id=talents.id AND scope='Exotic')"
|
||||
: filter === 'backpack' ? "WHERE category='Backpack'"
|
||||
: filter === 'body-armor' ? "WHERE category IN ('Vest','Body Armor')" : '';
|
||||
const filteredRow = await (await db.prepare(`SELECT COUNT(*) AS total FROM talents ${where}`)).get();
|
||||
const { search, pattern } = getRegistrySearch(url);
|
||||
const conditions = [
|
||||
...(filter === 'slot1' ? ['EXISTS (SELECT 1 FROM weapon_talent_slots WHERE talent_id=talents.id AND slot=1)']
|
||||
: filter === 'slot2' ? ['EXISTS (SELECT 1 FROM weapon_talent_slots WHERE talent_id=talents.id AND slot=2)']
|
||||
: filter === 'exotic' ? ["EXISTS (SELECT 1 FROM talent_weapon_scopes WHERE talent_id=talents.id AND scope='Exotic')"]
|
||||
: filter === 'backpack' ? ["category='Backpack'"] : filter === 'body-armor' ? ["category IN ('Vest','Body Armor')"] : []),
|
||||
...(search ? ["name LIKE ? ESCAPE '\\' COLLATE NOCASE"] : [])
|
||||
];
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const args = search ? [pattern] : [];
|
||||
const filteredRow = await (await db.prepare(`SELECT COUNT(*) AS total FROM talents ${where}`)).get(...args);
|
||||
const pagination = getPagination(url, Number(filteredRow?.total ?? 0));
|
||||
const [talents, tiers, triggers, links, effects, stats, templates, weaponScopes, weaponSlots, exoticOwners, icons] = await Promise.all([
|
||||
(await db.prepare(`SELECT * FROM talents ${where} ORDER BY active DESC,name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(pagination.perPage, pagination.offset),
|
||||
(await db.prepare(`SELECT * FROM talents ${where} ORDER BY active DESC,name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(...args, pagination.perPage, pagination.offset),
|
||||
(await db.prepare('SELECT id,talent_id,tier,description FROM talent_tiers')).all(),
|
||||
(await db.prepare('SELECT * FROM talent_triggers ORDER BY active DESC,name COLLATE NOCASE')).all(),
|
||||
(await db.prepare('SELECT talent_tier_id,trigger_id FROM talent_tier_triggers')).all(),
|
||||
@@ -60,6 +65,8 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
: filter === 'backpack' ? item.category === 'Backpack'
|
||||
: filter === 'body-armor' ? item.category === 'Body Armor'
|
||||
: filter === 'exotic' ? false : true
|
||||
).filter((item) =>
|
||||
!search || String(item.name).toLocaleLowerCase().includes(search.toLocaleLowerCase())
|
||||
).map((item) => ({
|
||||
...item,
|
||||
iconUrl: '',
|
||||
@@ -107,7 +114,8 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
bodyArmor: Number(gearSummary?.body_armor ?? 0)
|
||||
},
|
||||
pagination,
|
||||
filter
|
||||
filter,
|
||||
search
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -97,5 +97,5 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<RegistryPagination pagination={data.pagination}/>
|
||||
<RegistryPagination pagination={data.pagination} search={data.search}/>
|
||||
<div class="dashboard-table talent-dashboard-table"><div class="dashboard-row talent-row table-heading"><span>TALENT</span><span>CATEGORY</span><span>TIER I EFFECT</span><span>TIER II EFFECT</span><span>STATE</span><span></span></div>{#each data.talents as item}<div class="dashboard-row talent-row" class:pending-record={item.pending}><div class="registry-icon-identity">{#if item.iconUrl}<img src={item.iconUrl} alt=""/>{:else}<span>◇</span>{/if}<div><strong>{item.name}</strong><small>{item.pending ? `PROPOSED BY ${item.proposerName}` : item.category === 'Exotic Weapon' ? `OWNED BY ${item.ownerWeaponName ?? 'UNASSIGNED WEAPON'}` : 'Tiered talent'}</small>{#if cooldownLine(item.cooldown_seconds)}<small class="talent-cooldown">{cooldownLine(item.cooldown_seconds)}</small>{/if}</div></div><span>{item.category}</span><p class="talent-description" class:unconfigured={!configured(item.tiers, 'I')}>{tierDescription(item.tiers, 'I')}</p><p class="talent-description" class:unconfigured={!configured(item.tiers, 'II')}>{tierDescription(item.tiers, 'II')}</p><b class:pending={item.pending} class="state-dot">{item.pending ? 'PENDING' : 'ACTIVE'}</b>{#if item.pending}<span></span>{:else if item.category === 'Exotic Weapon'}{#if item.ownerWeaponId}<a class="row-action" href={`/isac/weapons?weapon=${item.ownerWeaponId}`}>EDIT WEAPON</a>{:else}<span></span>{/if}{:else}<button onclick={() => editTalent(item)}>EDIT</button>{/if}</div>{/each}</div>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
const date = (value: unknown) => value ? new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(String(value))) : 'NEVER';
|
||||
</script>
|
||||
<svelte:head><title>User operations — ISAC</title></svelte:head>
|
||||
<div class="module-toolbar"><div><p class="eyebrow">USER OPERATIONS</p><h2>AGENT REGISTRY</h2><p>Identity, authentication methods, access state, recent activity, and community contribution overview.</p></div><form class="user-search"><input name="q" value={data.query} placeholder="SEARCH USERNAME"/><button class="secondary">SEARCH</button></form></div>
|
||||
<div class="module-toolbar"><div><p class="eyebrow">USER OPERATIONS</p><h2>AGENT REGISTRY</h2><p>Identity, authentication methods, access state, recent activity, and community contribution overview.</p></div></div>
|
||||
<KpiFilters active={data.filter} items={[{value:'',label:'RESULTS',count:data.summary.results,description:'Show current query'},{value:'active',label:'ACTIVE',count:data.summary.active,description:'Active within 30 days'},{value:'inactive',label:'INACTIVE',count:data.summary.inactive,description:'No activity for 30 days'},{value:'restricted',label:'RESTRICTED',count:data.summary.restricted,description:'Blocked, banned, or deleted'}]}/>
|
||||
<RegistryPagination pagination={data.pagination}/>
|
||||
<RegistryPagination pagination={data.pagination} search={data.query} searchPlaceholder="SEARCH DISPLAY OR LOGIN NAME"/>
|
||||
<div class="dashboard-table">
|
||||
<div class="dashboard-row user-row table-heading"><span>USER</span><span>ACCESS</span><span>ROLES</span><span>LAST LOGIN</span><span>LAST ACTIVE</span><span>CONTENT</span><span></span></div>
|
||||
{#each data.users as user}<div class="dashboard-row user-row mobile-detail-row"><div><strong>{user.display_name}</strong><small>LOGIN: {user.username ?? 'NOT CONFIGURED'}</small></div><span class:restricted={user.accessState !== 'ACTIVE'}>{user.accessState}{user.inactive && user.accessState === 'ACTIVE' ? ' · INACTIVE' : ''}</span><div class="role-summary">{#each String(user.assigned_roles ?? user.role ?? 'User').split(' · ') as role}<b>{role.toUpperCase()}</b>{/each}</div><span><small>{user.last_login_provider?.toUpperCase() ?? 'NONE'}</small>{date(user.last_login_at)}</span><span>{date(user.last_active_at)}</span><span>{user.build_count} BUILDS · {user.comment_count} COMMENTS</span><a class="row-action" href={`/isac/users/${user.id}`}>VIEW</a><a class="mobile-row-open" href={`/isac/users/${user.id}`} aria-label={`View agent ${user.display_name}`}></a></div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getDatabase } from '$lib/server/database';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { getPagination } from '$lib/server/pagination';
|
||||
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
|
||||
import { auditRecord, recordIsacAudit } from '$lib/server/audit';
|
||||
import { applyIconMutation, gameDataIconMetadata, gameDataIconUrl, GameDataIconValidationError, readIconMutation } from '$lib/server/game-data-icons';
|
||||
import { getPendingEntities } from '$lib/server/data-moderation';
|
||||
@@ -35,8 +35,13 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
SUM(CASE WHEN is_exotic=1 THEN 1 ELSE 0 END) AS exotic FROM weapons`)).get();
|
||||
const filter = ['active', 'standard', 'exotic'].includes(url.searchParams.get('filter') ?? '') ? String(url.searchParams.get('filter')) : '';
|
||||
const requestedWeaponId = url.searchParams.get('weapon')?.trim() ?? '';
|
||||
const where = requestedWeaponId ? 'WHERE id=?' : filter === 'active' ? 'WHERE active=1' : filter === 'standard' ? 'WHERE is_exotic=0' : filter === 'exotic' ? 'WHERE is_exotic=1' : '';
|
||||
const whereArgs = requestedWeaponId ? [requestedWeaponId] : [];
|
||||
const { search, pattern } = getRegistrySearch(url);
|
||||
const conditions = [
|
||||
...(requestedWeaponId ? ['id=?'] : filter === 'active' ? ['active=1'] : filter === 'standard' ? ['is_exotic=0'] : filter === 'exotic' ? ['is_exotic=1'] : []),
|
||||
...(!requestedWeaponId && search ? ["name LIKE ? ESCAPE '\\' COLLATE NOCASE"] : [])
|
||||
];
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const whereArgs = [...(requestedWeaponId ? [requestedWeaponId] : []), ...(!requestedWeaponId && search ? [pattern] : [])];
|
||||
const filtered = await (await database.prepare(`SELECT COUNT(*) AS total FROM weapons ${where}`)).get(...whereArgs);
|
||||
const pagination = getPagination(url, Number(filtered?.total ?? 0));
|
||||
const [weapons, tierRanges, talentOptions, augmentEffects, baseBonuses, talent1Options, talent2Options, stats, triggers, exoticTemplates, exoticTiers, exoticTierTriggers, exoticTierEffects, talentIcons, pending] = await Promise.all([
|
||||
@@ -77,6 +82,8 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
requestedWeaponId ? item.id === requestedWeaponId
|
||||
: filter === 'standard' ? !item.isExotic
|
||||
: filter === 'exotic' ? Boolean(item.isExotic) : true
|
||||
).filter((item) =>
|
||||
requestedWeaponId || !search || String(item.name).toLocaleLowerCase().includes(search.toLocaleLowerCase())
|
||||
).map((weapon) => ({
|
||||
...weapon,
|
||||
native_tier: Number(weapon.nativeTier),
|
||||
@@ -133,6 +140,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
summary: { ...summary, total: Number(summary?.total ?? 0) + pending.filter((item) => item.kind === 'weapon').length, pending: pending.filter((item) => item.kind === 'weapon').length },
|
||||
pagination,
|
||||
filter,
|
||||
search,
|
||||
requestedWeaponId
|
||||
};
|
||||
};
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<RegistryPagination pagination={data.pagination}/>
|
||||
<RegistryPagination pagination={data.pagination} search={data.search}/>
|
||||
<div class="dashboard-table weapon-dashboard-table">
|
||||
<div class="dashboard-row weapon-row table-heading"><span>WEAPON</span><span>CLASS</span><span>FIXED ATTRIBUTES</span><span>TIER RANGE</span><span>TALENT POOLS</span><span>AUGMENTS</span><span>STATE</span><span></span></div>
|
||||
{#each data.weapons as weapon}
|
||||
|
||||
Reference in New Issue
Block a user