Skip to content

Finish Quests feature, add image upload + link PATCH, fold in review fixes from #46 - #47

Merged
camrun91 merged 13 commits into
stagingfrom
feat/api-augments-v14
Aug 3, 2026
Merged

Finish Quests feature, add image upload + link PATCH, fold in review fixes from #46#47
camrun91 merged 13 commits into
stagingfrom
feat/api-augments-v14

Conversation

@gharezlak

@gharezlak gharezlak commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Context

This branch was built independently, in parallel with #46, both starting from the same point in history and both finishing the same "half-built Quests feature" work — before I saw #46 existed. Once I found it, I diffed the two implementations file-by-file and folded in every genuine bug fix from #46 (with credit — several commits below are co-authored to reflect that). This PR is meant to supersede #46 rather than sit alongside it; happy to talk through anything below that looks wrong.

What's here

Quests, finished properly (previously only the title round-tripped to the API):

  • Structured edit form for questGiver/category/status/success-failure-conditions/next-action/resolution — _commitEdits now sends the full updateQuest payload instead of just the name.
  • Objectives (add/remove/edit text+status) now sync to the API instead of living only in local Foundry flags.
  • Quest relationships persist via relatedEntityRefs on the quest itself (the API has no ENTITY_CONFIG entry for Quest in the generic /links system in either direction, so it can't use the same link path as every other entity type). Links tab renders real clickable/unlinkable cards backed by entity IDs, not static name chips.
  • Character/Item/Location/Faction sheets get a read-only "Quests" tab (client-side filtered from listQuests(), since there's no reverse-lookup endpoint) so the relationship is visible from both sides.
  • Quests/Journals folded into the ongoing reconcile pass (reconcile-service.js), not just first import.
  • Quests now show up as a line item in the setup wizard's pre-sync summary table — they were already being auto-imported exactly like Factions/Recaps, just never surfaced there.

New API surface used:

  • Local image upload (Character/Item/Faction/Location, GM-only header button) — reads the linked Actor/Item/Scene's local image and pushes it to Archivist via the real 3-step presigned-URL flow (init → direct PUT to R2 → complete). Not available for Quest — the API has no image support for that type.
  • Link PATCH (updateLink) alongside the existing create/delete, for re-pointing a link's alias in place.

Folded in from #46 (verified independently, not just taken on faith):

  • Journal content was being dropped entirely on import/sync — sync-dialog.js read row.description/row.summary but never row.content, which is where Journal's actual content lives. The most serious bug in either branch.
  • _normalizeQuestPayload rewritten from a blocklist to a strict whitelist, matching QuestCreate/QuestUpdate's extra="forbid" schema. No call site in my branch was actually vulnerable to the 422 this describes, but the whitelist is a strictly safer contract for any future caller — verified functionally (read-only fields dropped, empty-text objectives dropped, relatedEntityRefs' nested keys correctly stay camelCase).
  • keepalive on writes is now conditional on body size — Chromium rejects keepalive fetches over ~64KB.
  • Shift+click range-select in the sync dialog was getting flipped back by the row-level click handler (missing stopPropagation).
  • Journal names were interpolated unescaped into the delete-confirmation dialog's HTML.
  • No folder fallback for journals/quests on worlds set up before this feature existed — they'd land in the world root.
  • Direct DOM progress updates in the setup wizard's import loops instead of a full re-render per entity.
  • The quest content-edit realtime hook was PATCHing the unchanged quest name back to the API on every keystroke commit — dead weight, renames are already handled by a separate title-sync hook.
  • stripHtml no longer calls foundry.utils.TextEditor.stripHTML, which doesn't exist in either v13 or v14 core (verified against the actual installed app) — it always fell through to the manual DOM strip anyway, just via a dead branch first.
  • Quest status/category labels localized via game.i18n with English fallback.

Deliberately NOT folded in from #46

module.json there sets compatibility.minimum: "13.351" while the code has already dropped ask-chat-sidebar-tab.js/the Sidebar.TABS registration (v13's actual mechanism for the chat sidebar tab — v14 can't use it the same way). That combination would let a v13 client install a build that's missing the chat tab. Given the whole reason this got picked back up was v14 code breaking for v13 users once before, I didn't want to reintroduce that shape of bug even scoped to the beta channel. This branch keeps compatibility.minimum: "14.359" (v14-only) and handles v13 separately — see the parity PR against main.

Also not touched: the wizard cleanup in world-setup-dialog.js/.hbs (removing the old "Step 4 mapping" UI) that's bundled into #46 — that's real, unrelated dead-code cleanup that predates both of our branches; worth doing but as its own PR so it doesn't get lost in this diff.

Heads up, unrelated to this PR

Noticed #45 (stagingmain, draft) is still open. Merging that as-is would collapse v13 support into a single v14-only main — probably worth a conversation before it goes anywhere, independent of this PR.

Testing

npm run lint: 0 errors. All touched files pass node --check. No test suite exists in this repo. I ran functional checks in isolation for the payload whitelist (confirmed read-only fields get dropped, empty objectives get dropped) and the plain-text description merge fix, but this hasn't been exercised in a live Foundry world yet — worth a manual pass on quest editing/linking and the image upload flow before merging.

🤖 Generated with Claude Code

Greptile Summary

This PR finishes the Quests feature (structured edit form, objectives and relatedEntityRefs synced to the API, read-only Quests tab on entity sheets), adds local image upload via the presigned R2 flow, exposes link PATCH (updateLink), and folds in a set of verified bug fixes from the parallel #46 branch (journal content field, keepalive size guard, delete-confirmation XSS, folder fallback for journals/quests, per-entity DOM progress updates, and dead-code removal).

  • Quests feature: _commitEdits now sends the full structured payload; objectives and relatedEntityRefs sync immediately on add/remove/edit; the sync dialog's diff path is quest-aware; reconcile fetches full quest detail to persist relatedEntityRefs flags accurately.
  • Image upload: three-step initImageUpload \u2192 uploadImageBytes \u2192 completeImageUpload flow wired to a GM-only header button; handles Character/Item/Faction/Location (Quest excluded per API limitation).
  • Bug fixes from Quest/journal sync fixes, v14 compat, and review cleanup #46: Journal content field was never read (silent data loss); _normalizeQuestPayload rewritten as a strict whitelist matching extra=\"forbid\"; range-select stopPropagation; escapeHTML on delete-confirmation names; folder fallback for worlds pre-dating the Journal/Quest destinations.

Confidence Score: 4/5

Safe to merge with one functional gap: the new Quests tab on entity sheets will silently show None for all entities until the _loadQuestsCached data source is fixed.

The core quest edit form, API sync, reconcile pass, image upload, and all #46 bug fixes look correct. The single issue is _loadQuestsCached sourcing quest data from the list endpoint, which the reconcile service itself explicitly notes omits relatedEntityRefs. The Quests tab introduced in this PR therefore can never surface a match, making the entire feature invisible to users until this is addressed.

Files Needing Attention: scripts/modules/sheets/page-sheet-v2.js - _loadQuestsCached and _renderRelatedQuests

Important Files Changed

Filename Overview
scripts/modules/sheets/page-sheet-v2.js Large quest feature implementation: structured edit form, objectives API sync, relatedEntityRefs-based link management, image upload button, Quests tab on entity sheets. Has one P1: _loadQuestsCached sources quest data from the list endpoint which omits relatedEntityRefs, causing the Quests tab to always show None.
scripts/services/archivist-api.js Adds updateLink (PATCH alias), three-step presigned image upload flow, conditional keepalive body-size guard, and rewrites _normalizeQuestPayload as a strict whitelist. All changes look correct.
scripts/dialogs/sync-dialog.js Adds quest diff helpers, quest-aware diff branch, folder fallback for journals/quests, relatedEntityRefs in import, _updateProgressUI for direct DOM progress updates, stopPropagation for shift-click range-select, and escapeHTML for deletion confirmation names. All fixes verified.
scripts/modules/reconcile/reconcile-service.js Adds Journals and Quests to the parallel reconcile pass; quests get full detail fetches to populate relatedEntityRefs flags correctly.
scripts/dialogs/world-setup-dialog.js Adds Quests row to pre-sync summary table, _updateSyncStatusUI for in-place DOM progress, relatedEntityRefs stored on quest import, and removes forced plain-text conversion for quest content.
scripts/modules/projection/merge.js Adds toArchivistPlainBlock / mergeArchivistPlainSection for non-HTML fields; removes the dead foundry.utils.TextEditor.stripHTML branch. Clean.
scripts/modules/projection/slot-resolver.js Updates non-HTML slot projection to use mergeArchivistPlainSection (non-destructive merge) instead of the previous destructive stripHtml overwrite.
scripts/modules/journal-manager.js Adds quest and journal to the folderNames map so reconcile can look them up.
scripts/archivist-sync.js Removes the redundant quest-name PATCH from the page-content realtime hook.
.cursor/rules/40-archivist-api.mdc Documents the new Quest, Journal, and Links-PATCH API surface. Description of _normalizeQuestPayload casing noted in a previous review thread.
scripts/modules/utils.js.bak Deleted backup file - correct housekeeping.

Reviews (5): Last reviewed commit: "fix(quest): include objectives in save-t..." | Re-trigger Greptile

gharezlak and others added 9 commits July 12, 2026 16:15
compatibility.verified was "13.351" while minimum was "14.359" -
verified must never be lower than minimum. Looks like a v13/v14 field
cross-contamination similar to the incident that caused the original
v14 rollback. Reset verified to match minimum until tested against a
newer 14.x build.
Previously only the quest title round-tripped to the API; every other
field (giver, category, status, success/failure/next-action/resolution)
was display-only after first import, objectives were local-only, and
Quests sat outside the shared linking system entirely.

- Info tab is now a structured edit form instead of a stubbed ProseMirror
  body (Quest has no generic `description` field in the API); drop the
  hack that stuffed successDefinition/nextAction into page content on
  import.
- _commitEdits sends the full updateQuest payload, not just questName.
- Objectives (add/remove/edit text+status) sync to the API instead of
  being local-flag-only.
- Quest relationships persist via relatedEntityRefs on the quest itself
  (updateQuest), since the API's link system has no ENTITY_CONFIG entry
  for Quest in either direction. Links tab now renders real, clickable,
  unlinkable cards backed by entity IDs instead of static name chips.
- Character/Item/Location/Faction sheets get a read-only "Quests" tab
  (client-side filtered from listQuests(), since there's no reverse
  lookup endpoint) so relationships are visible from both sides.
- reconcile-service now includes quests/journals in the ongoing sync
  pass, not just first import.
- Documented Quest/Journal/Links shapes in the Cursor API rules doc,
  which previously covered every other entity but not these.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Historically users couldn't import existing Foundry Actor/Item images
because they're stored locally and the module had no way to send image
bytes to the API. Confirmed the API's actual upload shape: a 3-step
presigned-URL flow (init -> PUT bytes directly to R2 -> complete), not
a raw multipart POST. 4.5MB cap and content-sniffing are server-enforced.

- archivist-api.js: initImageUpload/uploadImageBytes/completeImageUpload
  plus a uploadEntityImage convenience wrapper running all three steps.
- Character/Item/Location/Faction sheets get an "Upload local image"
  header button (GM-only) that reads the linked Actor/Item/Scene's (or
  the journal's own) local img via fetch() and pushes it to Archivist.
  Not available for Quest — the API has no image support for that type.
- archivist-api.js: added updateLink (PATCH) alongside the existing
  createLink/deleteLink so a link's alias can be corrected in place
  instead of delete+recreate, once a UI flow needs it.

Other endpoints surveyed but intentionally left for a future pass to
keep this change bounded: unified /campaigns/{id}/search, the
lightweight /entities picker endpoint, session transcript/handout ->
Journal auto-generation, the recordings/transcription pipeline, and
journal-folder CRUD (create/rename/delete).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed zero references anywhere in the repo (grep across .js/.json).
Stray backup file, not part of the build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Project Descriptions is documented as "Append an Archivist section to
the most likely description field," but projectDescription() only
appended for HTML-flagged slots (via mergeArchivistSection). For the
one plain-text slot in the registry (dnd5e/pf2e Actor
system.details.notes, adapter-registry.js html:false) — which becomes
the top candidate for actor subtypes lacking a biography field, e.g.
dnd5e's vehicle/group types — the code did stripHtml(archivistHtml)
with no reference to the field's existing content at all, silently
replacing whatever was there. This matches user reports of losing
existing descriptions.

Added mergeArchivistPlainSection (merge.js), which does for plain-text
fields what mergeArchivistSection already did for HTML ones: wrap the
Archivist content in a recognizable marker, append it if none exists
yet, and replace only that marked block on future syncs — never
touching the rest of the field's content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Quests were already being auto-imported exactly like Factions/Recaps —
_importArchivistMissing() pulls them in unconditionally, no reconcile/
selection step, and the count was already being computed
(count.imp.quests / plan.importFromArchivist.quests). They just never
made it into the summaryRows array the Step 6 table renders from, so
the count was invisible before clicking "Start Sync." Added a Quests
row identical in shape to the Recaps row, and updated Step 4's helper
note (which named Factions/Recaps as auto-imported) to mention Quests
too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two real bugs, both regressions from the Quests/image-upload work:

1. The upload-local-image button reused .archivist-edit-toggle's exact
   `position: absolute; top: 8px; right: 8px` slot with no offset, so
   it rendered directly on top of the edit-mode toggle — completely
   covering it. That's why editing a Faction (or any Character/Item/
   Location) sheet looked impossible: the button was there, just
   unclickable underneath the newer one. Added a distinguishing
   .archivist-upload-toggle class with `right: 48px` so they sit side
   by side.

