diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index d4068b8..397e37b 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -239,3 +239,20 @@ creating a new record keeps the editor open with a fresh blank definition for rapid consecutive entry. Failed validation leaves the current editor and entered values intact. Use a unique action result token so reactive handling runs exactly once and does not create update loops. + +Tiered-definition registries should show their rendered tier effects when the +text is operationally useful. Use compact wrapping description cells rather +than replacing meaningful data with a generic “configured” label; retain an +explicit textual missing state when a tier has no rendered description. + +### Registry pagination + +Every primary ISAC registry uses server-side pagination. The shared control +appears immediately above and visually attached to its table. It shows the +visible record range and total, previous/next navigation, a compact +window of page numbers, and row-count presets of 10, 25, 50, and 100. Existing +filter query parameters must survive page navigation, while changing page size +returns to page one. Summary metrics remain global or filter-global and must +never silently describe only the visible page. On narrow screens, retain +previous, next, current page, and page-size controls before optional page +numbers. diff --git a/src/app.css b/src/app.css index e1293d5..d2b1fea 100644 --- a/src/app.css +++ b/src/app.css @@ -75,10 +75,16 @@ .gear-identity{display:grid;grid-template-columns:2fr 1fr .65fr .65fr;gap:8px}.gear-ranges{display:grid;grid-template-columns:1fr 1fr;gap:8px}.range-band{grid-template-columns:58px 1fr 1fr}.effect-band.unavailable{opacity:.4}.selection-head{display:flex;justify-content:space-between;align-items:end;margin:14px 0 6px}.selection-head>div{display:grid}.selection-head span{font-size:8px;font-weight:bold;letter-spacing:1.2px}.selection-head small{font-size:7px;color:#697174;margin-top:3px}.selection-head b{color:#f6a821;font-size:8px;letter-spacing:1px}.selection-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border:1px solid #303536}.selection-grid label{min-height:38px;display:grid;grid-template-columns:20px 1fr auto;align-items:center;gap:5px;padding:6px 8px;border:1px solid transparent;border-right-color:#282d2e;border-bottom-color:#282d2e;cursor:pointer;font-size:9px}.selection-grid label:hover{background:#171b1c;border-color:#4b4f50}.selection-grid input{width:auto;margin:0;accent-color:#f6a821}.selection-grid small{font-size:6px;color:#646c6e;letter-spacing:.8px}.selection-grid input:checked~small{color:#75c08c}.gear-row{min-width:1050px;grid-template-columns:1.2fr .8fr .8fr 1.35fr .8fr .6fr .3fr}.gear-row>span small{font-size:8px;color:#858b8d} .unit-field{min-width:0;margin:8px 0;padding:0;border:0}.unit-field legend{font-size:8px;font-weight:bold;letter-spacing:1px;color:#858b8d}.unit-picker{display:grid;grid-template-columns:repeat(5,1fr);height:32px;margin-top:5px;border:1px solid #424849}.unit-picker label{display:grid;place-items:center;border-left:1px solid #303536;cursor:pointer}.unit-picker label:first-child{border-left:0}.unit-picker input{position:absolute;opacity:0;pointer-events:none}.unit-picker span{width:100%;height:100%;display:grid;place-items:center;color:#777f81;font-size:8px;font-weight:bold}.unit-picker label:hover span{background:#191d1e;color:#ecebe5}.unit-picker input:checked+span{background:#2b2111;color:#f6a821;box-shadow:inset 0 -2px #f6a821}.unit-picker input:focus-visible+span{outline:2px solid #f6a821;outline-offset:2px} .template-field{display:grid;margin-top:10px}.template-field small{margin-top:5px;color:#697174;font-size:7px}.tier-preview{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:8px 0 12px}.tier-preview>div{padding:9px 11px;border:1px solid #343a3b;background:#101314}.tier-preview span,.effect-heading{color:#697174;font-size:7px;letter-spacing:1.1px}.tier-preview p{min-height:30px;margin:5px 0 0;color:#d9d9d3;font-size:10px;line-height:1.45}.shared-trigger-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));max-height:170px}.shared-effects{border:1px solid #303536}.shared-effect{display:grid;grid-template-columns:minmax(180px,1fr) 120px 120px 30px;gap:6px;align-items:center;padding:6px;border-top:1px solid #292f30}.shared-effect:first-child{border-top:0}.shared-effect select,.shared-effect input{min-width:0;margin:0;padding:7px;font-size:9px}.shared-effect button{height:30px;border:1px solid #563734;background:#1d1212;color:#da746c}.effect-heading{min-height:28px;background:#0d1011}.effect-heading span:last-child{display:none} +.talent-row{grid-template-columns:minmax(150px,.8fr) minmax(105px,.45fr) minmax(260px,1.35fr) minmax(260px,1.35fr) 80px 45px;min-width:980px}.talent-row:not(.table-heading){align-items:start;padding-top:10px;padding-bottom:10px}.talent-description{margin:0;color:#b7bcba;font-size:9px;line-height:1.45;white-space:normal}.talent-description.unconfigured{color:#df8069;font-size:7px;letter-spacing:.8px}.talent-row .state-dot{margin-top:2px}.talent-row>button{margin-top:-3px} +.talent-kpis{grid-template-columns:repeat(6,minmax(105px,1fr))} +@media(max-width:1200px){.talent-kpis{grid-template-columns:repeat(3,minmax(105px,1fr))}}@media(max-width:650px){.talent-kpis{grid-template-columns:repeat(2,minmax(105px,1fr))}} .section-label{margin-top:10px;border-top:1px solid #2c3132}.user-search{display:flex;gap:6px}.user-search input{height:40px;min-width:240px;margin:0;padding:0 10px;font-size:9px}.user-search button{height:40px}.user-row{min-width:1020px;grid-template-columns:1.1fr .75fr 1.1fr 1.1fr 1fr .3fr}.restricted{color:#df8069!important}.row-action{justify-self:end;color:#f6a821;font-size:8px;font-weight:bold}.button-link{display:grid;place-items:center;height:40px;padding:0 14px;border:1px solid #424849;color:#d7d9d5;font-size:8px;font-weight:bold}.metric-date{font-size:11px!important}.user-console-grid{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:10px}.user-console-grid .dashboard-editor{margin-top:0}.permission-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.moderation-panel .field{display:block}.console-note{padding:20px;color:#747c7e;font-size:9px}.user-history-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.user-history-grid>section{min-width:0;border:1px solid #303536;background:#111415}.history-head{display:flex;justify-content:space-between;padding:10px;background:#0d1011;color:#727a7c;font-size:7px;letter-spacing:1px}.history-list{max-height:300px;overflow:auto}.history-list>div{display:grid;grid-template-columns:1fr auto auto;gap:8px;padding:9px 10px;border-top:1px solid #292e2f;font-size:8px}.history-list span{color:#767e80}.history-list b{font-size:7px}.history-list>p{padding:28px;text-align:center;color:#626a6c;font-size:8px}.comment-history>div{grid-template-columns:1fr}.comment-history span{white-space:normal;line-height:1.4} @media(max-width:1000px){.user-console-grid,.user-history-grid{grid-template-columns:1fr}.permission-grid{grid-template-columns:1fr 1fr}}@media(max-width:560px){.user-search{width:100%}.user-search input{min-width:0;flex:1}.permission-grid{grid-template-columns:1fr}} @media(max-width:1100px){.effect-editor{grid-template-columns:1fr}.selection-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:900px){.gear-identity,.gear-ranges{grid-template-columns:1fr 1fr}}@media(max-width:560px){.effect-band{grid-template-columns:48px 1fr}.effect-band .field:last-child{grid-column:2}.split-actions{align-items:stretch}.split-actions,.split-actions>div{width:100%}.split-actions>div:last-child button{flex:1}.gear-identity,.gear-ranges,.selection-grid{grid-template-columns:1fr}.range-band{grid-template-columns:48px 1fr}.range-band .field:last-child{grid-column:2}} .weapon-editor{border-top-color:#d07a2c}.weapon-identity-grid{display:grid;grid-template-columns:1.6fr repeat(5,minmax(0,1fr));gap:8px}.weapon-fixed-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px}.exotic-toggle>span{min-height:32px;margin-top:5px;padding:0 9px;display:flex;align-items:center;gap:7px;border:1px solid #363b3c;background:#0d0f10;color:#d3d5d1;font-size:8px}.exotic-toggle input{width:auto;margin:0;accent-color:#f6a821}.weapon-tier-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:9px}.weapon-tier-card{min-width:0;margin:0;padding:7px;border:1px solid #3b4142;background:#101314}.weapon-tier-card legend{padding:0 7px;color:#f6a821;font-size:8px;font-weight:900;letter-spacing:1.2px}.weapon-range-row{display:grid;grid-template-columns:55px 1fr 1fr;gap:5px;align-items:end;padding:5px;border-top:1px solid #282e2f}.weapon-range-row:first-of-type{border-top:0}.weapon-range-row>strong{align-self:center;color:#d7d9d5;font-size:9px}.weapon-range-row>strong small{display:block;color:#596164;font-size:5px;letter-spacing:.5px}.weapon-range-row label{color:#737a7c;font-size:6px;letter-spacing:.7px}.weapon-range-row input{margin-top:3px;padding:7px;font-size:8px}.weapon-talent-pools{display:grid;grid-template-columns:1fr 1fr;gap:9px}.compact-selection{grid-template-columns:repeat(2,minmax(0,1fr));max-height:210px;overflow:auto}.weapon-base-bonuses,.weapon-augment-effects{display:grid;gap:5px;margin-top:7px}.base-bonus-row,.augment-effect{display:grid;grid-template-columns:35px minmax(180px,1fr) 130px 30px;gap:6px;align-items:center;padding:5px;border:1px solid #303536;background:#101314}.base-bonus-row>strong,.augment-effect>strong{height:100%;display:grid;place-items:center;background:#171b1c;color:#f6a821;font-size:8px}.base-bonus-row .field,.augment-effect .field{margin:0}.base-bonus-row button,.augment-effect button{height:30px;border:1px solid #563734;background:#1d1212;color:#da746c;cursor:pointer}.base-bonus-row button:disabled{opacity:.3;cursor:not-allowed}.augment-effect:not(:has(>button)){grid-template-columns:35px minmax(180px,1fr) 130px}.augment-sequence{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;margin-top:7px}.augment-sequence>div{min-height:42px;display:grid;place-items:center;padding:5px;border:1px solid #303536;background:#0d1011}.augment-sequence b{color:#f6a821;font-size:8px}.augment-sequence span{color:#727a7c;font-size:7px}.augment-sequence .exotic-step{border-color:#875d25;background:#211a0e}.weapon-row{min-width:1180px;grid-template-columns:1.3fr .7fr 1.2fr .6fr .6fr .5fr .5fr .3fr}.weapon-row>span small{color:#596164}.weapon-dashboard-table{margin-top:10px} .assignment-picker{border:1px solid #303536;background:#0d1011}.assignment-selected{display:grid;grid-template-columns:70px 1fr;min-height:45px;border-bottom:1px solid #303536}.assignment-summary{display:grid;place-items:center;background:#15191a}.assignment-summary span{color:#687073;font-size:6px;letter-spacing:1px}.assignment-summary b{color:#f6a821;font-size:14px}.assignment-chips{display:flex;align-items:center;gap:5px;overflow:auto;padding:6px}.assignment-chips>button{flex:0 0 auto;display:flex;align-items:center;gap:7px;padding:6px 8px;border:1px solid #6b5128;background:#211a0e;color:#e2cda7;font-size:7px;cursor:pointer}.assignment-chips>button:hover{border-color:#f6a821;color:#f6a821}.assignment-chips>button:active{transform:translateY(1px)}.assignment-chips small{color:#596164;font-size:7px}.assignment-search{display:block;padding:7px 8px;color:#70787a;font-size:6px;letter-spacing:1px}.assignment-search input{height:32px;margin-top:5px;padding:0 9px;font-size:9px}.assignment-results-head{height:27px;display:flex;align-items:center;justify-content:space-between;padding:0 9px;border-top:1px solid #262b2c;border-bottom:1px solid #262b2c;color:#687073;font-size:6px;letter-spacing:1px}.assignment-results-head b{color:#8a672b}.assignment-results{max-height:210px;overflow:auto}.assignment-results>button{width:100%;min-height:34px;display:flex;align-items:center;justify-content:space-between;padding:6px 9px;border:0;border-top:1px solid #24292a;background:#111415;color:#a4aaac;text-align:left;font-size:8px;cursor:pointer}.assignment-results>button:first-child{border-top:0}.assignment-results>button:hover,.assignment-results>button:focus-visible{background:#171b1c;color:#fff;box-shadow:inset 2px 0 #f6a821}.assignment-results>button.selected{background:#211a0e;color:#f6a821;box-shadow:inset 2px 0 #f6a821}.assignment-results>button b{font-size:6px;letter-spacing:.7px}.assignment-results>button:active{transform:translateY(1px);filter:brightness(.88)}.assignment-results>p{margin:0;padding:25px;text-align:center;color:#596164;font-size:7px;letter-spacing:1px} +.registry-pagination{width:100%;height:auto;min-height:48px;margin:8px 0 0;padding:7px 9px;display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:12px;border:1px solid #303536;border-bottom:0;background:#0f1213}.pagination-range{display:flex;align-items:center;gap:6px;color:#697174;font-size:7px;letter-spacing:.8px}.pagination-range b{color:#d7d9d5;font-size:9px}.pagination-pages{display:flex;align-items:center;justify-content:center;gap:4px}.pagination-pages a,.pagination-sizes a{min-width:30px;height:30px;display:grid;place-items:center;border:1px solid #343a3b;background:#111415;color:#8c9395;font-size:7px;font-weight:900;letter-spacing:.5px;cursor:pointer}.pagination-pages a:first-child,.pagination-pages a:last-child{min-width:58px;padding:0 8px}.pagination-pages a:hover,.pagination-pages a:focus-visible,.pagination-sizes a:hover,.pagination-sizes a:focus-visible{border-color:#8a672b;color:#f6a821}.pagination-pages a.active,.pagination-sizes a.active{border-color:#f6a821;background:#211a0e;color:#f6a821}.pagination-pages a:active,.pagination-sizes a:active{transform:translateY(1px);filter:brightness(.88)}.pagination-pages a.disabled{opacity:.35;pointer-events:none}.pagination-gap{color:#555d5f;font-size:8px}.pagination-sizes{display:flex;align-items:center;gap:4px}.pagination-sizes>span{margin-right:3px;color:#697174;font-size:7px;letter-spacing:1px}.pagination-sizes a{min-width:34px}.registry-pagination a:focus-visible{outline:2px solid #f6a821;outline-offset:2px} +.registry-pagination+.dashboard-table{margin-top:0} +@media(max-width:900px){.registry-pagination{grid-template-columns:1fr}.pagination-range,.pagination-pages,.pagination-sizes{justify-content:center}}@media(max-width:560px){.pagination-pages a:not(:first-child):not(:last-child):not(.active),.pagination-gap{display:none}.pagination-sizes>span{display:none}} @media(max-width:1200px){.weapon-identity-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.weapon-fixed-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.weapon-tier-grid{grid-template-columns:1fr}.weapon-talent-pools{grid-template-columns:1fr}}@media(max-width:700px){.weapon-identity-grid,.weapon-fixed-grid{grid-template-columns:1fr 1fr}.compact-selection{grid-template-columns:1fr}.base-bonus-row,.augment-effect,.augment-effect:not(:has(>button)){grid-template-columns:30px 1fr}.base-bonus-row .field:nth-of-type(2),.augment-effect .field:nth-of-type(2){grid-column:2}.base-bonus-row>button,.augment-effect>button{grid-column:2}.augment-sequence{grid-template-columns:1fr 1fr}}@media(max-width:480px){.weapon-identity-grid,.weapon-fixed-grid{grid-template-columns:1fr}.weapon-range-row{grid-template-columns:45px 1fr 1fr}} diff --git a/src/lib/components/RegistryPagination.svelte b/src/lib/components/RegistryPagination.svelte new file mode 100644 index 0000000..3cb036b --- /dev/null +++ b/src/lib/components/RegistryPagination.svelte @@ -0,0 +1,29 @@ + + + diff --git a/src/lib/server/pagination.ts b/src/lib/server/pagination.ts new file mode 100644 index 0000000..ceaedd5 --- /dev/null +++ b/src/lib/server/pagination.ts @@ -0,0 +1,18 @@ +import type { URL } from 'node:url'; + +export const PAGE_SIZE_PRESETS = [10, 25, 50, 100] as const; + +export function getPagination(url: URL, total: number) { + const requestedSize = Number(url.searchParams.get('perPage')); + const perPage = PAGE_SIZE_PRESETS.includes(requestedSize as (typeof PAGE_SIZE_PRESETS)[number]) ? requestedSize : 25; + const pageCount = Math.max(1, Math.ceil(total / perPage)); + const requestedPage = Number(url.searchParams.get('page')); + const page = Math.min(pageCount, Math.max(1, Number.isInteger(requestedPage) ? requestedPage : 1)); + return { + page, + perPage, + total, + pageCount, + offset: (page - 1) * perPage + }; +} diff --git a/src/routes/admin/brands/+page.server.ts b/src/routes/admin/brands/+page.server.ts index 786e7d6..1af5214 100644 --- a/src/routes/admin/brands/+page.server.ts +++ b/src/routes/admin/brands/+page.server.ts @@ -4,20 +4,27 @@ 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'; const thresholds = [2, 3, 4] as const; -export const load: PageServerLoad = async ({ cookies }) => { +export const load: PageServerLoad = async ({ cookies, url }) => { await requirePermission(await getSessionUser(cookies), 'data.manage'); const database = await getDatabase(); - const [brands, effects, stats] = await Promise.all([ - (await database.prepare('SELECT * FROM brand_sets ORDER BY active DESC, name COLLATE NOCASE')).all(), + const summary = await (await database.prepare(`SELECT COUNT(*) AS total, + SUM(CASE WHEN active=1 THEN 1 ELSE 0 END) AS active FROM brand_sets`)).get(); + const pagination = getPagination(url, Number(summary?.total ?? 0)); + const [brands, effects, stats, effectSummary] = await Promise.all([ + (await database.prepare('SELECT * FROM brand_sets ORDER BY active DESC, name COLLATE NOCASE LIMIT ? OFFSET ?')).all(pagination.perPage, pagination.offset), (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 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() ]); return { brands: brands.map((brand) => ({ ...brand, effects: effects.filter((effect) => effect.brand_set_id === brand.id) })), - stats + stats, + summary: { ...summary, effects: Number(effectSummary?.total ?? 0) }, + pagination }; }; diff --git a/src/routes/admin/brands/+page.svelte b/src/routes/admin/brands/+page.svelte index 49fab04..35ffb6a 100644 --- a/src/routes/admin/brands/+page.svelte +++ b/src/routes/admin/brands/+page.svelte @@ -1,5 +1,6 @@ Stats & attributes — SHD.build admin

