Conversation
The Superior Hunter's Defense -> Evasion option built a trait via mod5e/trait-cfg with no :name. A nil name reaches the Features name sort (clojure.string/lower-case as the sort key), which throws on a non-string and unmounts the whole character view (the black screen). Add :name "Evasion" and a regression test that lints the canonical trait-cfg and proves it would catch the name being dropped again. Also document why opt5e/evasion in :modifiers is inert (it yields a plain map, not a ::mods/* modifier).
common/lower-case wraps clojure.string/lower-case with (str x) so a nil or non-string sort/compare key folds to "" instead of throwing; aloof-sort-by, the plugin-name sort, and compute's name filter use it. common/feature-name renders an obvious [Unnamed feature] placeholder for a missing/blank name (shown and sorted by, never a silent blank); a wrong-typed name is a real bug, so it throws in dev and coerces in prod. Coerce where a non-string is acceptable (sort keys), surface where it is a bug (a name).
Goal order: never black-screen, always surface an error, make it useful. - error-boundary (React 18 getDerivedStateFromError) at three nested layers: app-root in core.cljs (last-resort, never black-screen), per-tab in character-display, per-item render-guard. Nearest boundary = smallest blast radius and most specific message. - Fault isolation by re-execution: isolate-culprit pinpoints the offending item in an aggregate failure (leave-one-out + self-pair for the 2-element sort trap + bisection); isolate-culprit-selection prunes ::entity/options and rebuilds via the pure entity/build to name the builder choice to re-pick. - feature-render-error panel and character-health-warning banner lead with the traced selection and the in-builder fix; the exact error is one copy away. - guarded-feature-list renders once and reuses the result on the happy path.
Dev still binds loopback by default. Set ORCPUB_HTTP_HOST=0.0.0.0 to reach the dev server by IP (e.g. a Windows browser hitting a WSL VM where localhost forwarding is flaky), without exposing it to the LAN otherwise. Prod is unchanged (it binds all interfaces unconditionally for container networking).
What cljs silently tolerates (arithmetic, comparisons, collection access nil-pun or coerce) versus what throws (clojure.string ops and name on non-strings), why (JS interop), and the practical habits: coerce string ops in one shared wrapper, use an obvious placeholder for a missing displayed value rather than a blank.
Rename import-validation -> orcbrew-validation. Translate spec problems into plain English (cljs.core/* predicate forms no longer dump raw to the console), de-dup and cap output, and add export-support helpers (classify-plugins, apply-user-edits, format-export-validation-for-log). db.cljs: humanize the invalid-stored-item log instead of pprinting raw explain-data.
Unify import/export console output to single pre-formatted strings (errors->str); export-warning modal events (auto-fix / as-is / cancel, multi-plugin); homebrew save validation that names only the unfilled field(s), classifies each as missing or invalid, and drives the field cues. Subs for the export modal and :builder-field-errors.
Export-warning modal (inline edit, multi-plugin) and import-log panel. Builder fields flag amber when empty / red when filled-but-invalid, name-field live coloration as typed, the same cue on HP dropdowns, spell class-lists, and selection option names.
# Conflicts: # test/cljs/orcpub/test_runner.cljs
# Conflicts: # test/cljs/orcpub/test_runner.cljs
… seq guarded-feature-list returned (doall (render-coll items)) directly — a bare lazy-seq of keyed, render-guard-headed hiccup items. A Reagent component returning a bare seq mishandles function-component-headed children, so React received their raw parts (the component fn, map-entries, keywords) as children -> 'Objects/Functions are not valid as a React child' (#31) on EVERY character's Features tab. Wrap the happy path in a fragment: (into [:<>] rendered). Ironic regression: the never-black-screen work was itself crashing the Features render, and its own boundary caught it — so it passed verification.
WSL2 can't reliably reach a loopback-bound dev server from the Windows browser, so bind 0.0.0.0. Keep the default loopback for everyone else and make it opt-in via `lein with-profile +wsl fig:dev`, which sets :orcpub-http-host through environ. Document the ORCPUB_HTTP_HOST env var (Docker/script launches) alongside it in .env.example.
Save and load previously named their specs independently, so the two sides could drift: if the load floor ever grew stricter than a save spec, content that saved fine would be quarantined on the next boot. content_specs.cljc makes the mapping explicit — save-spec-for gives the strict per-type save spec, valid-for-load? the loose load floor (an :option-pack string plus a letter-leading key) that never falsely quarantines real content — with a generative test proving save specs are a subset of the load floor.
…ers, cap raw Splits the message composition (character-report-message) and gate decision (character-report-block-reason) out of send-character-report so they're inspectable without sending. Fixes two issues the dev preview surfaced: client-supplied char-id and user-email flowed raw into the Subject/Cc headers (CR/LF header-injection vector) — now stripped of control chars; and the raw character blob was unbounded — now capped at 100k. Tests cover shape, cc omission, injection, and truncation.
Renders the exact composed report email (recipient, subject, cc, body, size) and the would-it-send gate for canned scenarios — including header-injection and oversize-blob probes — WITHOUT sending, so the endpoint is testable without SMTP or the UI flow. Run: lein report-preview (via a minimal :tools profile that skips the heavy cljs/devtools dev-config).
…ddress set Adds branding/report-recipient: prefers APP_SUPPORT_EMAIL, else reuses the admin error inbox (EMAIL_ERRORS_TO) that the app's error notifications already use — so an operator who configured error reporting needs no new setting for reports to send. Wires it into the report :to and the send gate (reason :no-recipient); dev preview shows which address resolves and its source.
support-email carries a fork default (e.g. DMV's thDM@dungeonmastersvault.com); reading :app-support-email directly bypassed that default, so a report could skip the support address and fall through to the error inbox. Reference the resolved support-email so the fallback only kicks in when no support address is set on either fork — and the def stays identical across forks so it won't re-conflict.
Merges the June bug-patch bundle, homebrew conflict-resolution/salvage, and the summer additions (PDF printing + B&W cards, character-load self-heal + recovery + report endpoint, spell-key aliases) into a single cohesive release section.
PDF printing (Firefox-fillable, printer-friendly B&W, legible card-back logo), character-load resilience (single-colon crash fix + self-heal + recovery panel), character-load report endpoint, and spell-key alias reconciliation. Changelog consolidated into one [Summer Patch] section.
This was referenced Aug 9, 2026
Open
Author
|
This PR also patches several issues that weren't previously filed. I opened them and they'll close automatically when this merges (closing keywords added to the description):
Also related to the previously-reported black-screen-on-load reports (#617, #577, #632) — the fail-soft rendering + character-load recovery in this PR addresses that class of crash. |
Author
|
Filed #679 for the corrupt/unreadable-content black-screen — the source-side half of the recurring reports #592, #617, #577 (previously closed as adblocker/user-issue). This PR turns that corrupt-file case from a fatal black screen into graceful recovery, so #679 will close on merge. The adblocker half of those older reports is unrelated and stays out of scope. |
This was referenced Aug 9, 2026
Author
added 5 commits
August 9, 2026 21:21
…rfaced in-UI Save-anyway used to persist invalid input verbatim — a race named "1@-asdml;" derived a broken key and rendered as garbage. Items now pass through sanitize-item-names: an invalid/blank name (and any nested trait/option name) becomes a letter-leading placeholder and the key is re-derived from it, so a structurally-broken key can never be saved. Normal save validates the real input instead of silently persisting placeholders. Structural export blockers are now listed in the UI per source and problem, instead of telling users to open the browser console.
A quarantined entry with a present-but-invalid name (e.g. a race named "@@@") used to force the user to hand-type a valid name before Fix & Restore would work — only blanks were auto-filled. It now runs the same sanitize-item-names coercion on restore (coerce-invalid-names), so "@@@" becomes "Unnamed Race" (re-keyed) with one click. The blocking red "must start with a letter" hint is now an amber "will be auto-named on restore", and the instructions say so.
A share link now carries the custom content the character uses, so a recipient sees it correctly without owning the .orcbrew. - share_bundle.cljc: pure extractor — walks the character's selection closure (direct + subclass->class, subrace->race, granted spells, race->language by name, reverse :spell-lists) into a plugins-shaped bundle; plus whitelist-bundle, a fail-closed structural gate for untrusted input. Unit-tested. - share_url.cljs: browser codec — gzip (CompressionStream) + base64url, version prefixed. Ships the FULL payload (never strips content — a feat/trait is its description); only when it's too big for any link does it fall back to a file. decode-shared treats every payload as hostile: input cap, decompression-bomb cap, safe reader (no eval), then the structural whitelist. - integrations.cljs: the share cluster (Copy link + native Share) precomputes the embedded URL on character/plugins change so it's ready at click time. - events.cljs / spell_subs.cljs: on landing with a #c= fragment, decode + run through the same spec load-floor a file import uses, into :shared-plugins — an ephemeral overlay folded into content lookups only (wins collisions, never persisted, never in the library/export, cleared on any character route). Custom magic items are out of scope: they live in the owner's server account (::mi5e/custom-items, by DB id), not in :plugins, so they can't ride in the link.
When a shared character carries embedded homebrew, a banner above the sheet reports how much custom content came with it and that it's loaded view-only (:shared-plugins, never persisted). "Keep in my library" collapses it into the recipient's :plugins under a distinct "<character> (shared)" source (so it can't overwrite an existing same-named source); "Dismiss" hides the banner. Any shared item whose key matches the recipient's own content but differs is flagged by name (share-bundle/collisions) — the shared version renders on this sheet, their own copy is untouched. collisions is pure + unit-tested.
Extends the share link to carry the character's custom magic items/weapons/armor,
which (unlike homebrew plugins) live in the owner's server account and can't
otherwise reach a recipient.
- share_bundle.cljc: used-custom-items picks the equipped items by running the
app's OWN expand to get each item's real key(s) and matching the character's
selection keys — never a re-derived name-to-kw, so keys line up by construction.
whitelist-shared extends the untrusted-input gate to a container
{:plugins {...} :custom-items [raw item ...]} (still accepts the legacy
bare-plugins payload).
- share_url.cljs: decode-shared returns the container.
- integrations.cljs: the share cluster embeds the used items (raw form) alongside
the homebrew bundle.
- events.cljs: apply keeps only items that expand cleanly (validates untrusted
item data and protects every downstream expand site) into :shared-custom-items;
cleared with :shared-plugins on the character route.
- equipment_subs.cljs: :shared-custom-items folds into ::mi5e/expanded-custom-items
(guarded per-item expand), which flows to the display maps AND the template
options — so a shared item both renders and applies its bonuses.
- options.cljc: two modifier-condition sites read custom items straight from
app-db; include :shared-custom-items there too or a shared weapon's conditional
bonus silently no-ops.
- views.cljs: the shared-content banner reports the custom-item count; items are
view-only (keeping items needs the server item-save API — a later follow-up).
The item builder's type dropdown is a controlled <select> whose value is the
item's ::mi/type; when that's nil the value matches no option, so the browser
shows the first option ("Wondrous Item") while the stored type stays nil. Since
::type is optional in the spec, the item saved type-less and later rendered with
a blank type (e.g. ", very rare"). Save now persists the shown default
(:wondrous-item) when no valid type is set, so what the user sees is what saves.
A homebrew name must derive a letter-leading key; names that lead with a number
("9 Lives") or symbol ("@@@") are the common keyword-trap. Instead of discarding
them, common/repair-name-lead recovers least-destructively:
1. leading number -> word ("9 Lives" -> "Nine Lives", "2nd Wind" -> "Second Wind")
2. leading symbols -> strip ("@@@bob" -> "Bob")
3. nothing usable -> nil (caller falls back to "Unnamed <Type>")
The number->word translator (cardinal + ordinal) is bounded at max-number-word
(default 999, a named constant): above it a leading number reads as data
(a year/stat, "2020 Vision") not a name-word, so it declines. It also bails on
glued tokens like "3d6"/"5e" rather than mangling them.
Pure + CLJC, so it's testable in both runtimes; pinned by 5 deftests in
common_test.cljc (runs under fig:test). Paper trail in
docs/kb/keyword-trap-name-repair.md. Not wired into the repair panel yet — this
is the foundation; the Manual-vs-Auto button split + collision check + moving
coercion off the default restore path (fixing repair-quarantined-source-rejects-
still-invalid) come next.
Commit 54f4e87 changed the source-less fill default "Unnamed Content" -> "Default Option Source" but left these 3 assertions on the old value, so the browser cljs suite (which CI doesn't run) was red. Aligns them to the shipped behavior.
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.
Homebrew resilience, character-load recovery, and PDF printing fixes
A batch of stability and homebrew-import fixes (from the Dungeon Master's Vault
fork) offered back upstream. Homebrew content no longer silently breaks characters
or the app, character loads survive corrupt data, and PDFs render/print correctly.
Closes
:cleric-source-nameinstead of:cleric), orphaning saved characters. Keys now come from a stable class identity, and orphaned saves repair on load.Likely addressed (please confirm against your repro)
U+0009 ... not available in Helvetica-BoldOblique WinAnsiEncoding). Field text is now coerced to WinAnsi (control/smart chars downgraded) before drawing, so it renders instead of throwing; appearance streams are baked so sheets render, print, and stay fillable in Firefox.Additional fixes (no existing issue found)
:— an unreadable token that crashed the entire character on load. Guarded at key generation; already-corrupt saves self-heal on load.Notes
Also adds: character sharing
Building on the same homebrew import/sanitize gate this patch hardens, a character's share link now carries the custom content it uses, so a recipient sees it correctly without owning the
.orcbrew.Pure logic is unit-tested (extractor, security whitelist, size/collision helpers).