Repository navigation
Conversation
Banners render markup again, every export runs one gate, homebrew can be rescued from a broken app, and the footer's diagnostic tools are named behind a switch. Consolidates six branches cut over one day. The hiccup-rendering fix existed only on a branch that never returned to integration, so the break it fixed stayed live for nine hours; this is the single place that work lives now. lein test 492/0. CLJS 332/0. lint 0 errors. changelog lint OK. Boot rescue 16/16 against three real breakages; builder-card export 11/11.
Found by screenshotting it, which had not been done -- the effect was written and shipped without anyone looking at it. Two faults, both from hanging the sparkle off the corner at top:-4px right:-3px. It landed in the gap between Save and the next header button, where it read as belonging to neither; and since the buttons are siblings in a row, the next one along painted over it, so most of it was simply not visible. Inset into the button's own top-right corner now, and the button raised a layer so its glow and sparkle draw above the sibling rather than under it.
clj-kondo flagged it. The value was always the literal, so the compiled CSS does not change.
Watched in a real browser for the first time, the heal worked when a character was opened and failed twice in ways the unit tests could not see. A reload never healed. :initialize-db restores the builder's in-progress character with a bare assoc straight into app-db, which never reaches :set-character, so the reconcilers did not run: a character that healed when opened from the list stayed broken after a refresh. Boot now goes through the same set-character, so both paths are one code path. The toast never painted. Every way a saved character reaches the builder dispatches [:route ...] beside :set-character, and :route queues a [:hide-message] that runs after anything :set-character queued, so the message was set and cleared in the same frame. :set-character now only records the heal, and :route announces it after its own clear. dispatch-n is FIFO, which makes that the one place navigation cannot erase the toast it delivered. Builder only, and once per heal. test/browser/character_heal_e2e.js drives both paths against lein e2e-server and asserts the stored key is rewritten, the option renders bound by name, the toast is on screen and the save button carries its cue. It found both bugs, passes 13/13 with the fix, and is registered with the probe runner so a later change cannot quietly undo either. Saving needs a verified account the in-memory server does not seed, so the save-clears-the-cue half stays in events_test.
Restoring the builder's draft on startup repaired it in memory and stopped there. The copy in browser storage stayed broken, so every refresh repaired the same keys again and announced it again: three refreshes, three repairs, three toasts, found by watching it in a browser. The repaired draft is now written back to browser storage when startup repaired something, and only then. That is the builder's own working copy, which it already rewrites on every edit. The character saved on the server is not touched; keeping the fix there still takes Save, and the save button still says so.
Loading sets an invalid entry aside for repair, and it did save that set-aside copy, but it left the entry in the library copy as well. So every load found the same entry and set it aside again, until some unrelated homebrew save happened to rewrite the library. Watched in a browser: set aside on load 1, set aside again on load 2, gone only after an ordinary save. The library copy is now rewritten on the load that sets the entry aside. The order is the safety: it is rewritten only after the set-aside copy is written. If storage is full and that write fails, the entry stays exactly where it was, which costs a repeat on the next load instead of the entry. The ordering lives in persist-set-aside!, which takes the storage writer as an argument, so the test drives a refused write directly. Faking a full localStorage by patching Storage.prototype did not take effect in the CLJS test page, although the same patch worked from a plain page, which is how the real behaviour was confirmed before this was written.
The save banner's "Export this source" link carried a copy of the source from the moment of saving and handed it to the export check. Since export began saving its cleanup back to the library, clicking that link after any later change wrote the old copy over the library: a source turned off came back on, an entry added since was gone, and the file lacked it too. This is the third time this link has broken, each time by trusting what it was handed. The export check now works from what the library holds whenever it holds the source (export-source), and falls back to a copy handed in only when it does not. The banners pass just the source name. The save always lands before the banner can be clicked, so the older bug where the file came out missing the item just saved cannot return by this route. builder_card_export_e2e.js now changes the source after saving and clicks the link again. The stored spell carries curly quotes so cleanup has something to rewrite, which is the only condition under which the old link wrote back. Checked both ways on lein e2e-server: 13/13 with this change; without it both new checks fail, the source back on and the added spell missing from the library and the file.
A content section stored as text or as a list still holds everything that was in it, and nothing recovered it. Salvage kept any section that was not a map exactly as it was, so it loaded silently, its source looked fine, and export later refused the source with no fix in sight. mend-section puts those back in shape: text read back, a list keyed by each entry's own key or the key its name derives, an empty section dropped. It refuses to guess -- two entries claiming one key, or an element that is not an entry, leaves the section for a person. mend-plugin, mend-library and mend-import-data run it over a source, a library, and freshly read import data, including a whole file stored as text. What nothing can read is now set aside instead of kept: a damaged section, and a whole source that is not a map. Salvage used to drop the latter from both sides, which a write-back of the library would have turned into an erasure. merge-plugins lets the newer value win wherever either side is not a map. Merging set-aside data called merge on text, which throws, and it runs while the app starts. For sections that are maps the result is unchanged.
Import now mends right after reading the file, before any other step walks the data. A section stored as text or as a list imports with everything in it and exports. A file whose whole content was stored as text used to crash inside the cleanup steps and import nothing, with no message. The notice adds "Repaired N damaged sections", the import log lists each repair, and a section nothing could read no longer counts as an imported item -- it said "Imported 1 items" for one. Loading mends before salvage and writes the repaired library back through the same ordering as the set-aside write-back, so each repair happens once. A section nothing could read is set aside, and the import notice and the needs-attention panel both count it. The panel names the damaged section and offers Export raw and Discard, since there is no name to type for it. Both used to count only entries, which would have read "0 entries couldn't load". Checked on lein e2e-server with every shape imported through the real import event: 28 of 28, including the startup repair written back on the first load and nothing left to repair on the second.
An import that skipped items arrived in the green success card with a check mark, so the one thing worth noticing looked like good news. It said "Skipped 1 invalid items" and sent people to the browser console. A partial import, or one with key clashes, now uses the warning card. Its lines carry a green check for what came in, an orange triangle for what was skipped and a wrench for what was repaired. The colour sits on the icon, not the text, so the text stays readable on the tinted box. The skipped line points at the import log, which lists each skipped item and why. Counts read "1 item" and "2 items". A clean import keeps the success card unchanged.
An item with no name showed as its bare key, ":no-name", which reads as code. It now shows as the placeholder Export & Auto-Fix will give it -- "Unnamed Spell", "Unnamed Monster" -- with the key beneath it in small type when there is one. The header also said "1 item need attention"; it reads "needs" now.
…er open menus The round import-log button sat in the bottom-right corner of every page, even with nothing in the log. On a laptop-height window the My Content menu reaches that corner, and the button, drawn at z-index 900, covered the menu's last item: clicking Selection Builder opened the empty log instead. The button now appears only when the log has something in it, or while its panel is open so the panel can be closed from it. Its z-index is 150: under an open header tab (200), still above the sticky button row (100). The panel it opens stays at 950. Its styles move from inline to garden. header_menus_e2e.js runs the shortest window a second time after a real partial import puts the button on the page. With the old z-index that pass fails on Selection Builder; with this change all 30 checks pass.
… is none Twenty-six probes carried their own copy of findChrome, a dozen variants between them. All of them looked only for chrome-linux/chrome, which Playwright 1.57 names chrome-linux64, so none found an installed browser by path. Five of the copies threw when /opt/pw-browsers did not exist. Eight more probes skipped the finder and hardcoded /opt/pw-browsers/chromium or read one env var each: CHROME, CHROME_PATH or PLAYWRIGHT_CHROMIUM. Outside the web sandbox those failed at launch unless all four variables were set by hand. test/browser/lib/find-chrome.js is now the one copy, used by every probe and by the CLJS test runner. It takes a path from any of those env vars (or E2E_CHROMIUM, which scripts/e2e/run.js reads), a binary or an install folder. Then it tries the newest chromium-<n> under PLAYWRIGHT_BROWSERS_PATH or /opt/pw-browsers, in either folder layout, and otherwise leaves it to Playwright's own browser. A variable that points at nothing is named on stderr rather than silently used. run-browser-probes.js resolves the browser once. With none, each probe is a SKIP naming the fix, not thirty identical launch failures; STRICT still fails the run. Checked with no browser variables set: 14 of 14 probes pass, and header_menus runs 30 of 30.
… just once A library that reads back as text of itself loaded nothing: the loader saw a string, copied it to plugins:corrupt and moved on. mend-library now reads it back when the text is a map keyed by source names, and the load writes the library back, so the repair happens once. A stored value that is still not a library was copied to plugins:corrupt on every load, because the active slot was never cleared; the unreadable-text case beside it always cleared its slot. It is now cleared as well, but only once the copy is saved, so a full storage cannot turn the copy into a loss.
Save anyway with placeholders in the selection builder derived the key straight from the name. A name like "9 Lives" gave :9-lives, which the save accepted and the next load set aside, so the placeholder save undid itself on refresh. The other builders run sanitize-item-names first; the selection builder now does too.
… kept Resolving an import conflict can rename or disable the copy already in the library. Those changes sit in the base library handed to the import store, and the store only wrote the library when at least one incoming entry was kept. When every incoming entry was set aside, the renames and disables went with it, in storage and on screen.
… export fixes_persist_e2e.js applies each repair through the real app, then reads localStorage, reloads, confirms the reload has nothing left to repair, and exports where there is a file. Paths: import cleanups, the loader's repairs and set-asides, a library stored as text, a stored value that is not a library, conflict renames, Auto-name & Restore, Export & Auto-Fix, Save anyway for a spell and a selection, and a character repaired on refresh. Against the build before the two load fixes and the selection fix it passes 25 of 33, failing exactly those paths; with them, 33 of 33.
An entry stored as text, or anything else that is not a map, reached the step that fills missing fields. That step assoc'ed into it and threw "No protocol method IAssociative.-assoc defined for type string", so the import stopped partway. It now leaves such an entry alone; validation skips it and the import log lists it, as with any entry the import cannot use. The full-import tests used an entry with no source as their broken example, which the next commit makes importable, so they use an entry that is text instead.
The July rule sends content with no source to Default Option Source, but only an :option-pack that was present and blank or nil got filled. An entry that left the field out was skipped on a single-source import, counted as imported and then set aside on a multi-source one, and set aside when found in stored homebrew. An entry with no usable :option-pack now takes the name of the source it is filed under. A single-source file has no source key, so there it takes the one source the other entries name. Only when there is no name to take does it get Default Option Source, as before. Import and load share the rule (e5/source-for), and the loader writes the filled library back with its other repairs, so it happens once. single-plugin-source now asks e5/declared-source, the same reading of which source a file's entries name. The persistence probe checks the import and the load path (35 of 35). The load test and the header-menu probe used a sourceless entry as their broken example and now use an entry that is text. The import cleanup table in ORCBREW_FILE_VALIDATION also said a nameless source became Default Option Source; it becomes Unnamed Content, as decided in b5ba38d.
A stored race whose :key was text left every page on the loading spinner, with only the rescue download bar. At startup core calls init-template-cache!, which builds the character template. The races subscription keeps races in a set sorted by key, and compare threw comparing :dwarf with "text". That throw happened outside render, where no error boundary can catch it, and the rest of core never ran, so the app was never mounted. Two layers. Each startup step in core runs through boot-step, which logs a failure and carries on, and the template cache catches its own failure; the save handler already works without a cache. The sorts that see homebrew keys or names use common/safe-compare, which orders mixed types by type and then by printed form instead of throwing: races, spells, class options, subclass names, template options and the spell list. boot_rescue_e2e.js stores such a race and checks the app still draws. It failed on the build before this change and passes on this one. A sweep of wrong types and shapes through import, reload, the template and export went from 43 crashes to 31: every key crash is gone, and what remains is traits and options lists, and a source that is not a map.
…port crashing on them A sweep of wrong types and shapes through import, reload and export found 31 crashes left, all in three places: - A :traits list (classes, subclasses, races, subraces, backgrounds) or a selection's :options holding something that is not a card. [42] or ["text"] made the import throw with nothing saved and no message; a bare 42 imported and then made export throw. - A source that is a number or unreadable text made the import throw. - A :key that is not a keyword, which used to stop the app starting. mend-plugin now repairs inside entries as well as sections, so import and load share it and a load writes a repair back once. A :key that is not a letter-first keyword takes the key the entry is filed under, which keeps characters that point at it working. A card list keeps its cards, turns text into a card with that name, drops anything else, wraps a lone card, and is removed when it is none of these. :options is only treated as a card list on a selection. A healthy entry is not touched and reports no repair. The import steps that walk every source or card now step past ones of the wrong shape, so a non-map source reaches the store, which already sets it aside. The import notice counts damaged entries apart from damaged sections, and the import log names each repair.
… still breaks Startup built the character builder's whole template before anything was drawn, outside every error boundary, only so autosave had a cached copy. A throw there stopped the app from mounting. Startup now reads storage and builds nothing from it: - The template cache starts on the first character save (ensure-template-cache!), from the sheet, list, parties or combat tracker. A save retries while it fills and says so if it never does, instead of skipping silently. - If loading the library itself throws, core copies it to plugins:corrupt (clearing the slot only once the copy is saved), starts without it and shows a notice with a download. If startup fails without homebrew too, the library is put back. For an entry no repair can mend, each conversion into a character option is guarded (homebrew-guard): races, subraces, backgrounds, feats, classes, subclasses and spell lists. A throw skips that entry and reports it; the report sets it aside in plugins:rejected, which is saved, and a notice names it with a link to My Content. Option builders return lazy sequences, so some damage only throws when a page draws the option, outside the guard. Fully realizing every entry to catch that costs about 2 s with a 12-source library against about 40 ms unrealized, so the everyday path does not. A deep check (homebrew-check) realizes entry by entry, only: after an import, for the sources it changed; after a Restore, for that source; and when a page has already failed, over everything, after which the page tries again. The conversions it runs are registered by the subscriptions, so the events namespace still does not load them. The startup notice's download goes through save-orcbrew-blob! (new :raw? option) and is declared as a hatch in export_paths_test, like the other unvalidated downloads. The error boundary's retry never redrew the page: Reagent does not re-render on React state alone, so it now forces the update. A spell whose :spell-lists is not a map is repaired on import and load like the other entry damage. homebrew_safety_net_e2e.js makes one race throw on conversion and another throw only while drawn. With this change all 16 checks pass; on the build before, 1 of the first 13.
Three defects, one cause: the save derived the key from the name every time.
save-collision tells an edit returning to its own slot from a name landing on
somebody else's by comparing :key on the item in the builder, and the save never
stamped that key back onto it. So author an item, save, fix a typo, save again
-> "already uses the name ... Saving would replace it", with no way forward but
renaming.
Renaming was worse. save-into-plugins knows a rename is a MOVE, but it is told
what to remove by (and (:key item) (not= (:key item) key)) -- and a newly
authored item never had :key. So the move degraded to a copy: the old entry
stayed in the library, in exports and in the source list, and with no :former-key
a character that had picked it had nothing to rebind to.
The key is now minted once -- (or (:key item) (name-to-kw name)) -- so editing
the Name field edits the name. Key and name may diverge; the key is an address,
the name is display text. The item is stamped back into the builder (and its
localStorage draft, so a refresh keeps it), which is what makes a second save
land in its own slot.
Changing a key stays possible where it is deliberate: import conflict resolution
and the manual relink, through rename-key-in-plugin.
:former-key was one slot, so A->B->C kept only the last hop and stranded anyone
still on A. It is now :former-keys, oldest first, capped at 4, and the cap drops
from the MIDDLE -- entry 0 is the key the item was minted under, which is what a
character nobody has opened since then still points at. The singular is still
read and folded in at the next key change, so existing libraries keep healing.
former-key-index needed one line: it already maps {former -> current} in one hop,
so a chain resolves directly.
Also corrects save-collision's docstring, which claimed the cross-source case
"informs rather than blocks". It blocks, and should: a key is a global address,
the combines that dedupe pick their winner by the hash order of source names,
and the ones that do not show both copies.
Fourteen tests over the scenarios that produced the three defects in the previous commit and had no coverage at any layer: save, save again, rename, come back to a restored draft, edit a saved item, mint a key another item holds (same source and other source), a chain of key changes, the cap, and an item still carrying the old singular :former-key. Layer is re-frame events through dispatch-sync. GOTCHA in there for the next person: the save returns its work as :dispatch-n and re-frame's dispatch is async, so a bare dispatch-sync returns before the save has applied. The fixture captures the queue and drains it. test/e2e/cljs-harness.js comes with them -- the cljs suite has no runner on this branch, and a test that cannot be run is not a test. It serves target/test over a static server and drives it with Playwright; run lein fig:test first. GOTCHA it was built with: serve the JS as charset=utf-8. A classic script with no charset is decoded as windows-1252, and orcpub/common.js's accent-range regex then fails to parse, which leaves orcpub.common undefined and every test that touches it erroring.
Editing saved homebrew: a second save was refused as a collision with itself, a rename left the old entry behind as an orphan, and a chain of renames stranded anyone still on the first key. One cause -- the save re-derived the key from the name every time -- and the key is now minted once. Comes with the lifecycle tests for the scenarios that produced them, and the cljs test harness needed to run them.
Four comments cited the owner's decisions by number only, and one told how an export click used to close its banner. They now state the behaviour and link the decision in homebrew-keys-design.md.
comment_discipline_test parses src/ and web/ (comments and docstrings only, never data strings) and flags history phrasing, docstrings over 6 lines and comment blocks over 4. FIELD NOTE blocks are exempt from length. The 336 existing hits are recorded in test/comment-baseline.edn and listed on every run as the cleanup list. A hit not in the baseline fails; a baseline entry no longer found fails until it is deleted, so the baseline only shrinks. The allowlist needs a reason per entry. rewrite-clj is declared in :dev-config at the version already on the classpath.
…ct text
An entry is now [file signal text hash], the hash of the whole comment block or
docstring, so an approval covers the text that was reviewed: editing it makes
the hit new again and the old approval stale, and both fail. Recorded debt that
is edited has to be fixed or reviewed the same way.
The baseline can no longer be re-recorded wholesale. COMMENT_BASELINE=prune only
removes cleaned entries and stale approvals; seed works only when there is no
baseline. A new hit is approved one entry at a time in :allowed, and a missing or
placeholder reason ("REVIEW: ...", "TODO") fails. A failing run prints each new
hit's entry ready to paste.
State wording ("a previously-corrupted spot") is filtered out of the history
check. The decision logic is a pure function with its own tests. Re-seeded: 335.
reconcile-former-keys takes the map former-key-indexes returns ({:flat :typed})
and refuses one without :typed, so a type-blind heal cannot be written by
accident; the three heal sites and one subscription build it the same way.
pick-homes is the only list of pick types: picked-only-at-home is derived from
it, and the missing-content check reads each extracted pick's :path through it
instead of a second content-type map. Tests: a heal without the typed index is
refused; every extracted pick but the subclass has a home; the missing check
runs from a real character.
The upgrade test reads ver.js values by function name, not position.
Each ;; FIELD NOTE (id): is recorded in test/comment-baseline.edn :field-notes as
{[file id] hash}, the hash of the note's text from its marker to a blank comment
line, the next note or a GOTCHA. A note that is new or edited fails until its
hash is recorded by hand, a recorded note that is gone fails, and two notes with
one id in a file fail. The failing run prints each line to paste. Prune leaves
field notes alone. This replaces field_notes_test's hand-kept phrase lists.
- move-homebrew-keeps-dependents.js moves to test/e2e and uses its lib (it required a scripts/e2e/lib.js that never existed, so it could not start). - The upgrade test's footer check fails with a message, instead of throwing, when the served bundle has no out/orcpub/ver.js (a production build). - Restore keeps what the author typed: a restore whose library write fails no longer clears the fields. quarantine-restore.js makes the write fail and checks the typed name is still there. - Every on/off switch takes keyboard focus and toggles with Space or Enter, through one helper (switch-attrs) that calls the switch's own click handler. switches-keyboard.js covers all of them. Both new checks fail on the bundle from before this change.
It reads the version, date and description from the file the served build keeps them in: out/orcpub/ver.js behind the dev loader, or the production orcpub.js. The footer passes when all three appear there, in either mode.
…ecked
- A FIELD NOTE is exempt from the length check for its own lines only; the rest
of its block is counted, so a long comment beside a note is still flagged.
Note spans are computed once (note-spans) for both the length check and the
note recorder.
- Docstrings given as ^{:doc ...} on the name or as an attr-map's :doc are
checked like positional ones.
- A blank line already ends a comment block (rewrite-clj gives it its own
:newline node, top level and inside forms); tests now pin that, and that a
note does not absorb text after a blank line.
The baseline is unchanged: integration has no blocks with notes, and none of its
metadata docstrings trips a rule.
The check found 25 comments and docstrings here that broke the rule. 24 are trimmed to spec, their history already kept in key-collision-behavior.md and homebrew-keys-design.md; the library-links `links` field table is approved as spec with its reason. The four FIELD NOTEs were checked against the code and recorded, so field_notes_test and its phrase lists go. The 26 recorded entries this branch cleaned are pruned. Only comments and docstrings changed.
scripts/fold-branch-changelog.sh "Summer Patch": the entries land as ### port/save-gate with its Highlights, and docs/branch-changelog.md is removed so the changelog guard passes once this lands on integration.
A deliberate delete or a re-import goes ahead and reports the links it left pointing at nothing, and the first-load fetch writes only into an empty library. The entry and the Highlights claimed every write was refused.
Homebrew keys: one gate for library writes, links that follow keys, characters that heal. Includes the comment-rule check (from #38).
add-character and remove-character pulled the party from the request's :db, the snapshot the db-interceptor takes before the handler runs, so the response showed the party as it was before the change. Pull from the transaction's :db-after, as update-party-name already reads a fresh db. The client updates optimistically and ignores the body, so nothing visible broke; the new party_test fails on the old pull.
Every comment and docstring the check recorded as debt on integration is trimmed to spec: what the code does, its args and return, and a short GOTCHA at most. History, rationale and measurements move to the knowledge base; existing KB pointers stay. Docstrings found stale against the code while trimming are corrected (CSP dev-mode notes, spell-packing and PDF return shapes, share-url names, among others). Only comment and docstring text changes: with comments stripped and strings masked, every file tokenizes the same as before, and each changed string is a docstring. COMMENT_BASELINE=prune removed all 309 entries. .gitignore ignores the linter's cache rather than the whole .clj-kondo directory, so config.edn stays addable.
Fixes the comment check's 309 recorded hits (baseline now empty) and makes party add/remove answer with the updated party.
Brings save-gate (#37: builder origin, move-or-refuse saves, the shared library-links model that repoints every link on a key change) and comment-debt (#39). Where both sides changed one place: - homebrew save, collision and links: integration's, which supersedes the two relocate fixes and the gating revert made here - accounts, auth and sharing: this branch's (share line, party share tokens, image-probe limits, reg-api-sub logging out on 401) - the library loader keeps this branch's repair step (mend-library, persist-set-aside!, fail-soft startup), which integration never had; integration's additions beside it (relinks, revision, cross-tab watch, pre-fix copy, builder origin) are all kept - combined: party add/remove answer with the party after the write; imports and restores still run the homebrew build check through commit-library; the selection save-anyway sanitizes names under the save gate; export reads the live library and no longer writes back; an import that keeps nothing still stores renames of existing items - the heal toast stays in :route, where a route change cannot clear it - dropped a doubled set-aside notice the auto-merge produced
Integration's comment check (#37) fails docstrings over 6 lines, comment blocks over 4 and history phrasing. 77 hits here, all from code written before the rule. Docstrings are spec now, with a GOTCHA where a reader would otherwise write a bug; the rationale moved out, and each trimmed comment names the KB page that holds it (account-flows.md, homebrew-safety-net.md, share-links.md, homebrew-keys-design.md). Code is unchanged, checked token by token against the merge commit. Also marks the startup write that restores a set-aside library as part of the library load, the one write outside the gate the gate test allows.
set-aside-broken-homebrew wrote the library directly, bypassing the gate's revision count and cross-tab merge, which library_gate_test now refuses. It writes the set-aside copy first as before, then commits the smaller library through commit-library with :deleting?, since removing the entry is deliberate. If storage refuses the set-aside copy, the entry now stays in the library and the author is told; before, it was removed for that session only. Also trims this file's comments to the comment rule.
Integration's heal of a renamed pick waits for the template's offered keys, and integration built the template at startup. This branch builds nothing at startup, and only a save asked for the template, so merged, a character with a renamed pick stayed unhealed until it was saved. :set-character and the routes to the builder and to a character's page now ask for the template; other pages still build nothing. A heal that lands when the template arrives comes after the route change, so it announces itself through ::e5/announce-heal, which shares one rule with the :route announcement (heal-announcement). Measured with scripts/e2e/template-on-open.js, three runs each: opening a character heals in 3 ms (before: not within 15 s); login and the character list build nothing; the builder's first load goes from about 1.05 s to 1.22 s, the template's cost moving from the first save to the first builder visit. test/browser/character_heal_e2e.js passes 13/13. The heal tests now follow integration's model, and the test runner registers the template request as a no-op so the watcher it starts cannot dispatch into later tests.
The template's 230-320 ms build ran just after the builder appeared, so the page was briefly unresponsive right as it showed. On the builder and character pages it is now built at startup, before the app mounts: the spinner stays up about a quarter second longer and the page is responsive when it appears. Measured over three loads: the template lands before the spinner clears, and what follows is the usual 60-150 ms of drawing. Other pages still build nothing at startup; the step is guarded like the lazy build, so a failure leaves the cache empty.
The stale-bundle check kept its own list of source folders, and the list missed web/cljs, so startup edits were tested against an old bundle with no warning. It now reads :watch-dirs from the build config of whichever bundle is on disk, the same list the compiler uses, and stops the run if it cannot read it rather than skipping the check. Proved by making web/cljs/orcpub/core.cljs newer than the bundle: the run names it.
The boot spinner was spiral.gif: a 60 KB separate download, stair-stepped against the gradient. It is now drawn inline by the server, so it shows with the first bytes of the page, picked at random per load for now so all three get seen; ?spinner=spiral|d20|spell forces one, and anything else falls back to random. The shapes are computed at load (orcpub.loading-spinner): the spiral's curve, the d20's twenty face placements, and a ring of made-up runes. Numbers are formatted with Locale/ROOT so a server locale cannot write 100,5 into the SVG. Only transform and opacity animate, each moving piece its own element, so all three keep moving while the app builds: measured on the real page with the main thread blocked for two seconds, the spiral and spell circle stay smooth and the d20 drops to about 10 frames/s.
Owner
Author
|
Superseded by #48, which merges this work into integration (this PR targeted develop because integration had been deleted at the time). #48 also rewrites 37 commits' author email to github@codeglaze.com, so its hashes differ from this branch's. Closing in favour of #48. |
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.
Accounts: signing up, signing in, recovering — rebuilt, plus the hotfix batch that preceded it.
Base is
develop, which is the only base branch on this remote — theintegrationbranch was deleted upstream. That makes the diff wide: it carries the earlier share-links/hotfix batch as well as the accounts work. The accounts work is the last 63 commits, from2c9c553donward; everything before that has been reviewed already.What a reviewer should look hardest at
Auth and sessions (
src/clj/orcpub/routes.clj,src/clj/orcpub/security.clj)check-authis no longer purely stateless: tokens carry a:mintedstamp and one minted before an account's password moved is refused. The register is in memory (rebuilt from Datomic at boot and hourly) rather than a per-request DB read, because the live peer is Postgres-backed and:orcpub.user/usernameis neither unique nor indexed.withdraw-sessions— "sign out everywhere". Deliberately does not require the password; reasoning is in the docstring.Password rules (
src/cljc/orcpub/registration.cljc)\p{L}/\p{N}, which are Java-only and silently fail in ClojureScript.Email (
src/clj/orcpub/email.clj)Known and deliberate
Deploy notes
api.pwnedpasswords.com. On by default;ORCPUB_PWNED_CHECK=offis the kill switch.Verification
590 JVM tests / 4265 assertions, 434 ClojureScript / 1956, clj-kondo 0/0.
scripts/e2e/auth-flows.js— 45 browser checks over register → verify → login → reset → sign out. These flows had no coverage and no route to any: the verification and reset keys exist only inside an email, and registration fails outright when none can be sent.scripts/e2e/lib/mail-sink.jsis a dependency-free SMTP server that makes them drivable.Background, with the measurements and the rejected alternatives:
docs/kb/account-flows.mdonagents/develop.