2. _onRender fired _renderLinkedGrids()/_renderActorItemCards()/
   _renderRelatedQuests() without awaiting them, all async (dynamic
   import + DOM work). Verified against Foundry's own core source
   (client/applications/api/application.mjs): _render() does
   `await this._doEvent(this._onRender, ...)`, but since our _onRender
   wasn't declared async, that await resolved as soon as the
   synchronous body returned — before grid population finished. Opening
   a sheet by clicking a card in another custom sheet chains
   .render({force:true}).then(bringToFront) right after that early
   resolution, so the window could appear with cards populated but tab
   counts (the "(N)" suffix, set later by the same population pass)
   never applied — a real, reproducible gap, not just a cosmetic flash.
   Made _onRender (base class, plus the Location and Quest overrides
   that call super._onRender) async and properly awaited so Foundry's
   pipeline doesn't consider the render complete until population
   actually finishes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…l work

A collaborator independently built the same quest/journal feature set
on his fork (upstream/quest-journal-review-fixes, from the same 40d439f
base). His module.json/compatibility changes and the ask-chat-sidebar-tab.js
deletion are NOT ported — that branch reintroduces the exact incident this
whole effort started from (a v14-only build with compat.minimum low enough
for v13 clients to install, missing the Sidebar.TABS registration v13
actually needs). But several of his bug fixes are real, verified
independently against this branch's actual call sites, and are ported here:

- sync-dialog.js: journal content was being dropped entirely on import/sync
  (read row.description/row.summary but never row.content, which is where
  Journal's actual content lives) — the most serious bug found. Also:
  shift+click range-select was getting flipped back by the row-level click
  handler (missing stopPropagation); journal names were interpolated
  unescaped into the delete-confirmation dialog's HTML; no folder fallback
  for journals/quests on worlds that predate this feature, landing them in
  the world root. Wholesale-replaced with the fixed version, then re-added
  this branch's own relatedEntityRefs hydration (needed by the richer
  quest-linking Links tab) which his simpler version doesn't populate.
- world-setup-dialog.js: added _updateSyncStatusUI() (direct progress-bar
  DOM update) to the six "always imported in full" loops (characters,
  items, locations, factions, journals, quests), replacing a full
  await this.render() per entity — a real perf cost on large imports.
- archivist-sync.js: the realtime page-content-edit hook was PATCHing the
  quest's unchanged name back to the API on every content keystroke commit;
  quest content isn't a synced field and renames are already handled by a
  separate title-sync hook, so this was pure waste. Now a no-op return.
- archivist-api.js: _normalizeQuestPayload rewritten from a blocklist
  (rename+delete known keys, pass everything else through) to a strict
  whitelist, matching QuestCreate/QuestUpdate's extra="forbid" schema.
  Verified no current call site was actually vulnerable (all send narrow
  literal payloads), but the whitelist is a strictly safer contract for any
  future caller — verified functionally: read-only fields (questGiverId,
  progressLogEntries, first/lastSession) are dropped, empty-text objectives
  are dropped (API requires min_length=1), relatedEntityRefs' nested keys
  stay camelCase as the backend's alias_generator expects. Also fixed the
  keepalive guard: Chromium rejects keepalive fetches over ~64KB, so it's
  now conditional on body size instead of unconditional for all writes.
