Push module credits at end-of-session

endSession() now pushes the world's active module list to Frappe after
the snapshot push. Each module is projected to {id, title, url,
authors[].name + url} — versions and compatibility info are intentionally
omitted so the eventual public credits page doesn't leak dependency
versions. The Frappe endpoint also strips these fields server-side.

Module inventory push is treated as non-critical: a failure logs a
warning and adds a line to the chat summary, but doesn't abort the
session-completion flow. The chat summary now reports the count of
modules tracked.

Public API gains seitimeBridge.pushModuleInventory() for manual pushes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Vassili Minaev 2026-05-10 03:17:30 -06:00
parent c7159ee84b
commit ed853de6a2
3 changed files with 57 additions and 4 deletions

View file

@ -104,6 +104,38 @@ export async function pushSnapshot(actor) {
return result;
}
/**
* Project each active Foundry module to a credit-only summary. Versions
* are deliberately omitted: this list is intended for a public credits
* page on the Frappe side and we don't want to leak dependency versions
* if the site is ever indexed. The Frappe endpoint also strips any
* version/compatibility fields server-side as defense-in-depth.
*/
function buildModuleInventory() {
const out = [];
for (const mod of game.modules.values()) {
if (!mod.active) continue;
const authors = (mod.authors || [])
.map((a) => ({ name: a?.name, url: a?.url || null }))
.filter((a) => a.name);
out.push({
id: mod.id,
title: mod.title || mod.id,
url: mod.url || null,
authors,
});
}
return out;
}
export async function pushModuleInventory() {
const modules = buildModuleInventory();
console.log(`[${MODULE_ID}] pushing module inventory (${modules.length} active)`);
const result = await bridgeFetch("receive_module_inventory", { modules });
console.log(`[${MODULE_ID}] module inventory result:`, result);
return result;
}
export async function listScheduledSessions() {
const result = await bridgeFetch("list_scheduled_sessions", {});
return result?.message ?? [];

View file

@ -11,6 +11,7 @@ import { MODULE_ID } from "./constants.js";
import {
completeSession,
listScheduledSessions,
pushModuleInventory,
pushSnapshot,
} from "./api.js";
@ -91,7 +92,7 @@ async function pickSession(sessions) {
});
}
function buildSummaryHtml(completion, snapshotResult) {
function buildSummaryHtml(completion, snapshotResult, moduleResult) {
const parts = [
`<h3>Session Complete</h3>`,
`<p><b>${escapeHtml(completion.completed_session)}</b> marked Completed.</p>`,
@ -110,6 +111,15 @@ function buildSummaryHtml(completion, snapshotResult) {
.join("");
parts.push(`<p style="color:#a00"><b>Failed pushes:</b></p><ul>${items}</ul>`);
}
if (moduleResult?.upserted) {
parts.push(
`<p>Tracked ${moduleResult.upserted} active module(s) for credits.</p>`,
);
} else if (moduleResult?.error) {
parts.push(
`<p style="color:#a00">Module inventory push failed: ${escapeHtml(moduleResult.error)}</p>`,
);
}
return parts.join("");
}
@ -152,8 +162,19 @@ export async function endSession() {
const snapshotResult = await pushAllSnapshots();
// Module inventory is non-critical — log and continue if the push fails
// so the session still gets marked Completed with a clean summary.
let moduleResult = null;
try {
const raw = await pushModuleInventory();
moduleResult = raw?.message ?? null;
} catch (err) {
console.warn(`[${MODULE_ID}] module inventory push failed:`, err);
moduleResult = { error: err.message };
}
await ChatMessage.create({
content: buildSummaryHtml(completion, snapshotResult),
content: buildSummaryHtml(completion, snapshotResult, moduleResult),
speaker: { alias: "Seitime Bridge" },
whisper: ChatMessage.getWhisperRecipients("GM").map((u) => u.id),
});

View file

@ -10,7 +10,7 @@
import { MODULE_ID } from "./constants.js";
import { registerSettings } from "./settings.js";
import { pushManifest, pushSnapshot, testConnection } from "./api.js";
import { pushManifest, pushModuleInventory, pushSnapshot, testConnection } from "./api.js";
import { endSession, pushAllSnapshots } from "./macros.js";
let manifestTimerId = null;
@ -62,7 +62,7 @@ Hooks.once("init", () => {
Hooks.once("ready", () => {
const mod = game.modules.get(MODULE_ID);
const api = { pushManifest, pushSnapshot, testConnection, endSession, pushAllSnapshots };
const api = { pushManifest, pushSnapshot, pushModuleInventory, testConnection, endSession, pushAllSnapshots };
mod.api = api;
globalThis.seitimeBridge = api;