Release 0.0.9

This commit is contained in:
2026-04-20 21:32:36 +00:00
parent c99788f3e2
commit 43d1a61535
5 changed files with 111 additions and 10 deletions

View File

@@ -2,7 +2,7 @@
"id": "kosmos-storage-audit", "id": "kosmos-storage-audit",
"title": "Kosmos Storage Audit", "title": "Kosmos Storage Audit",
"description": "Analyzes media references and risky storage locations across Foundry data and public roots.", "description": "Analyzes media references and risky storage locations across Foundry data and public roots.",
"version": "0.0.8", "version": "0.0.9",
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",
"verified": "13" "verified": "13"

View File

@@ -41,6 +41,7 @@ function worldCollectionEntries() {
sourceLabel: `${doc.documentName ?? "Document"} ${doc.id}`, sourceLabel: `${doc.documentName ?? "Document"} ${doc.id}`,
sourceName: doc.name ?? null, sourceName: doc.name ?? null,
sourceUuid: doc.uuid ?? null, sourceUuid: doc.uuid ?? null,
resolveSourceTrail: candidatePath => resolveSourceTrail(doc, candidatePath),
value: doc.toObject ? doc.toObject() : doc value: doc.toObject ? doc.toObject() : doc
}); });
} }
@@ -95,6 +96,7 @@ async function* packagePackEntries(onProgress = null) {
sourceLabel: `${pack.collection} ${document.id}`, sourceLabel: `${pack.collection} ${document.id}`,
sourceName: document.name ?? null, sourceName: document.name ?? null,
sourceUuid: document.uuid ?? null, sourceUuid: document.uuid ?? null,
resolveSourceTrail: candidatePath => resolveSourceTrail(document, candidatePath),
value: document.toObject ? document.toObject() : document value: document.toObject ? document.toObject() : document
}; };
} }
@@ -119,3 +121,73 @@ export async function runRuntimeAnalysis({ onProgress }={}) {
function format(key, data) { function format(key, data) {
return game.i18n?.format(key, data) ?? key; return game.i18n?.format(key, data) ?? key;
} }
function resolveSourceTrail(document, candidatePath = []) {
const trail = [createTrailNode(document.uuid, document.name ?? `${document.documentName ?? "Document"} ${document.id}`)];
const path = Array.isArray(candidatePath) ? candidatePath : [];
if (!path.length) return trail;
if (document.documentName === "Actor") {
const itemNode = resolveActorItemTrail(document, path);
if (itemNode) trail.push(itemNode);
return trail;
}
if (document.documentName === "Scene") {
const sceneTrail = resolveSceneTrail(document, path);
if (sceneTrail.length) trail.push(...sceneTrail);
}
return trail;
}
function resolveActorItemTrail(actor, path) {
if ((path[0] !== "items") || !Number.isInteger(path[1])) return null;
const itemData = actor.items?.contents?.[path[1]] ?? actor.toObject?.().items?.[path[1]] ?? null;
const itemId = itemData?.id ?? itemData?._id ?? null;
if (!itemId) return null;
const itemName = itemData?.name ?? `Item ${itemId}`;
return createTrailNode(`${actor.uuid}.Item.${itemId}`, itemName);
}
function resolveSceneTrail(scene, path) {
if ((path[0] !== "tokens") || !Number.isInteger(path[1])) return [];
const token = scene.tokens?.contents?.[path[1]] ?? scene.toObject?.().tokens?.[path[1]] ?? null;
const actorId = token?.actorId ?? token?.actor?.id ?? null;
const actor = actorId ? game.actors?.get(actorId) ?? null : null;
if (!actor) return [];
const trail = [createTrailNode(actor.uuid, actor.name ?? `Actor ${actor.id}`)];
const itemPath = extractTokenItemPath(path);
if (!itemPath) return trail;
const tokenData = scene.toObject?.().tokens?.[path[1]] ?? null;
const itemData = resolveTokenItemData(tokenData, itemPath.itemIndex);
const itemId = itemData?._id ?? itemData?.id ?? null;
if (!itemId) return trail;
trail.push(createTrailNode(`${actor.uuid}.Item.${itemId}`, itemData?.name ?? `Item ${itemId}`));
return trail;
}
function extractTokenItemPath(path) {
if ((path[2] === "actorData") && (path[3] === "items") && Number.isInteger(path[4])) {
return { itemIndex: path[4] };
}
if ((path[2] === "delta") && (path[3] === "items") && Number.isInteger(path[4])) {
return { itemIndex: path[4] };
}
return null;
}
function resolveTokenItemData(tokenData, itemIndex) {
if (!tokenData || !Number.isInteger(itemIndex)) return null;
if (Array.isArray(tokenData.actorData?.items)) return tokenData.actorData.items[itemIndex] ?? null;
if (Array.isArray(tokenData.delta?.items)) return tokenData.delta.items[itemIndex] ?? null;
return null;
}
function createTrailNode(uuid, label) {
return { uuid, label };
}

View File

@@ -78,7 +78,7 @@ export class StorageAuditReportApp extends foundry.applications.api.ApplicationV
</div> </div>
</section> </section>
${renderProgress(context.progress, context.loading)} ${renderProgress(context.progress, context.loading)}
${renderSummary(context.summary)} ${renderSummary(context.summary, context.loading)}
${renderGroupedFindingList(context.groupedFindings, context.hasAnalysis, context.loading, context.showAll)} ${renderGroupedFindingList(context.groupedFindings, context.hasAnalysis, context.loading, context.showAll)}
${renderFindingList(context.findings, context.hasAnalysis, context.loading, context.showAll, context.showRaw)} ${renderFindingList(context.findings, context.hasAnalysis, context.loading, context.showAll, context.showRaw)}
`; `;
@@ -228,7 +228,8 @@ function renderProgress(progress, loading) {
`; `;
} }
function renderSummary(summary) { function renderSummary(summary, loading) {
if (loading) return "";
if (!summary) { if (!summary) {
return ` return `
<section class="storage-audit__summary"> <section class="storage-audit__summary">
@@ -447,7 +448,8 @@ function groupByTarget(findings) {
current.sources.set(key, { current.sources.set(key, {
sourceLabel: finding.source.sourceLabel, sourceLabel: finding.source.sourceLabel,
sourceName: finding.source.sourceName ?? null, sourceName: finding.source.sourceName ?? null,
sourceUuid: finding.source.sourceUuid ?? null sourceUuid: finding.source.sourceUuid ?? null,
sourceTrail: finding.source.sourceTrail ?? null
}); });
} }
grouped.set(target, current); grouped.set(target, current);
@@ -507,6 +509,7 @@ function serializeFinding(finding) {
sourceLabel: finding.source.sourceLabel, sourceLabel: finding.source.sourceLabel,
sourceName: finding.source.sourceName, sourceName: finding.source.sourceName,
sourceUuid: finding.source.sourceUuid, sourceUuid: finding.source.sourceUuid,
sourceTrail: finding.source.sourceTrail,
sourceScope: finding.source.sourceScope, sourceScope: finding.source.sourceScope,
rawValue: finding.source.rawValue, rawValue: finding.source.rawValue,
normalized: finding.source.normalized normalized: finding.source.normalized
@@ -550,9 +553,14 @@ function renderLocalizedCodeText(key, data, codeValues) {
} }
function renderSourceLink(source) { function renderSourceLink(source) {
const trail = Array.isArray(source.sourceTrail) && source.sourceTrail.length ? source.sourceTrail : null;
if (trail) {
return trail.map(renderTrailNode).join(' <span class="storage-audit__trail-separator">-&gt;</span> ');
}
const label = source.sourceName ? `${source.sourceLabel} (${source.sourceName})` : source.sourceLabel; const label = source.sourceName ? `${source.sourceLabel} (${source.sourceName})` : source.sourceLabel;
if (!source.sourceUuid) return escapeHtml(label); if (!source.sourceUuid) return escapeHtml(label);
return `<a class="content-link" draggable="true" data-link data-uuid="${escapeHtml(source.sourceUuid)}" data-tooltip="${escapeHtml(label)}"><i class="fa-solid fa-file-lines"></i><code>${escapeHtml(source.sourceUuid)}</code></a>${label ? ` <span>${escapeHtml(label)}</span>` : ""}`; return renderUuidLink(source.sourceUuid, label);
} }
async function openSourceUuid(uuid) { async function openSourceUuid(uuid) {
@@ -568,3 +576,12 @@ async function openSourceUuid(uuid) {
} }
ui.notifications.warn(format("KSA.Notify.OpenSourceFailed", { uuid })); ui.notifications.warn(format("KSA.Notify.OpenSourceFailed", { uuid }));
} }
function renderTrailNode(node) {
if (!node?.uuid) return escapeHtml(node?.label ?? "");
return renderUuidLink(node.uuid, node.label ?? node.uuid);
}
function renderUuidLink(uuid, label) {
return `<a class="content-link" draggable="true" data-link data-uuid="${escapeHtml(uuid)}" data-tooltip="${escapeHtml(label)}"><i class="fa-solid fa-file-lines"></i><span>${escapeHtml(label)}</span></a>`;
}

View File

@@ -5,23 +5,23 @@ const ATTRIBUTE_PATTERNS = [
/url\(\s*["']?([^"')]+)["']?\s*\)/gi /url\(\s*["']?([^"')]+)["']?\s*\)/gi
]; ];
export function collectStringCandidates(value, visit) { export function collectStringCandidates(value, visit, path = []) {
if (typeof value === "string") { if (typeof value === "string") {
visit(value); visit(value, path);
return; return;
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
for (const entry of value) collectStringCandidates(entry, visit); for (const [index, entry] of value.entries()) collectStringCandidates(entry, visit, [...path, index]);
return; return;
} }
if ((value !== null) && (typeof value === "object")) { if ((value !== null) && (typeof value === "object")) {
for (const entry of Object.values(value)) collectStringCandidates(entry, visit); for (const [key, entry] of Object.entries(value)) collectStringCandidates(entry, visit, [...path, key]);
} }
} }
export function extractReferencesFromValue(value, source) { export function extractReferencesFromValue(value, source) {
const references = []; const references = [];
collectStringCandidates(value, candidate => { collectStringCandidates(value, (candidate, candidatePath) => {
const direct = resolveReference(candidate, source); const direct = resolveReference(candidate, source);
if (direct && isMediaPath(direct.path)) { if (direct && isMediaPath(direct.path)) {
references.push({ references.push({
@@ -30,6 +30,7 @@ export function extractReferencesFromValue(value, source) {
sourceLabel: source.sourceLabel, sourceLabel: source.sourceLabel,
sourceName: source.sourceName, sourceName: source.sourceName,
sourceUuid: source.sourceUuid, sourceUuid: source.sourceUuid,
sourceTrail: source.resolveSourceTrail?.(candidatePath) ?? null,
rawValue: candidate, rawValue: candidate,
normalized: { normalized: {
...direct, ...direct,
@@ -50,6 +51,7 @@ export function extractReferencesFromValue(value, source) {
sourceLabel: source.sourceLabel, sourceLabel: source.sourceLabel,
sourceName: source.sourceName, sourceName: source.sourceName,
sourceUuid: source.sourceUuid, sourceUuid: source.sourceUuid,
sourceTrail: source.resolveSourceTrail?.(candidatePath) ?? null,
rawValue: match[1], rawValue: match[1],
normalized: { normalized: {
...nested, ...nested,

View File

@@ -270,6 +270,16 @@
padding-left: 1.25rem; padding-left: 1.25rem;
} }
.storage-audit__samples li,
.storage-audit__finding dd {
overflow-wrap: anywhere;
}
.storage-audit__trail-separator {
opacity: 0.65;
margin: 0 0.25rem;
}
@media (max-width: 900px) { @media (max-width: 900px) {
.storage-audit__hero { .storage-audit__hero {
flex-direction: column; flex-direction: column;