- merge.js: stripHtml no longer calls foundry.utils.TextEditor.stripHTML,
  which doesn't exist in either v13 or v14 core (verified against the
  actual installed Foundry app) — always fell through to the manual
  DOM-based strip anyway, just via a dead branch first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: camrun01 <cameron.b.llewellyn@gmail.com>
Adopts camrun91's approach: QuestPageSheetV2._localize(key) tries
game.i18n.localize() and returns null (not the raw key) on a miss or
when i18n isn't ready yet, so callers can chain `|| fallback` cleanly.
statusLabel/categoryLabel now try ARCHIVIST_SYNC.quest.status.*/
category.* (already present in lang/en.json from the earlier merge of
his branch's new keys) before falling back to the hardcoded English
strings that were the only source before.

Not in scope here: the edit-mode <select> option text in quest.hbs
(questCategory/status dropdowns) — those don't exist on his branch at
all (it kept the free-text Info tab instead of the structured form),
so there's nothing of his to adopt for them; they're still hardcoded
English, same as before this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: camrun01 <cameron.b.llewellyn@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

: type === 'Quest'
? arch.questName || arch.quest_name || arch.name || ''

P2 Badge Diff structured quest fields in manual sync

For linked Quest sheets, the manual Sync dialog now only compares the quest name here and then falls through to generic description/link checks, but Quest has no generic description and its relationships/objectives/status live in questData/relatedEntityRefs. If Archivist changes a quest's status, objectives, progress, or related entities, no diff is shown and _applyDiff() cannot pull those updates; add Quest-specific comparisons and application for the structured fields.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/modules/sheets/page-sheet-v2.js
Comment thread templates/sheets/quest.hbs
Comment thread scripts/modules/reconcile/reconcile-service.js
Comment thread scripts/modules/sheets/page-sheet-v2.js Outdated
Comment thread scripts/modules/reconcile/reconcile-service.js Outdated
Comment thread scripts/modules/reconcile/reconcile-service.js
Comment thread scripts/modules/sheets/page-sheet-v2.js
Comment thread scripts/modules/reconcile/reconcile-service.js
Comment thread scripts/modules/sheets/page-sheet-v2.js Outdated
Comment thread scripts/modules/sheets/page-sheet-v2.js
Comment thread .cursor/rules/40-archivist-api.mdc Outdated
camrun91 and others added 2 commits July 31, 2026 16:33
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
…n API rules

- Add _invalidateQuestsCache() and call from _commitEdits (quest),
  _commitQuestLinks, and _syncObjectives so related-quest tabs refresh
  immediately after linking or editing
- Correct 40-archivist-api.mdc to document that _normalizeQuestPayload
  emits snake_case on the wire while _normalizeQuestResponse ingests
  camelCase internally

Co-authored-by: Cameron Llewellyn <cameron.b.llewellyn@gmail.com>
Comment thread scripts/modules/sheets/page-sheet-v2.js
Read form objectives before building the commit payload so _commitEdits
sends a single atomic PATCH covering all editable quest fields, avoiding
local/API divergence when per-field objective sync calls fail silently.

Co-authored-by: Cameron Llewellyn <cameron.b.llewellyn@gmail.com>
@camrun91

camrun91 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

@greptile-apps give this one final review.

@camrun91
camrun91 merged commit b0bb3cd into staging Aug 3, 2026
1 check passed
@camrun91
camrun91 deleted the feat/api-augments-v14 branch August 5, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants