[Backlog] In-world calendar for timeline events #126

Closed
opened 2026-07-14 19:57:10 +00:00 by claude-bot · 6 comments
Collaborator

Context / Motivation

Research/product brief — explicitly gated on real user pull; do not build speculatively. Wiki entries already carry structured timeline items: LoreEntry.timeline_events is a JSONB array (webapp/backend/app/models/lore_entry.py:225, mirrored in lore_entry_version.py:39), with input shape LoreTimelineEventInput (routers/campaigns.py:205) validated by _normalise_timeline_events (campaigns.py:560) — each event can link to a lore entry, session, or milestone in the campaign. What they lack is when in the world something happened (Harptos "15 Mirtul, 1492 DR" style). Prior art: World Anvil calendars, Fantasy-Calendar.com.

Spec (starting sketch)

Calendar definition — per campaign, one optional WorldCalendar: campaign_id, name (e.g. "Calendar of Harptos"), months (JSONB list of {name, days}), weekday_names (JSONB list), year_epoch_label (e.g. "DR"), current_world_date (optional "today" marker). No leap rules initially — state this limitation in the settings UI.

In-world dates on timeline events — extend the timeline_events item shape (JSONB, so no migration for the items themselves) with an optional world_date object {year, month_index, day} validated against the campaign's calendar in _normalise_timeline_events. Rendered per the calendar definition ("15 Mirtul, 1492 DR").

Campaign timeline view — a campaign page collecting all timeline events (across lore entries + milestones) that have world_date, sorted by in-world date (year → month index → day); events without world dates fall back to the existing ordering.

Out of scope

  • Leap years/intercalary days, moons, seasons, astronomical events.
  • Real-time↔world-time conversion or session-date mapping.
  • Importing Fantasy-Calendar definitions (revisit if pulled).

Open questions

  • Is there real demand? Gate: wait for explicit user requests before pulling this out of backlog.
  • Should sessions themselves (not just wiki timeline items) get an optional in-world date ("this session covered 3-5 Mirtul")?
  • Sort stability for partial dates (year-only events)?

Acceptance criteria (for when this is pulled)

  • Calendar definition CRUD (GM); month/weekday rendering matches definition.
  • Timeline events accept/validate optional world dates; invalid month/day rejected against the definition.
  • Campaign timeline view sorts by in-world date with documented partial-date behavior.

References

  • webapp/backend/app/models/lore_entry.py:225 (timeline_events JSONB), models/lore_entry_version.py:39
  • webapp/backend/app/routers/campaigns.py:205 (LoreTimelineEventInput), :560 (_normalise_timeline_events, cross-linking rules at :597-629)

Filed from the July 2026 full-project review.

## Context / Motivation Research/product brief — **explicitly gated on real user pull**; do not build speculatively. Wiki entries already carry structured timeline items: `LoreEntry.timeline_events` is a JSONB array (`webapp/backend/app/models/lore_entry.py:225`, mirrored in `lore_entry_version.py:39`), with input shape `LoreTimelineEventInput` (`routers/campaigns.py:205`) validated by `_normalise_timeline_events` (`campaigns.py:560`) — each event can link to a lore entry, session, or milestone in the campaign. What they lack is *when in the world* something happened (Harptos "15 Mirtul, 1492 DR" style). Prior art: World Anvil calendars, Fantasy-Calendar.com. ## Spec (starting sketch) **Calendar definition** — per campaign, one optional `WorldCalendar`: `campaign_id`, `name` (e.g. "Calendar of Harptos"), `months` (JSONB list of `{name, days}`), `weekday_names` (JSONB list), `year_epoch_label` (e.g. "DR"), `current_world_date` (optional "today" marker). **No leap rules initially** — state this limitation in the settings UI. **In-world dates on timeline events** — extend the `timeline_events` item shape (JSONB, so no migration for the items themselves) with an optional `world_date` object `{year, month_index, day}` validated against the campaign's calendar in `_normalise_timeline_events`. Rendered per the calendar definition ("15 Mirtul, 1492 DR"). **Campaign timeline view** — a campaign page collecting all timeline events (across lore entries + milestones) that have `world_date`, sorted by in-world date (year → month index → day); events without world dates fall back to the existing ordering. ## Out of scope - Leap years/intercalary days, moons, seasons, astronomical events. - Real-time↔world-time conversion or session-date mapping. - Importing Fantasy-Calendar definitions (revisit if pulled). ## Open questions - Is there real demand? Gate: wait for explicit user requests before pulling this out of backlog. - Should sessions themselves (not just wiki timeline items) get an optional in-world date ("this session covered 3-5 Mirtul")? - Sort stability for partial dates (year-only events)? ## Acceptance criteria (for when this is pulled) - Calendar definition CRUD (GM); month/weekday rendering matches definition. - Timeline events accept/validate optional world dates; invalid month/day rejected against the definition. - Campaign timeline view sorts by in-world date with documented partial-date behavior. ## References - `webapp/backend/app/models/lore_entry.py:225` (`timeline_events` JSONB), `models/lore_entry_version.py:39` - `webapp/backend/app/routers/campaigns.py:205` (`LoreTimelineEventInput`), `:560` (`_normalise_timeline_events`, cross-linking rules at `:597-629`) _Filed from the July 2026 full-project review._
Author
Collaborator

