seitime-bridge/scripts/settings.js
Vassili Minaev 2b7fb97696 Snapshot push on actor and item edits
Foundry side of the per-PC snapshot flow: the primary GM pushes a debounced
snapshot of any character that gets edited, including embedded item changes
(updateItem/createItem/deleteItem don't trigger updateActor in v12).

Payload is actor.toObject() augmented with a denormalized _player_name
field — the Frappe extractor reads it from there since Foundry's ownership
map uses opaque user IDs.

Per-actor debouncers (default 5s, configurable) keep combat HP edits from
spamming the webhook. Toggle off via the snapshotAutoSync setting if you
want explicit-only pushes from a macro.

API exposed at globalThis.seitimeBridge.pushSnapshot(actor) for manual
pushes (single argument: a Foundry Actor document).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 02:43:09 -06:00

71 lines
2.4 KiB
JavaScript

/**
* Module settings registration.
*
* Note on `sharedSecret`: Foundry stores world-scope settings in a place
* readable by all connected clients, not just GMs. A player with browser
* console access can read the value. This is acceptable here because the
* secret only authorizes pushing dnd5e actor data — which players already
* have access to through normal play — and the Frappe side enforces all
* other authorization. Rotate the secret if you stop trusting a player.
*/
import { MODULE_ID } from "./constants.js";
export function registerSettings() {
game.settings.register(MODULE_ID, "frappeBaseUrl", {
name: "Frappe Base URL",
hint: "Base URL of your Seitime Frappe site, e.g. https://seitime.vassi.li (no trailing slash).",
scope: "world",
config: true,
type: String,
default: "",
});
game.settings.register(MODULE_ID, "sharedSecret", {
name: "Shared Secret",
hint: "Bridge secret — must match Foundry Settings → Shared Secret on the Frappe side. Readable by any connected client; rotate if compromised.",
scope: "world",
config: true,
type: String,
default: "",
secret: true,
});
game.settings.register(MODULE_ID, "worldId", {
name: "World ID",
hint: "Identifier sent in the X-Bridge-World header. If you've configured Allowed World IDs on the Frappe side, this must be one of them. Leave blank to use the current Foundry world's id.",
scope: "world",
config: true,
type: String,
default: "",
});
game.settings.register(MODULE_ID, "manifestSyncIntervalMinutes", {
name: "Manifest Sync Interval (minutes)",
hint: "How often to push the actor manifest while the world is open. 0 disables periodic sync; on/create/delete pushes still fire.",
scope: "world",
config: true,
type: Number,
default: 15,
range: { min: 0, max: 120, step: 1 },
});
game.settings.register(MODULE_ID, "snapshotDebounceMs", {
name: "Snapshot Debounce (ms)",
hint: "How long to wait after the last actor edit before pushing a snapshot. Combat causes frequent edits, so a few seconds is usually right.",
scope: "world",
config: true,
type: Number,
default: 5000,
range: { min: 500, max: 60000, step: 500 },
});
game.settings.register(MODULE_ID, "snapshotAutoSync", {
name: "Auto-Sync on Actor Edits",
hint: "When enabled, the primary GM pushes a snapshot every time a PC is edited (debounced). Disable to push only via macro/console.",
scope: "world",
config: true,
type: Boolean,
default: true,
});
}