Feature/cpr character import - #86
Merged
Merged
Conversation
added 11 commits
August 17, 2026 18:55
The Cyberpunk RED character importer. A point release on the same reasoning as 1.9.1 and 1.9.2: the sheet importer already exists and takes a PDF or a paste, so this adds a third source to it rather than introducing the capability.
Turns the document behind a six-digit code into the candidate object the sheet importer already takes, so a code becomes a third source beside the PDF and the paste box rather than a second way to write a sheet. Pure - no fetching - so the whole of the risk is testable without a network. No mapping table between the two vocabularies. The importer's aliases already normalise case and punctuation, so the Companion's AirVehicleTech and our "Air Vehicle Tech" reduce to the same key; emitting the export's own names means one alias table rather than two that drift. A real export's stat and skill names all land, bar WardrobeAndStyle, which needed an alias because ours is "Wardrobe & Style" and the ampersand does not survive normalising. Two things a real export does that the module this was reverse-engineered from does not mention, both of which broke the first version: collections are maps keyed by a generated uuid rather than arrays, and there is no role field - the role is the single key of roleAbilities. v1 exports are recognised but their stats and skills are not read. They are keyed by numeric id through a table we do not have, and guessing at it is how you import the wrong numbers without anyone noticing. The adapter says so instead, in a `missing` list the preview can show - which also carries what no export has at all: weapon damage, armour SP, vehicle SDP.
… exists Two fields added, because both are currencies a table spends between sessions and neither had anywhere to live but prose in the notes: a number you increment every week should be a field you can edit. Everything else the Companion carries went to fields we already had, which is most of it - max humanity, death save, luck, critical injuries, eddies to the bank, and personality/motivation/features/background folded into the one description the sheet keeps. The role ability's rank comes out of roleAbilities too, where it sits beside the role name. Images are not pulled. Players upload their own, so their storage paths are theirs to keep. Sub-skills and contacts are named in the missing list rather than forced somewhere: a specialised skill needs a row that can hold its specialisation and ours are fixed fields, so the honest answer is to say so. Per the standing note, the two new fields moved together with their importer aliases and the downloadable form, and the round-trip test still walks every label on it.
Two hops - the code names a uuid, the uuid names the document - behind POST /api/sheets/import/companion, feeding the same preview the PDF and the paste box feed. Nothing is written until the player looks at what came back. Server-side, never from the browser: it avoids CORS, and it keeps the player's address out of a request to a third party they did not choose to contact. A malformed code is refused before anything leaves the building. Kept apart from the parsing module, which stays pure. `fetch` is injected, so every failure is reachable in tests without a network: a timeout, a 404, a body that is not JSON, a lookup that answers with no uuid in it. We do not own this endpoint - it is a public Firestore path a Foundry module found - so the day it changes, the dialog has to say so rather than hang. Two distinctions the reasons are shaped around: a code the player can fix versus a service that is down, and their format changing versus the player mistyping. Reading the second as the first sends someone off to re-check a code that was fine all along. A test that passed `fetchImpl: undefined` fell through to the real global fetch and quietly called their service on every run. Now explicitly null.
A third source beside the PDF and the paste box, under Cyberpunk RED only — the Companion is a Cyberpunk tool, and a button that could only ever fail is worse than no button. Six characters, upper-cased as you type, Enter to fetch. The dialog had no idea what system was active: it learned that from a preview coming back, which is too late to decide what to offer. Passed in now from both windows that render it. The preview gained a TYPE IN YOURSELF line, which is a different statement from NOT RECOGNIZED. One is what we failed to read; this is what the source never held. An export names a car and carries no SDP, and a player not told that reads the gap as a broken import. It applies nothing on its own, like the other two.
…the cost Merging was leaving the past behind: a skill dropped at the source sat on the sheet forever, and a weapon row that no longer existed kept its damage. So an import now starts from empty. That is destructive, so it goes through a confirmation - and the confirmation lists the fields that will be lost by name. "Are you sure?" tells a player nothing they can act on; "you will lose weapon2_dmg and vehicle1_hp_max" tells them exactly what to write down before saying yes. Cancel puts them back with the preview intact. Occupancy survives a replace, and is not counted as a loss. Which car a player is sitting in is not character data, and re-importing a sheet is not a statement about it - clearing it would turn someone out of a vehicle mid-session with no message at all. The dialog is handed the sheet as it stands, because it cannot warn about losing something it cannot see.
Three inputs now sit above it and the window's content pane scrolls at 300px, so the preview and the error both landed out of sight - and a perfectly clear error looked exactly like nothing happening. The result scrolls itself into view when there is something to see. A reply that is not JSON now says the server may need restarting, rather than "could not reach server". An unknown route falls through to the single-page app and answers with HTML, which is what a backend that has not been restarted since this route was added does - and calling that a network fault sends someone hunting the wrong problem. The scroll is an optional call: jsdom has no scrollIntoView, and an effect that throws takes the whole dialog down. Nine tests failed that way before the guard, which is a fair warning about what it would have done in a browser that lacked it.
It was a block under three inputs in a pane that scrolls at 300px, so the one thing a player has to read before a destructive choice was the one thing they could not see. Scrolling it into view helped; giving it a window it can be dragged, read and decided in is the right shape. The window *is* the confirmation, so the two-step arm-then-confirm is gone. Everything needed to decide is on screen at once - what was recognised, what the source could never provide, and which of your own fields would be cleared - and the button says what it does: REPLACE SHEET WITH 12 FIELDS, beside CANCEL. Cancel closes the preview and leaves the import dialog open to try again from.
They were `title` attributes, which browsers only show once the pointer has rested - so the label arrived a beat late and never at all if you kept moving. The rail draws its own instead, on hover, immediately. Positioned from the button and portalled to the body, because `.rail-center` sets `overflow: hidden` and would clip anything drawn beside a button. One delegated listener on the rail rather than handlers on fourteen buttons: the label a button wants is on the button, and the rail reads it on the way past. `title` became `aria-label` plus `data-tip` - dropping title outright would have cost the accessible name, and keeping it would have shown the slow native tooltip underneath the new one. A test pins the consistency, which was the other half of the report. It reads the source rather than a render: several rail buttons only appear for an admin or in one view, so a render test would pass while missing exactly the ones most likely to be forgotten.
Five added files, five entries, audited against the README rather than eyeballed. Two existing entries also moved: the import dialog now lists three sources, and the Cyberpunk template carries IP and Reputation. The changelog leads with what a player does - build a character on the Companion, type six digits - and says plainly that applying replaces the sheet, since that is the part somebody needs to know before they try it rather than after.
The panel it opens holds master and music, so MASTER_VOLUME described half of it and implied the music slider was somewhere else.
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.
Summary
Test plan
Pre-merge checklist
Code quality:
Version & Release:
frontend/package.jsonversiondocker-compose.ymlAPP_VERSIONCHANGELOG.mdwith release notesBefore merging to main:
Related issues