The wiki design record #593 covers most of this issue (owner decision, 2026-09-10). The owner tracks in-world time "very loosely", so the agreed design has in-world dates without a calendar definition:

  • Timeline items get world_date {label, sort}: the date as the setting writes it ("3 Rainmonth, 812"), plus a hidden number that keeps the order correct. Year-only and approximate dates sort correctly.
  • Sessions record their in-world start and end dates, so time visibly passes, including on the map trail.
  • An event can carry both clocks: when it happened in the world, and the session in which the party learned of it.
  • A campaign timeline view merges every article's timeline items with session events.

What remains here is the optional full calendar: months, weekdays, and dates drawn from a calendar definition. The owner doesn't need it, but another group on a hosted instance might. Keep this issue for that, still waiting on real demand as it already says. If it's ever built, the label above can be generated from the calendar and sort computed from it, so nothing recorded earlier has to change.

**The wiki design record #593 covers most of this issue** (owner decision, 2026-09-10). The owner tracks in-world time "very loosely", so the agreed design has **in-world dates without a calendar definition**: - **Timeline items get `world_date {label, sort}`:** the date as the setting writes it ("3 Rainmonth, 812"), plus a hidden number that keeps the order correct. Year-only and approximate dates sort correctly. - **Sessions record their in-world start and end dates,** so time visibly passes, including on the map trail. - **An event can carry both clocks:** when it happened in the world, and the session in which the party learned of it. - **A campaign timeline view** merges every article's timeline items with session events. **What remains here is the optional full calendar**: months, weekdays, and dates drawn from a calendar definition. The owner doesn't need it, but another group on a hosted instance might. Keep this issue for that, still waiting on real demand as it already says. If it's ever built, the `label` above can be generated from the calendar and `sort` computed from it, so nothing recorded earlier has to change.
Author
Collaborator

The owner's requirements, 2026-09-24 (from lane 4's in-story dates, #730 L9)

This issue now has a user: sessions need in-world dates, and lane 4 (#730) has deferred its in-story dates here rather than ship a free-text field. That answers the open question above ("should sessions themselves get an optional in-world date?"): yes, as follows.

Sessions get in-world dates, never as free text. The owner: "I will never, ever use a free-text field where I need to look up dates."

  • The GM picks the day the session started in the game's calendar. It defaults to the day the previous session ended, with the next day one click away.
  • The GM says how much in-game time the session covered, in hours or days.
  • Quest Board works out the range ("3–5 Mirtul, 1492 DR") and shows it on the session. The next session's default start follows from it.

The calendar must work across game systems, and be customisable:

  • Built-in calendars for the game systems Quest Board supports: for example, the Calendar of Harptos for Forgotten Realms D&D, and Golarion's calendar for Pathfinder. Check the system registry for which ones to ship.
  • Custom calendars:
    • different year lengths;
    • any number of months, with their own names and lengths;
    • weekday names;
    • an epoch or era label;
    • lunar cycles, including several moons, each with its own cycle length and phase offset.
  • That means the spec's "Out of scope: moons" and "no leap rules" no longer hold as written. Moons are in. Intercalary days, such as Harptos's festival days, are likely needed for the built-in calendars to be correct. Leap rules should at least be possible where a built-in calendar needs them.

Also carried from the lane 4 survey: the timeline view above and the session dates should read from the same calendar, so a session and a wiki timeline event on the same in-world day sort together.

This stays in v4.6.0 unless the owner pulls it forward.

🤖 Generated with Claude Code

## The owner's requirements, 2026-09-24 (from lane 4's in-story dates, #730 L9) This issue now has a user: sessions need in-world dates, and lane 4 (#730) has **deferred its in-story dates here** rather than ship a free-text field. That answers the open question above ("should sessions themselves get an optional in-world date?"): **yes**, as follows. **Sessions get in-world dates, never as free text.** The owner: "I will never, ever use a free-text field where I need to look up dates." - The GM picks the **day the session started** in the game's calendar. It defaults to the day the previous session ended, with the next day one click away. - The GM says how much in-game time the session **covered**, in **hours or days**. - **Quest Board works out the range** ("3–5 Mirtul, 1492 DR") and shows it on the session. The next session's default start follows from it. **The calendar must work across game systems, and be customisable:** - Built-in calendars for the game systems Quest Board supports: for example, the Calendar of Harptos for Forgotten Realms D&D, and Golarion's calendar for Pathfinder. Check the system registry for which ones to ship. - **Custom calendars:** - different year lengths; - any number of months, with their own names and lengths; - weekday names; - an epoch or era label; - **lunar cycles, including several moons**, each with its own cycle length and phase offset. - That means the spec's "Out of scope: moons" and "no leap rules" no longer hold as written. Moons are in. Intercalary days, such as Harptos's festival days, are likely needed for the built-in calendars to be correct. Leap rules should at least be possible where a built-in calendar needs them. **Also carried from the lane 4 survey:** the timeline view above and the session dates should read from the same calendar, so a session and a wiki timeline event on the same in-world day sort together. This stays in **v4.6.0** unless the owner pulls it forward. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Collaborator

Picked up for 4.6.0 (owner, 17 Oct 2026). The design is ready for the owner to read: docs/design/in-world-calendar.md on branch design/126-in-world-calendar (53458c36).

The main calls:

  • One calendar per campaign, copied from a preset so a preset never moves anyone's dates.
  • Generic presets only, pending Q183. Setting names (Harptos, the Absalom Reckoning) aren't open content. GMs type them in or load a calendar file.
  • A date is stored as what the GM picked (year, month, day, precision, "about"), with a derived sort number.
  • One set of arithmetic in Python and JS, held together by a shared file of test cases.
  • Session dates: a start day and a span; the range is worked out.
  • Timeline dates use the same picker, with day, month or year precision.
  • Storyline gets an "In the world" switch.
  • Calendar edits that would break a stored date are refused.
  • Export schema 21, and templates carry the calendar.

The owner's questions: Q183–Q187 (presets, hours, where the GM is asked, Discord, timeline precision). Build order: the engine and shared test cases first (none of the questions changes them), then a prototype page for the owner, then the surfaces.

**Picked up for 4.6.0 (owner, 17 Oct 2026).** The design is ready for the owner to read: `docs/design/in-world-calendar.md` on branch `design/126-in-world-calendar` (53458c36). The main calls: - **One calendar per campaign**, copied from a preset so a preset never moves anyone's dates. - **Generic presets only, pending Q183.** Setting names (Harptos, the Absalom Reckoning) aren't open content. GMs type them in or load a calendar file. - **A date is stored as what the GM picked** (year, month, day, precision, "about"), with a derived sort number. - **One set of arithmetic in Python and JS**, held together by a shared file of test cases. - **Session dates:** a start day and a span; the range is worked out. - **Timeline dates** use the same picker, with day, month or year precision. - **Storyline** gets an "In the world" switch. - **Calendar edits** that would break a stored date are refused. - **Export schema 21**, and templates carry the calendar. The owner's questions: **Q183–Q187** (presets, hours, where the GM is asked, Discord, timeline precision). Build order: the engine and shared test cases first (none of the questions changes them), then a prototype page for the owner, then the surfaces.
Author
Collaborator

Owner's answers, 17 Oct 2026 (Questions page):

  • Q183 (a): generic presets only. These are 12 × 30; 12 × 30 with five festival days and a leap day (Harptos's shape, neutral names); the real-world calendar; and blank. No setting names ship. GMs type the names or load a calendar file.
  • Q184 (a): a session of under 24 hours stays on its start day, and a "ran past midnight" tick moves the end to the next day. From 24 hours it counts on.
  • Q185 (a): the dates are on the session page at any time, plus one quiet, skippable row in the post-session review, prefilled with the day the last session ended.
  • Q186 (a): the approved summary's Discord post gains one line, "In the story: …". This is additive and needs no contract bump.
  • Q187 (timeline precision): not answered yet.

Recorded in the design doc's header.

**Owner's answers, 17 Oct 2026 (Questions page):** - **Q183 (a):** generic presets only. These are 12 × 30; 12 × 30 with five festival days and a leap day (Harptos's shape, neutral names); the real-world calendar; and blank. No setting names ship. GMs type the names or load a calendar file. - **Q184 (a):** a session of under 24 hours stays on its start day, and a "ran past midnight" tick moves the end to the next day. From 24 hours it counts on. - **Q185 (a):** the dates are on the session page at any time, plus one quiet, skippable row in the post-session review, prefilled with the day the last session ended. - **Q186 (a):** the approved summary's Discord post gains one line, "In the story: …". This is additive and needs no contract bump. - **Q187 (timeline precision):** not answered yet. Recorded in the design doc's header.
Author
Collaborator

Q187 (b) (owner, 17 Oct 2026): a wiki timeline date can be a day, month or year, each with "about". A timeline event can instead carry a named age with no year ("the Age of Ash"): a short plain-text label that the GM places in order by hand. It sorts at that point in the "In the world" view and never takes part in date arithmetic. The design doc's C10 now describes it. All five calendar questions are answered. Next: the engine and its shared test cases, then a prototype page of the picker and the settings editor for the owner.

**Q187 (b) (owner, 17 Oct 2026):** a wiki timeline date can be a day, month or year, each with "about". A timeline event can instead carry a **named age with no year** ("the Age of Ash"): a short plain-text label that the GM places in order by hand. It sorts at that point in the "In the world" view and never takes part in date arithmetic. The design doc's C10 now describes it. All five calendar questions are answered. Next: the engine and its shared test cases, then a prototype page of the picker and the settings editor for the owner.
Author
Collaborator

Build step 1 done: the calendar engine is on branch feat/126-calendar-engine (6aff85de), with no PR yet. It covers:

  • the definition schema (Pydantic);
  • the Python engine (app/services/world_calendar.py) and the JS engine (src/calendar/engine.js);
  • the four generic presets (Q183 a);
  • one shared case file, src/calendar/cases.json, with 1128 cases, read by both pytest and vitest;
  • the committed generator scripts/calendar_cases.py --check, whose expectations come from brute force, datetime and hand-written strings, never the engine.

Decisions made while building:

  • Every month and festival entry has a stable id. check_change and remap_dates work from the ids, so inserting or reordering entries keeps every stored date's meaning.
  • Festival and leap days can be outside_week, as Harptos's are.
  • There is no year 0: year -1 is "1 before".
  • week.offset is the weekday of the first in-week day at or after day zero.

Verified on docker-test from git archive of 6aff85de:

  • backend: 1176 passed;
  • frontend: 1129 passed, eslint 0 errors;
  • ruff format and check: clean;
  • changelog check: OK.

Next: a prototype page of the date picker, the session row and the settings editor, for the owner.

**Build step 1 done: the calendar engine** is on branch `feat/126-calendar-engine` (6aff85de), with no PR yet. It covers: - the definition schema (Pydantic); - the Python engine (`app/services/world_calendar.py`) and the JS engine (`src/calendar/engine.js`); - the four generic presets (Q183 a); - one shared case file, `src/calendar/cases.json`, with 1128 cases, read by both pytest and vitest; - the committed generator `scripts/calendar_cases.py --check`, whose expectations come from brute force, `datetime` and hand-written strings, never the engine. Decisions made while building: - Every month and festival entry has a stable `id`. `check_change` and `remap_dates` work from the ids, so inserting or reordering entries keeps every stored date's meaning. - Festival and leap days can be `outside_week`, as Harptos's are. - There is no year 0: year -1 is "1 before". - `week.offset` is the weekday of the first in-week day at or after day zero. Verified on docker-test from `git archive` of 6aff85de: - backend: 1176 passed; - frontend: 1129 passed, eslint 0 errors; - ruff format and check: clean; - changelog check: OK. Next: a prototype page of the date picker, the session row and the settings editor, for the owner.
rbrooks referenced this issue from a commit 2026-10-17 09:22:26 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#126
No description provided.