(feat) Discord build share

This commit is contained in:
jb
2026-08-02 12:00:30 -04:00
parent d09b6220ee
commit d9924a95b9
19 changed files with 1193 additions and 96 deletions
+2
View File
@@ -4,6 +4,7 @@ import { getSessionUser } from '$lib/server/auth/session';
import { fail } from '@sveltejs/kit';
import { randomUUID } from 'node:crypto';
import { parseLoadoutDraft } from '$lib/builder/loadout';
import { publicOrigin } from '$lib/server/public-origin';
import type { Actions, PageServerLoad } from './$types';
import { attachWeaponCategoryBonuses } from '$lib/weapons/category-bonuses';
@@ -284,6 +285,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
}));
return {
viewer: viewer ? { id: viewer.id, displayName: viewer.displayName } : null,
publicOrigin: publicOrigin(url.origin),
initialBuild,
attributes,
coreRanges,
+11 -4
View File
@@ -8,6 +8,7 @@
import SegmentedChoice from '$lib/components/SegmentedChoice.svelte';
import PendingForm from '$lib/components/PendingForm.svelte';
import ItemIcon from '$lib/components/ItemIcon.svelte';
import BuildShareControl from '$lib/components/BuildShareControl.svelte';
import LoadoutMatrix, { type MatrixCell } from '$lib/components/builder/LoadoutMatrix.svelte';
import LoadoutStatsSummary from '$lib/components/builder/LoadoutStatsSummary.svelte';
import PlannedCombatModel from '$lib/components/builder/PlannedCombatModel.svelte';
@@ -25,7 +26,10 @@
let message = $state('');
let persistedBuildId = $state(untrack(() => data.initialBuild?.id ?? ''));
let persistedSlug = $state(untrack(() => data.initialBuild?.slug ?? ''));
let persistedVisibility = $state(untrack(() => data.initialBuild?.visibility ?? 'private'));
type BuildVisibility = 'private' | 'unlisted' | 'public';
let persistedVisibility = $state<BuildVisibility>(untrack(() => (data.initialBuild?.visibility ?? 'private') as BuildVisibility));
let savedBuildVisibility = $state<BuildVisibility>(untrack(() => (data.initialBuild?.visibility ?? 'private') as BuildVisibility));
let savedBuildTitle = $state(untrack(() => draft.buildName));
let handledBuildToken = '';
const weaponRangeKeys = ['damage','firepower','toughness','engineering'] as const;
const coreKeys = ['firepower','toughness','engineering'] as const;
@@ -378,7 +382,9 @@
handledBuildToken = form.buildToken;
persistedBuildId = form.savedBuild.id;
persistedSlug = form.savedBuild.slug;
persistedVisibility = form.savedBuild.visibility;
persistedVisibility = form.savedBuild.visibility as BuildVisibility;
savedBuildVisibility = form.savedBuild.visibility as BuildVisibility;
savedBuildTitle = form.savedBuild.title;
message = form.savedBuild.visibility === 'private'
? 'Private build saved to your account.'
: form.savedBuild.visibility === 'unlisted'
@@ -419,15 +425,16 @@
{ value:'private', label:'PRIVATE' },
{ value:'unlisted', label:'UNLISTED' },
{ value:'public', label:'PUBLIC' }
]} value={persistedVisibility} onchange={(values) => persistedVisibility = values[0]} required/>
]} value={persistedVisibility} onchange={(values) => persistedVisibility = values[0] as BuildVisibility} required/>
<p>{persistedVisibility === 'private' ? 'Only you can open this build.' : persistedVisibility === 'unlisted' ? 'Anyone with its direct link can open this build.' : 'Listed publicly and accessible by direct link.'}</p>
{#if persistedSlug && persistedVisibility !== 'private'}<a class="builder-share-link" href={`/builds/${persistedSlug}`}>VIEW SHARE PAGE →</a>{/if}
{#if persistedSlug && savedBuildVisibility !== 'private'}<a class="builder-share-link" href={`/builds/${persistedSlug}`}>VIEW SHARE PAGE →</a>{/if}
</div>
{/if}
<div class="builder-status"><span><i></i> KNOWN STATS ONLINE</span><span>ADVANCED MODEL · COMING SOON</span><span>12 LOADOUT CELLS + 9 CHIPS</span><span>{data.gear.length + data.weapons.length + data.chipSets.length + data.protocols.length} CATALOG ITEMS</span></div>
</div>
{#if message}<p class="success" role="status">{message}</p>{/if}
{#if form?.buildError}<p class="error" role="alert">{form.buildError}</p>{/if}
{#if data.viewer && persistedSlug}<BuildShareControl title={savedBuildTitle} slugOrUrl={new URL(`/builds/${encodeURIComponent(persistedSlug)}`, data.publicOrigin).toString()} visibility={savedBuildVisibility}/>{/if}
{#if !data.gear.length && !data.weapons.length && !data.chipSets.length && !data.protocols.length}<div class="builder-empty-data"><strong>ISAC DATA LINK EMPTY</strong><span>Add active equipment definitions in ISAC to populate this workbench.</span></div>{/if}
<div class="loadout-workspace">
<LoadoutMatrix
+9 -85
View File
@@ -1,91 +1,15 @@
import { error } from '@sveltejs/kit';
import { getDatabase } from '$lib/server/database';
import { getSessionUser } from '$lib/server/auth/session';
import { parseLoadoutDraft, type LoadoutDraft } from '$lib/builder/loadout';
import { summarizeLoadout } from '$lib/builder/loadout-summary';
import { loadSharedBuildSummaryCatalog } from '$lib/server/shared-build-summary';
import { avatarUrl } from '$lib/avatar';
import { publicOrigin } from '$lib/server/public-origin';
import { loadSharedBuildProjection } from '$lib/server/shared-build';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, cookies }) => {
const database = await getDatabase();
export const load: PageServerLoad = async ({ params, cookies, url }) => {
const viewer = await getSessionUser(cookies);
const build = await (
await database.prepare(`
SELECT builds.*,COALESCE(user_profiles.display_name,users.username,'Deleted Agent') AS author,
user_profiles.public_slug,
COALESCE(user_profiles.is_public,1) AS profile_is_public,deleted_accounts.user_id AS deleted_user_id,
user_avatars.source AS avatar_source,user_avatars.value AS avatar_value,
user_avatars.updated_at AS avatar_updated_at
FROM builds
JOIN users ON users.id=builds.owner_id
LEFT JOIN user_profiles ON user_profiles.user_id=users.id
LEFT JOIN deleted_accounts ON deleted_accounts.user_id=users.id
LEFT JOIN user_avatars ON user_avatars.user_id=users.id
WHERE builds.slug=?
`)
).get(params.slug);
if (
!build ||
(build.visibility === 'private' && build.owner_id !== viewer?.id)
)
error(404, 'Build not found');
let loadout: LoadoutDraft;
try {
const parsed = parseLoadoutDraft(JSON.parse(String(build.loadout_json)));
if (!parsed) error(404, 'Build data is unavailable');
loadout = parsed;
} catch {
error(404, 'Build data is unavailable');
}
const [gear, weapons, chips, protocols, brands, summaryCatalog] =
await Promise.all([
(
await database.prepare(
'SELECT id,name,slot,tier,base_armor_min FROM gear',
)
).all(),
(
await database.prepare(
'SELECT id,name,category,is_exotic,rpm,magazine,reload_speed,damage_type FROM weapons',
)
).all(),
(await database.prepare('SELECT id,name FROM skill_chip_sets')).all(),
(
await database.prepare('SELECT id,name,quality FROM os_protocols')
).all(),
(await database.prepare('SELECT id,name FROM brand_sets')).all(),
loadSharedBuildSummaryCatalog(database),
]);
const summary = summarizeLoadout(loadout, summaryCatalog);
return {
build: {
id: String(build.id),
title: String(build.title),
slug: String(build.slug),
visibility: String(build.visibility),
author: String(build.author),
ownerId: String(build.owner_id),
authorPublicSlug: build.public_slug ? String(build.public_slug) : null,
profileAvailable:
Boolean(build.profile_is_public) && !build.deleted_user_id,
authorAvatarUrl: avatarUrl(
build.avatar_source,
build.avatar_value,
String(build.owner_id),
build.avatar_updated_at,
),
owner: build.owner_id === viewer?.id,
updatedAt: String(build.updated_at),
},
loadout,
gear,
weapons,
chips,
protocols,
brands,
summary,
attributes: summaryCatalog.attributes,
talents: summaryCatalog.talents,
};
const projection = await loadSharedBuildProjection(params.slug, {
kind: 'viewer-page',
viewerId: viewer?.id ?? null,
});
if (!projection) error(404, 'Build not found');
return { ...projection, publicOrigin: publicOrigin(url.origin) };
};
+25 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { gearSlots } from '$lib/builder/loadout';
import AgentIdentityLink from '$lib/components/AgentIdentityLink.svelte';
import BuildShareControl from '$lib/components/BuildShareControl.svelte';
import LoadoutStatsSummary from '$lib/components/builder/LoadoutStatsSummary.svelte';
import PlannedCombatModel from '$lib/components/builder/PlannedCombatModel.svelte';
let { data } = $props();
@@ -17,14 +18,37 @@
.join(' · ');
const coreLine = (selection: { firepower: number; toughness: number; engineering: number }) =>
`FP ${number.format(selection.firepower)} · TGH ${number.format(selection.toughness)} · ENG ${number.format(selection.engineering)}`;
const description = $derived(`${data.build.title}, a Division Resurgence Build by ${data.build.author}. View its equipped weapons, gear, Skill Chips, and known Agent stats on SHD.build.`);
const canonicalUrl = $derived(new URL(`/builds/${encodeURIComponent(data.build.slug)}`, data.publicOrigin).toString());
const previewUrl = $derived(new URL(`${canonicalUrl}/preview.png`).toString());
</script>
<svelte:head><title>{data.build.title} — SHD.build</title></svelte:head>
<svelte:head>
<title>{data.build.title} — SHD.build</title>
<meta name="description" content={description}/>
<link rel="canonical" href={canonicalUrl}/>
<meta property="og:type" content="website"/>
<meta property="og:site_name" content="SHD.build"/>
<meta property="og:title" content={`${data.build.title} — SHD.build`}/>
<meta property="og:description" content={description}/>
<meta property="og:url" content={canonicalUrl}/>
{#if data.build.visibility !== 'private'}
<meta property="og:image" content={previewUrl}/>
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/>
<meta property="og:image:type" content="image/png"/>
<meta property="og:image:alt" content={`Build preview for ${data.build.title}`}/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:image" content={previewUrl}/>
{/if}
{#if data.build.visibility !== 'public'}<meta name="robots" content="noindex,nofollow"/>{/if}
</svelte:head>
<main class="app-page shared-build-page">
<header class="shared-build-command">
<div><p class="eyebrow"><span></span> SHD LOADOUT ARCHIVE / {data.build.visibility.toUpperCase()}</p><h1>{data.build.title}</h1><AgentIdentityLink size="compact" linked={data.build.profileAvailable} agent={{ id: data.build.ownerId, displayName: data.build.author, publicSlug: data.build.authorPublicSlug, avatarUrl: data.build.authorAvatarUrl }} meta={`UPDATED ${new Date(data.build.updatedAt).toLocaleDateString()}`}/></div>
<div>{#if data.build.owner}<a class="secondary" href={`/guides/new?build=${encodeURIComponent(data.build.id)}`}>CREATE GUIDE FOR BUILD</a><a class="secondary" href={`/builder?build=${data.build.slug}`}>EDIT LOADOUT</a>{/if}<a class="primary" href="/builder">NEW BUILD +</a></div>
</header>
<BuildShareControl title={data.build.title} slugOrUrl={canonicalUrl} visibility={data.build.visibility}/>
<div class="shared-build-intelligence">
<LoadoutStatsSummary summary={data.summary} attributes={data.attributes} talents={data.talents} headingId="shared-known-stats-heading" layout="page" />
<PlannedCombatModel headingId="shared-planned-model-heading" headingLevel="h2" variant="compact" />
@@ -0,0 +1,180 @@
import { createHash } from 'node:crypto';
import { error } from '@sveltejs/kit';
import {
renderBuildSocialCardPng,
type BuildSocialCardFact,
type BuildSocialCardInput,
} from '$lib/server/build-social-card';
import { publicOrigin } from '$lib/server/public-origin';
import { loadSharedBuildProjection } from '$lib/server/shared-build';
import type { RequestHandler } from './$types';
const number = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });
const MAX_CACHE_BYTES = 16 * 1024 * 1024;
const MAX_ACTIVE_REQUESTS = 4;
const MAX_QUEUED_REQUESTS = 32;
type CachedImage = { body: Buffer; etag: string };
const imageCache = new Map<string, CachedImage>();
const pendingImages = new Map<string, Promise<CachedImage>>();
const requestQueue: Array<(release: () => void) => void> = [];
let cacheBytes = 0;
let activeRequests = 0;
function unitValue(value: number, unit: string | null | undefined) {
const rendered = number.format(value);
return unit === 'Shots' ? `${rendered} ${unit}` : `${rendered}${unit ?? ''}`;
}
export function _socialCardInput(
projection: NonNullable<
Awaited<ReturnType<typeof loadSharedBuildProjection>>
>,
directUrl: string,
): BuildSocialCardInput {
const attribute = (id: string) =>
projection.attributes.find((item) => String(item.id) === id);
const statByName = (name: string) =>
projection.summary.stats.find(
(stat) => String(attribute(stat.id)?.name) === name,
);
const statValue = (name: string) => {
const stat = statByName(name);
return stat ? unitValue(Number(stat.value), attribute(stat.id)?.unit) : '0';
};
const weaponName = (index: number) => {
const selected = projection.loadout.weapons[index];
return (
projection.weapons.find((item) => String(item.id) === selected?.itemId)
?.name ?? 'Unassigned'
);
};
const coreNames = new Set(['Firepower', 'Toughness', 'Engineering']);
const supportingAttributes: BuildSocialCardFact[] = projection.summary.stats
.filter((stat) => !coreNames.has(String(attribute(stat.id)?.name)))
.slice(0, 3)
.map((stat) => ({
label: String(attribute(stat.id)?.name ?? 'Attribute'),
value: unitValue(Number(stat.value), attribute(stat.id)?.unit),
}));
return {
title: projection.build.title,
author: projection.build.author,
primaryWeapon: String(weaponName(0)),
secondaryWeapon: String(weaponName(1)),
firepower: statValue('Firepower'),
toughness: statValue('Toughness'),
engineering: statValue('Engineering'),
supportingAttributes,
visibility: projection.build.visibility as 'public' | 'unlisted',
directUrl,
};
}
function matchesEtag(request: Request, etag: string) {
return request.headers
.get('if-none-match')
?.split(',')
.some((value) => value.trim() === etag);
}
function releaseRequest() {
const next = requestQueue.shift();
if (next) {
next(releaseRequest);
return;
}
activeRequests -= 1;
}
function acquireRequest(): Promise<(() => void) | null> {
if (activeRequests < MAX_ACTIVE_REQUESTS) {
activeRequests += 1;
return Promise.resolve(releaseRequest);
}
if (requestQueue.length >= MAX_QUEUED_REQUESTS) return Promise.resolve(null);
return new Promise((resolve) => requestQueue.push(resolve));
}
function storeImage(key: string, image: CachedImage) {
imageCache.set(key, image);
cacheBytes += image.body.byteLength;
while (cacheBytes > MAX_CACHE_BYTES && imageCache.size > 1) {
const oldestKey = imageCache.keys().next().value;
if (!oldestKey) break;
const oldest = imageCache.get(oldestKey);
imageCache.delete(oldestKey);
cacheBytes -= oldest?.body.byteLength ?? 0;
}
}
async function loadImage(input: BuildSocialCardInput, inputKey: string) {
const cached = imageCache.get(inputKey);
if (cached) {
imageCache.delete(inputKey);
imageCache.set(inputKey, cached);
return cached;
}
const pending = pendingImages.get(inputKey);
if (pending) return pending;
const request = renderBuildSocialCardPng(input).then((body) => {
const image = {
body,
etag: `"${createHash('sha256').update(body).digest('base64url')}"`,
};
storeImage(inputKey, image);
return image;
});
pendingImages.set(inputKey, request);
try {
return await request;
} finally {
pendingImages.delete(inputKey);
}
}
export const GET: RequestHandler = async ({ params, request, url }) => {
const release = await acquireRequest();
if (!release) error(503, 'Build preview capacity exceeded');
try {
const projection = await loadSharedBuildProjection(params.slug, {
kind: 'public-preview',
});
if (!projection) error(404, 'Build preview not found');
const origin = publicOrigin(url.origin);
const directUrl = new URL(
`/builds/${encodeURIComponent(projection.build.slug)}`,
origin,
).toString();
const input = _socialCardInput(projection, directUrl);
const inputKey = createHash('sha256')
.update(JSON.stringify({ version: 1, input }))
.digest('base64url');
const image = await loadImage(input, inputKey);
const cacheControl = 'public, max-age=0, must-revalidate';
if (matchesEtag(request, image.etag)) {
return new Response(null, {
status: 304,
headers: { 'cache-control': cacheControl, etag: image.etag },
});
}
return new Response(new Uint8Array(image.body), {
headers: {
'cache-control': cacheControl,
'content-length': String(image.body.byteLength),
'content-type': 'image/png',
'cross-origin-resource-policy': 'same-origin',
etag: image.etag,
'x-content-type-options': 'nosniff',
...(projection.build.visibility === 'unlisted'
? { 'x-robots-tag': 'noindex' }
: {}),
},
});
} finally {
release();
}
};
@@ -0,0 +1,119 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
loadProjection: vi.fn(),
render: vi.fn(),
}));
vi.mock('$lib/server/shared-build', () => ({
loadSharedBuildProjection: mocks.loadProjection,
}));
vi.mock('$lib/server/build-social-card', () => ({
renderBuildSocialCardPng: mocks.render,
}));
import { GET, _socialCardInput } from './+server';
const projection = {
build: {
title: 'Field Medic',
slug: 'field-medic-12345678',
author: 'Aewyn',
visibility: 'unlisted',
},
loadout: {
weapons: [{ itemId: 'weapon-1' }, { itemId: 'weapon-2' }],
},
weapons: [
{ id: 'weapon-1', name: 'ACR' },
{ id: 'weapon-2', name: 'Super 90' },
],
attributes: [
{ id: 'fp', name: 'Firepower', unit: '' },
{ id: 'tgh', name: 'Toughness', unit: '' },
{ id: 'eng', name: 'Engineering', unit: '' },
{ id: 'crit', name: 'Critical Hit Chance', unit: '%' },
],
summary: {
stats: [
{ id: 'fp', value: 1200 },
{ id: 'tgh', value: 950 },
{ id: 'eng', value: 610 },
{ id: 'crit', value: 18.5 },
],
},
};
describe('Build social preview endpoint', () => {
beforeEach(() => {
mocks.loadProjection.mockReset().mockResolvedValue(projection);
mocks.render.mockReset().mockResolvedValue(Buffer.from([1, 2, 3]));
});
it('projects canonical Build facts into the card input', () => {
expect(
_socialCardInput(
projection as never,
'https://shd.build/builds/field-medic-12345678',
),
).toEqual({
title: 'Field Medic',
author: 'Aewyn',
primaryWeapon: 'ACR',
secondaryWeapon: 'Super 90',
firepower: '1,200',
toughness: '950',
engineering: '610',
supportingAttributes: [{ label: 'Critical Hit Chance', value: '18.5%' }],
visibility: 'unlisted',
directUrl: 'https://shd.build/builds/field-medic-12345678',
});
});
it('serves a cacheable PNG and honors its ETag', async () => {
const first = await GET({
params: { slug: 'field-medic-12345678' },
request: new Request(
'https://shd.build/builds/field-medic-12345678/preview.png',
),
url: new URL('https://shd.build/builds/field-medic-12345678/preview.png'),
} as never);
expect(first.status).toBe(200);
expect(first.headers.get('content-type')).toBe('image/png');
expect(first.headers.get('x-content-type-options')).toBe('nosniff');
expect(first.headers.get('cache-control')).toBe(
'public, max-age=0, must-revalidate',
);
expect(first.headers.get('x-robots-tag')).toBe('noindex');
expect(mocks.loadProjection).toHaveBeenCalledWith('field-medic-12345678', {
kind: 'public-preview',
});
const etag = first.headers.get('etag');
expect(etag).toBeTruthy();
const second = await GET({
params: { slug: 'field-medic-12345678' },
request: new Request(
'https://shd.build/builds/field-medic-12345678/preview.png',
{ headers: { 'if-none-match': String(etag) } },
),
url: new URL('https://shd.build/builds/field-medic-12345678/preview.png'),
} as never);
expect(second.status).toBe(304);
expect(mocks.render).toHaveBeenCalledTimes(1);
});
it('returns a generic 404 when the public projection is unavailable', async () => {
mocks.loadProjection.mockResolvedValue(null);
await expect(
GET({
params: { slug: 'private-build' },
request: new Request(
'https://shd.build/builds/private-build/preview.png',
),
url: new URL('https://shd.build/builds/private-build/preview.png'),
} as never),
).rejects.toMatchObject({ status: 404 });
expect(mocks.render).not.toHaveBeenCalled();
});
});