Skip to content

Feature/cpr character import - #86

Merged
over2take merged 11 commits into
mainfrom
feature/cpr-character-import
Aug 18, 2026
Merged

Feature/cpr character import#86
over2take merged 11 commits into
mainfrom
feature/cpr-character-import

Conversation

@over2take

@over2take over2take commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Summary

Test plan

  • Tested locally
  • Tests pass

Pre-merge checklist

Code quality:

  • Code follows project style
  • No breaking changes (or clearly documented)
  • No console errors or warnings

Version & Release:

  • Version bumped? If releasing to users, update:
    • frontend/package.json version
    • docker-compose.yml APP_VERSION
    • CHANGELOG.md with release notes
  • GitHub Actions will auto-tag Docker images with the new version

Before merging to main:

  • All tests passing
  • PR reviewed and approved
  • Branch is up to date with main
  • No merge conflicts

Related issues

Developer 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.
@over2take
over2take merged commit 44fc9d5 into main Aug 18, 2026
3 checks passed
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.

1 participant