ATTRIBUTE REGISTRY

STATS & ATTRIBUTES

Builder values, hidden calculation targets, source ranges, and caps.

-
TOTAL RECORDS{data.stats.length}All definitions
ACTIVE{data.stats.filter((item)=>Number(item.active)===1).length}Available to systems
OFFENSIVE{data.stats.filter((item)=>item.type==='Offensive').length}Damage-oriented
DEFENSIVE{data.stats.filter((item)=>item.type==='Defensive').length}Survival-oriented
+
TOTAL RECORDS{data.summary.total}All definitions
ACTIVE{data.summary.active}Available to systems
OFFENSIVE{data.summary.offensive}Damage-oriented
DEFENSIVE{data.summary.defensive}Survival-oriented
{#if editing}

{editing.id?'MODIFY RECORD':'NEW RECORD'}

{editing.id?editing.name:'ATTRIBUTE DEFINITION'}

UNIT
{#each [['','NONE'],['%','%'],['s','s'],['Shots','SHOTS'],['m','m']] as option}{/each}
{#each [['gear','GEAR'],['weapon','WEAPON'],['skill_chip','SKILL CHIP']] as source}
{source[1]} RANGE · OPTIONAL
{/each}
{#if form?.error}

{form.error}

{/if}
{/if} +
ATTRIBUTECLASSIFICATIONCATEGORYCALCULATIONSTATE
{#each data.stats as stat}
{stat.name}{stat.unit || 'NO UNIT'}
{stat.type}{stat.category}{stat.calculation_type}{Number(stat.active)===1?'ACTIVE':'ARCHIVED'}
{/each}
diff --git a/src/routes/admin/talents/+page.server.ts b/src/routes/admin/talents/+page.server.ts index f497879..d20c556 100644 --- a/src/routes/admin/talents/+page.server.ts +++ b/src/routes/admin/talents/+page.server.ts @@ -4,14 +4,17 @@ 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'; type SharedEffect = { statId: string; valueI: number; valueII: number }; -export const load: PageServerLoad = async ({ cookies }) => { +export const load: PageServerLoad = async ({ cookies, url }) => { await requirePermission(await getSessionUser(cookies), 'data.manage'); const db = await getDatabase(); + const totalRow = await (await db.prepare('SELECT COUNT(*) AS total FROM talents')).get(); + const pagination = getPagination(url, Number(totalRow?.total ?? 0)); const [talents, tiers, triggers, links, effects, stats, templates, weaponScopes, weaponSlots] = await Promise.all([ - (await db.prepare('SELECT * FROM talents ORDER BY active DESC,name COLLATE NOCASE')).all(), + (await db.prepare('SELECT * FROM talents ORDER BY active DESC,name COLLATE NOCASE LIMIT ? OFFSET ?')).all(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(), @@ -21,6 +24,17 @@ export const load: PageServerLoad = async ({ cookies }) => { (await db.prepare('SELECT talent_id,scope FROM talent_weapon_scopes')).all(), (await db.prepare('SELECT talent_id,slot FROM weapon_talent_slots')).all() ]); + const [triggerSummary, slotSummary, exoticSummary, gearSummary] = await Promise.all([ + (await db.prepare('SELECT COUNT(*) AS total FROM talent_triggers')).get(), + (await db.prepare(`SELECT + SUM(CASE WHEN slot=1 THEN 1 ELSE 0 END) AS slot1, + SUM(CASE WHEN slot=2 THEN 1 ELSE 0 END) AS slot2 FROM weapon_talent_slots`)).get(), + (await db.prepare("SELECT COUNT(*) AS total FROM talent_weapon_scopes WHERE scope='Exotic'")).get(), + (await db.prepare(`SELECT + SUM(CASE WHEN category='Backpack' THEN 1 ELSE 0 END) AS backpack, + SUM(CASE WHEN category IN ('Vest','Body Armor') THEN 1 ELSE 0 END) AS body_armor + FROM talents`)).get() + ]); return { talents: talents.map((talent) => ({ ...talent, @@ -43,7 +57,17 @@ export const load: PageServerLoad = async ({ cookies }) => { })) })), triggers, - stats + stats, + summary: { + total: Number(totalRow?.total ?? 0), + triggers: Number(triggerSummary?.total ?? 0), + slot1: Number(slotSummary?.slot1 ?? 0), + slot2: Number(slotSummary?.slot2 ?? 0), + exotic: Number(exoticSummary?.total ?? 0), + backpack: Number(gearSummary?.backpack ?? 0), + bodyArmor: Number(gearSummary?.body_armor ?? 0) + }, + pagination }; }; diff --git a/src/routes/admin/talents/+page.svelte b/src/routes/admin/talents/+page.svelte index fc22c58..0718cf3 100644 --- a/src/routes/admin/talents/+page.svelte +++ b/src/routes/admin/talents/+page.svelte @@ -1,5 +1,6 @@ User operations — ISAC

USER OPERATIONS

AGENT REGISTRY

Identity, access state, recent activity, and community contribution overview.

-
RESULTS{data.users.length}Current query
ACTIVE{data.users.filter((user) => user.accessState === 'ACTIVE' && !user.inactive).length}Active within 30 days
INACTIVE{data.users.filter((user) => user.inactive).length}No activity for 30 days
RESTRICTED{data.users.filter((user) => user.accessState !== 'ACTIVE').length}Blocked or banned
+
RESULTS{data.summary.results}Current query
ACTIVE{data.summary.active}Active within 30 days
INACTIVE{data.summary.inactive}No activity for 30 days
RESTRICTED{data.summary.restricted}Blocked or banned
+
USERACCESSLAST LOGINLAST ACTIVECONTENT
{#each data.users as user}
{user.display_name}LOGIN: {user.username} · {user.discord_id ? 'DISCORD LINKED' : 'LOCAL IDENTITY'}
{user.accessState}{user.inactive && user.accessState === 'ACTIVE' ? ' · INACTIVE' : ''}{date(user.last_login_at)}{date(user.last_active_at)}{user.build_count} BUILDS · {user.comment_count} COMMENTSVIEW
diff --git a/src/routes/admin/weapons/+page.server.ts b/src/routes/admin/weapons/+page.server.ts index a501699..f8e517d 100644 --- a/src/routes/admin/weapons/+page.server.ts +++ b/src/routes/admin/weapons/+page.server.ts @@ -4,16 +4,21 @@ 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'; const categories = ['Shotgun', 'Rifle', 'MMR', 'AR', 'LMG', 'SMG']; const damageTypes = ['Blast', 'Pierce', 'Shredding']; const rangeNames = ['damage', 'firepower', 'toughness', 'engineering'] as const; -export const load: PageServerLoad = async ({ cookies }) => { +export const load: PageServerLoad = async ({ cookies, url }) => { await requirePermission(await getSessionUser(cookies), 'data.manage'); const database = await getDatabase(); + const summary = await (await database.prepare(`SELECT COUNT(*) AS total, + SUM(CASE WHEN active=1 THEN 1 ELSE 0 END) AS active, + COUNT(DISTINCT category) AS categories FROM weapons`)).get(); + const pagination = getPagination(url, Number(summary?.total ?? 0)); const [weapons, tierRanges, talentOptions, augmentEffects, baseBonuses, talent1Options, talent2Options, exoticTalents, stats] = await Promise.all([ - (await database.prepare('SELECT * FROM weapons ORDER BY active DESC, category, name COLLATE NOCASE')).all(), + (await database.prepare('SELECT * FROM weapons ORDER BY active DESC, category, name COLLATE NOCASE LIMIT ? OFFSET ?')).all(pagination.perPage, pagination.offset), (await database.prepare('SELECT * FROM weapon_tier_ranges ORDER BY tier')).all(), (await database.prepare('SELECT weapon_id,talent_slot,talent_id FROM weapon_talent_options')).all(), (await database.prepare('SELECT weapon_id,stat_attribute_id,value,sort_order FROM weapon_augmentation_effects ORDER BY sort_order')).all(), @@ -46,7 +51,9 @@ export const load: PageServerLoad = async ({ cookies }) => { talent1Options, talent2Options, exoticTalents, - stats + stats, + summary, + pagination }; }; diff --git a/src/routes/admin/weapons/+page.svelte b/src/routes/admin/weapons/+page.svelte index d2a8cfd..44a27fb 100644 --- a/src/routes/admin/weapons/+page.svelte +++ b/src/routes/admin/weapons/+page.svelte @@ -1,6 +1,7 @@