diff --git a/scripts/api.js b/scripts/api.js index 5a10fce..a4bd19e 100644 --- a/scripts/api.js +++ b/scripts/api.js @@ -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. diff --git a/scripts/macros.js b/scripts/macros.js new file mode 100644 index 0000000..5c70c68 --- /dev/null +++ b/scripts/macros.js @@ -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: ` +
End ${escapeHtml(s.session_title)}?
+This will mark it Completed, schedule the next session, and push snapshots for all PCs.
+ `, + yes: () => true, + no: () => false, + defaultYes: true, + }); + return confirmed ? s.session_id : null; + } + + const options = sessions + .map( + (s) => + ``, + ) + .join(""); + + return new Promise((resolve) => { + new Dialog({ + title: "End Session", + content: ` +Multiple scheduled sessions match this Foundry world. Pick one to end:
+ + `, + 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 = [ + `${escapeHtml(completion.completed_session)} marked Completed.
`, + ]; + if (completion.next_session) { + parts.push( + `Next session scheduled: ${escapeHtml(completion.next_session)} (#${escapeHtml(completion.next_session_number)}).
`, + ); + } + parts.push( + `Snapshots pushed: ${snapshotResult.successful} of ${snapshotResult.total}.
`, + ); + if (snapshotResult.failed.length) { + const items = snapshotResult.failed + .map((f) => `Failed pushes: