End Session macro flow
Adds the GM-facing end-of-session orchestration. The expected use is a Foundry macro of type Script with body: seitimeBridge.endSession(); Flow: 1. Fetch scheduled sessions for this world from Frappe. 2. If 0 → notify and abort. If 1 → confirm dialog. If 2+ → picker dialog. 3. POST complete_session to mark the chosen session Completed and schedule the next one (Frappe-side schedule_next_session). 4. Push snapshots for every PC in parallel (Promise.allSettled — partial failures are surfaced, not fatal). 5. Whisper a summary chat message to the GM (counts and any failed actor names with errors). scripts/macros.js holds the orchestration; scripts/api.js gains thin fetch wrappers for list_scheduled_sessions and complete_session. HTML in dialogs and chat is escaped on output since session titles ultimately come from user input. Public API now: testConnection, pushManifest, pushSnapshot, pushAllSnapshots, endSession. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2b7fb97696
commit
c7159ee84b
3 changed files with 177 additions and 2 deletions
|
|
@ -104,6 +104,17 @@ export async function pushSnapshot(actor) {
|
|||
return result;
|
||||
}
|
||||
|
||||
export async function listScheduledSessions() {
|
||||
const result = await bridgeFetch("list_scheduled_sessions", {});
|
||||
return result?.message ?? [];
|
||||
}
|
||||
|
||||
export async function completeSession(sessionId) {
|
||||
if (!sessionId) throw new Error("completeSession requires a session_id.");
|
||||
const result = await bridgeFetch("complete_session", { session_id: sessionId });
|
||||
return result?.message ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Round-trip an empty manifest push to verify URL, secret, world ID, and
|
||||
* Frappe-side enablement. Surfaces the result in the Foundry UI.
|
||||
|
|
|
|||
163
scripts/macros.js
Normal file
163
scripts/macros.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* GM-facing orchestration: the End Session flow and the bulk
|
||||
* snapshot push it depends on. Imported by main.js and exposed on
|
||||
* `globalThis.seitimeBridge` for use from Foundry macros.
|
||||
*
|
||||
* Recommended macro body (Script type, no arguments):
|
||||
* seitimeBridge.endSession();
|
||||
*/
|
||||
|
||||
import { MODULE_ID } from "./constants.js";
|
||||
import {
|
||||
completeSession,
|
||||
listScheduledSessions,
|
||||
pushSnapshot,
|
||||
} from "./api.js";
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s ?? "").replace(
|
||||
/[&<>"']/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push snapshots for every PC in the world, in parallel. Returns a summary
|
||||
* with successful/failed counts and the names of any failures so the caller
|
||||
* can surface them.
|
||||
*/
|
||||
export async function pushAllSnapshots() {
|
||||
const pcs = game.actors.filter((a) => a.type === "character");
|
||||
const results = await Promise.allSettled(pcs.map((a) => pushSnapshot(a)));
|
||||
const failed = results
|
||||
.map((r, i) => (r.status === "rejected" ? { name: pcs[i].name, error: r.reason?.message ?? String(r.reason) } : null))
|
||||
.filter(Boolean);
|
||||
return {
|
||||
total: pcs.length,
|
||||
successful: pcs.length - failed.length,
|
||||
failed,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the appropriate dialog for the number of candidates and resolve to
|
||||
* the chosen session_id, or null if the user cancels.
|
||||
*/
|
||||
async function pickSession(sessions) {
|
||||
if (sessions.length === 1) {
|
||||
const s = sessions[0];
|
||||
const confirmed = await Dialog.confirm({
|
||||
title: "End Session",
|
||||
content: `
|
||||
<p>End <b>${escapeHtml(s.session_title)}</b>?</p>
|
||||
<p>This will mark it Completed, schedule the next session, and push snapshots for all PCs.</p>
|
||||
`,
|
||||
yes: () => true,
|
||||
no: () => false,
|
||||
defaultYes: true,
|
||||
});
|
||||
return confirmed ? s.session_id : null;
|
||||
}
|
||||
|
||||
const options = sessions
|
||||
.map(
|
||||
(s) =>
|
||||
`<option value="${escapeHtml(s.session_id)}">${escapeHtml(s.session_title)}${s.session_start ? ` — ${escapeHtml(s.session_start)}` : ""}</option>`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
return new Promise((resolve) => {
|
||||
new Dialog({
|
||||
title: "End Session",
|
||||
content: `
|
||||
<p>Multiple scheduled sessions match this Foundry world. Pick one to end:</p>
|
||||
<select id="seitime-session-pick" style="width:100%; margin-bottom:0.5em;">
|
||||
${options}
|
||||
</select>
|
||||
`,
|
||||
buttons: {
|
||||
cancel: { label: "Cancel", callback: () => resolve(null) },
|
||||
end: {
|
||||
label: "End Session",
|
||||
callback: (html) => {
|
||||
const value = html.find("#seitime-session-pick").val();
|
||||
resolve(value || null);
|
||||
},
|
||||
},
|
||||
},
|
||||
default: "end",
|
||||
close: () => resolve(null),
|
||||
}).render(true);
|
||||
});
|
||||
}
|
||||
|
||||
function buildSummaryHtml(completion, snapshotResult) {
|
||||
const parts = [
|
||||
`<h3>Session Complete</h3>`,
|
||||
`<p><b>${escapeHtml(completion.completed_session)}</b> marked Completed.</p>`,
|
||||
];
|
||||
if (completion.next_session) {
|
||||
parts.push(
|
||||
`<p>Next session scheduled: <b>${escapeHtml(completion.next_session)}</b> (#${escapeHtml(completion.next_session_number)}).</p>`,
|
||||
);
|
||||
}
|
||||
parts.push(
|
||||
`<p>Snapshots pushed: ${snapshotResult.successful} of ${snapshotResult.total}.</p>`,
|
||||
);
|
||||
if (snapshotResult.failed.length) {
|
||||
const items = snapshotResult.failed
|
||||
.map((f) => `<li>${escapeHtml(f.name)}: ${escapeHtml(f.error)}</li>`)
|
||||
.join("");
|
||||
parts.push(`<p style="color:#a00"><b>Failed pushes:</b></p><ul>${items}</ul>`);
|
||||
}
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Full end-of-session flow. Invoked from a Foundry macro by the GM.
|
||||
*/
|
||||
export async function endSession() {
|
||||
if (!game.user.isGM) {
|
||||
ui.notifications.warn("End Session is GM-only.");
|
||||
return;
|
||||
}
|
||||
|
||||
let sessions;
|
||||
try {
|
||||
sessions = await listScheduledSessions();
|
||||
} catch (err) {
|
||||
console.error(`[${MODULE_ID}]`, err);
|
||||
ui.notifications.error(`Could not fetch scheduled sessions: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessions.length) {
|
||||
ui.notifications.warn(
|
||||
"No scheduled sessions found for this Foundry world. Check that the world ID matches a Game in Frappe.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = await pickSession(sessions);
|
||||
if (!sessionId) return;
|
||||
|
||||
let completion;
|
||||
try {
|
||||
completion = await completeSession(sessionId);
|
||||
} catch (err) {
|
||||
console.error(`[${MODULE_ID}]`, err);
|
||||
ui.notifications.error(`Could not complete session: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshotResult = await pushAllSnapshots();
|
||||
|
||||
await ChatMessage.create({
|
||||
content: buildSummaryHtml(completion, snapshotResult),
|
||||
speaker: { alias: "Seitime Bridge" },
|
||||
whisper: ChatMessage.getWhisperRecipients("GM").map((u) => u.id),
|
||||
});
|
||||
|
||||
const failureNote = snapshotResult.failed.length ? ` (${snapshotResult.failed.length} snapshot push(es) failed — see chat)` : "";
|
||||
ui.notifications.info(`Session ended. ${snapshotResult.successful}/${snapshotResult.total} snapshots pushed${failureNote}.`);
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
import { MODULE_ID } from "./constants.js";
|
||||
import { registerSettings } from "./settings.js";
|
||||
import { pushManifest, pushSnapshot, testConnection } from "./api.js";
|
||||
import { endSession, pushAllSnapshots } from "./macros.js";
|
||||
|
||||
let manifestTimerId = null;
|
||||
const snapshotDebouncers = new Map();
|
||||
|
|
@ -61,11 +62,11 @@ Hooks.once("init", () => {
|
|||
|
||||
Hooks.once("ready", () => {
|
||||
const mod = game.modules.get(MODULE_ID);
|
||||
const api = { pushManifest, pushSnapshot, testConnection };
|
||||
const api = { pushManifest, pushSnapshot, testConnection, endSession, pushAllSnapshots };
|
||||
mod.api = api;
|
||||
globalThis.seitimeBridge = api;
|
||||
|
||||
console.log(`[${MODULE_ID}] ready. Call seitimeBridge.testConnection() to verify config.`);
|
||||
console.log(`[${MODULE_ID}] ready. Call seitimeBridge.testConnection() to verify config, or seitimeBridge.endSession() from a macro.`);
|
||||
|
||||
safePushManifest("ready");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue