Foundry: Calendaria import, follow wizard-made calendars, one-paste connect line - #954
Merged
Merged
Conversation
…retire old routes POST /calendar creates a campaign's first calendar from the Foundry module's Calendaria payload (Owner only, 409 when the campaign already has a calendar, created as the default so a racing second import is refused by the one-default index). The sync routes follow the default calendar or, when none is marked default, the first, since the V5 wizard never marks one. The 16 pre-V5 structure/advance/export/import routes the module never called answer 410 calendar_route_retired with what to use instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UexL55BkZcztyC1eezXKfn
Calendaria's live API counts months from 0, so the Foundry import uses that base instead of guessing it from the smallest season start. A time of day past a 24-hour day is clamped with a warning rather than refusing the import, since the payload carries no day length. The 201 body names the calendar "created" (the plugin-isolation guard reads a bare "calendar" literal outside its plugin as a cross-plugin reference). Moves a misplaced doc comment and drops a stale world-state route comment. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UexL55BkZcztyC1eezXKfn
The Foundry row on Apps & game system now shows a status dot (connected now / last seen / never), the module version, a prefix-only preview of the connect line, and a button that mints a new key and shows the full line once. - campaigns: FoundryConnector interface, ComputeFoundryStatus, BuildFoundryConnectLine (single definition of the wire format), owner-only POST /extensions/foundry/connect-line, response is Cache-Control: no-store - app: foundryConnectorAdapter over the sync key service, websocket hub and BaseURL - syncapi: api_keys.module_version (migration 008) stored from X-Chronicle-Module-Version when it matches the allowed pattern; absent or invalid leaves the stored value alone; the session door's synthetic key is never written - cors: allow X-Chronicle-Module-Version on preflight so a browser-hosted Foundry is not blocked by the new header Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UexL55BkZcztyC1eezXKfn
A key with the "custom" label belongs to a bot or script, so its last use and missing module version no longer show as Foundry's. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UexL55BkZcztyC1eezXKfn
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UexL55BkZcztyC1eezXKfn
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UexL55BkZcztyC1eezXKfn
Owner
Author
|
Generated by Claude Code |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UexL55BkZcztyC1eezXKfn
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Key Maker · project thread
Fixes #869
Fixes #893
Security implication: The import is Owner-only (a DM grant without membership doesn't count, same as the web wizard). It is scoped to the URL campaign, which
RequireCampaignMatchpins to the key's campaign; the payload carries no campaign id. The body is capped at 1 MiB. The primary-calendar fallback picks the same calendar for every viewer, then gates it, so a player never gets a different or hidden calendar.Consumer-verified: Payload shape from
Chronicle-Foundry-Module/scripts/sync-calendar-import-from-calendaria.mjs(transformCalendariaCalendar). The module's POST and its 404/405 handling are atscripts/sync-calendar.mjs:1214-1229. The module calls only/calendar,/calendar/date(/confirm),/calendar/events(/:id)and/calendar/weather(checked by greppingscripts/).Mockup: The calendar import is backend only. The Foundry row follows the approved Sign-offs card "Before and after: the Foundry row on Apps & game system" (from the signed #851 Manage mockup).
What this changes
Before: The Foundry module's "Import into Chronicle" button got a 404 ("Chronicle doesn't have the create endpoint yet"). A calendar made in the V5 wizard was never marked default, so Foundry couldn't find it at all: every sync read 404'd and the module offered to import. Sixteen other old calendar routes answered 503 "rebuilding".
After: The Import button creates the campaign's first calendar from the Calendaria calendar. That covers months, weekdays, seasons, moons, eras, today's date and time, and the description. The calendar is made the default, and dates and events then sync. Foundry follows the default calendar, or the first one when none is marked default, so wizard-made calendars sync too. The 16 old routes answer 410 with what to use instead.
Why
#869, under #778. The Calendaria import is the one remaining route the module calls. The other 16 are called by nothing, and V5 edits calendar structure only in Chronicle's calendar, so each is retired with a reason rather than ported:
GET /calendar/datecarries season, moons and weather;PUT /calendar/date;POST /calendarfor a first calendar.Part 2: the Foundry row on Apps & game system (#893)
Before: The Foundry row said "Connection keys and sync status" and linked to Settings › Integrations. There you copied a key and the address into Foundry separately.
After:
chronicle://line once with a Copy button. All keys links to the key list.Load-bearing lines:
campaigns/foundry_connect.go:BuildFoundryConnectLineis the wire contract with the module (One-paste connect line from Chronicle, and report the module version Chronicle-Foundry-Module#121):chronicle://for an httpsBASE_URL,chronicle+http://for http.ComputeFoundryStatusturns hub presence and key last-use into the dot and wording.campaigns/extensions_hub_handler.goNewFoundryConnectLine:POST /campaigns/:id/extensions/foundry/connect-line.Cache-Control: no-storeand the line is never logged. The base URL is checked before a key is minted.app/routes.gofoundryConnectorAdapter: reads the campaign's active keys and skips ones labelled "custom" (other tools). Hub presence comes from the websocket hub.syncapi/middleware.goandrepository.go:X-Chronicle-Module-Versionis stored on the key when it matches^[0-9A-Za-z.+\-]{1,32}$.COALESCEkeeps the stored value when the header is absent, and the session door's synthetic key is never written.syncapi/migrations/008_api_key_module_version:ADD COLUMN IF NOT EXISTS module_version VARCHAR(32). Additive and idempotent; the migration-safety check passed.middleware/cors.go: the header is added to the allow-list so a cross-origin Foundry's preflight passes. The module drops the header for older servers that don't allow it.Live check after deploy: #966. Before deploying, confirm
BASE_URLis the public https address. The connect line is built from it, and the defaulthttp://localhost:8080would give Foundry a line pointing at localhost.Load-bearing lines (calendar)
calendar/import_foundry.goParseFoundryImport:ImportResult, reusing the Calendaria file parser's season and moon arithmetic.leap_extra_daysis Calendaria's total leap-month length, read the same way as the file parser'sleapDays.calendar/service.goImportFoundryCalendar:MakeDefault, so theidx_one_default_per_campaignunique index refuses a racing second insert. ThatDuplicate entrybecomes 409. No duplicate copies.calendar/service.goGetPrimaryCalendarForViewer: the default, else the first by sort order. A hidden default gives 404 and does not fall back. The campaign export adapter already had the same fallback.syncapi/calendar_api_handler.goCreateCalendar: requireOwner, then the body cap, then 201{created, warnings}.syncapi/routes.go: the 16 routes now useretiredCalendarRoute(...).calendarRebuildingand its test are gone.CreateCalendarFromImportnow also carriesToday.Hour/Minute(range-checked) andSettings.Description. Only the Foundry parser sets them; every other import path sends 0 and nil, as before.Honest deviations and follow-ups
docs/api/openapi.yamlstill describes the calendar routes as 503; it was already stale on main. Issue to follow.Test plan
go build ./... && go vet ./internal/plugins/calendar/ ./internal/plugins/syncapi/: clean.go test -count=1 ./internal/plugins/calendar/ ./internal/plugins/syncapi/ ./internal/plugins/campaigns/ ./internal/app/ ./internal/middleware/ ./internal/wire/ ./internal/systems/: all ok. A review also ranmake verify:go test ./... -shortgreen andmake test-js291/291.recordKeyUsage, route tests through the real router (Owner 200; Scribe and Player 403 with no key minted; the POST shows the line once with no-store; the GET never contains a raw key), and adapter tests (inactive, expired and custom keys ignored). The calendar and syncapi packages have 1,361 test runs: 1,335 pass, and the rest are DB integration tests skipped locally.tools/check-*.shguard exits 0, including plugin-isolation and migration-immutability.TestParseFoundryImport_*: structure, seasons, moons, eras, names, warnings, time clamp, 0-based seasons, errors.TestImportFoundryCalendar: 409 with an existing or hidden calendar; duplicate-default race gives 409; 400 on bad payloads; a failed apply leaves no calendar.TestGetPrimaryCalendarForViewer.TestCalendarAPI_CreateCalendar: Owner 201; Scribe and Player 403; 409 passes through; body cap.TestRetiredCalendarRoute,TestCalendarRoutes_RetiredAreRegisteredRetired.Tenet self-check
CalendarService.syncapi/.ai.md,calendar/.ai.md,.ai/architecture.mdand.ai/data-model.mdnow describe the routes as they are.🤖 Generated with Claude Code
https://claude.ai/code/session_01UexL55BkZcztyC1eezXKfn
Generated by Claude Code
Generated by Claude Code