Skip to content

v2.0.0: rewritten data pipeline, finished UI, monthly auto-refresh#72

Merged
maxiride merged 53 commits into
mainfrom
v2
Jul 25, 2026
Merged

v2.0.0: rewritten data pipeline, finished UI, monthly auto-refresh#72
maxiride merged 53 commits into
mainfrom
v2

Conversation

@maxiride

@maxiride maxiride commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

Issues closed by this merge

Closes #46
Closes #17
Closes #1
Closes #55
Closes #53
Closes #57
Fixes #68
Closes #61
Closes #62
Closes #60
Closes #15
Closes #12

(#70 was an earlier in-progress PR for this same rewrite, already merged/closed — not an issue, so no closing keyword applies.)

Known gaps (not in this release)

#43 (separate player-XP-award figure), #52/#51 (save/share encounters), #18 (random creature filter), #23 (mobile responsiveness).

Before merging

  • main still carries the v1 code (GPLv3 LICENSE.md, semantic-release, umami analytics) that this branch does not have — resolved: LICENSE.md ported from main and root LICENSE_AON.md content fixed (it had been holding GPLv3 text instead of the Paizo Community Use Policy/OGL text LicenseDialog.vue promises); analytics already matches main (same self-hosted Umami script + website ID, no action needed); semantic-release deliberately dropped rather than ported — it was never wired into a working CI pipeline on main, and its whole point (SemVer bumps from Conventional Commits) conflicts with this project's stated "not SemVer, opinionated versioning" policy in CHANGELOG.md.
  • Review GitHub's Dependabot alerts on the default branch before/after merge.

Test plan

  • pnpm lint clean
  • pnpm build (vue-tsc strict) clean
  • Manual browser pass: creature load, search/filters (name, type, level range, rarity, size, alignment, family, traits, source), add-to-encounter, weak/base/elite XP math, threat bar thresholds, license dialog
  • Reviewer smoke-test on a fresh clone

maxiride and others added 30 commits February 18, 2025 08:29
Closes: #53, #48, #56

Upgraded Quasar and Vue, added Pinia integration.
Moved all encounter logic to dedicated Pinia store and simplified some business logic.

Adapted tables and objects to new AoN JSON dump obtainable directly from their website.

Added new menu buttons for support, donation and release info (to be reviewed further).
Allows developing and deploying the app under a subfolder by reading the base path from an environment variable. This enables hosting on GitHub Pages and having two versions coexist simultaneously before promoting to the root.
Adjusted `$primary` and `$secondary` color values in Quasar variables to align with project colors.
…eatures

Adjusted logic for level modifications, fixes #57
…rated to dedicated repository

Deleted unused scraper module, including associated configuration, dependencies, and source files, to clean up the codebase.
…ated to pnpm

Since the deletion of the webscraper (moved to separate repo), moved the quasar spa code to the root level.
…ed caching

Replaced `/table-data_stripped.json` with `/creatures.json` to load the correct data source.
…ogic

We no longer compute metadata values on the client but the app is provided with a metadata.json containing all of them.

Removed `indexedProperties` and related compute methods. Added metadata fetching to streamline handling of creature metadata like sizes and traits. Enhanced getters for improved state access.
Deleted `example-store.js` as it was unused.
… improving utility functions

Added meaningful comments to document getters and actions. Refactored `computeDeltaCost` to use a cleaner mapping approach for predefined deltas.
Introduced `$reset` method to reset the encounter state, clearing all creatures. Streamlines resetting functionality for encounters.
Deleted outdated `table-data_all.json` data source to clean up unnecessary artifacts and reduce redundancy.
… files

Added `quasar.config.ts`, `tsconfig.json`, and updated project files for TypeScript support. Configured ESLint and Prettier to align with project standards.
node_modules is centrally handled by pnpm through workspaces
…fetch

The old fetcher drove a headless Chromium browser through the AoN
Creatures.aspx UI (click "Load remaining", click "Export as JSON") to
scrape data. It broke when AoN added a cookie-consent overlay and
never captured NPCs, since the UI export only covers monsters.

The new tools/aon-downloader/fetch-creatures.js queries AoN's public
Elasticsearch endpoint directly: one HTTP request returns every
monster and NPC (4665 total, 917 of them NPCs) with a richer schema
(alignment, family, numeric level/hp/ac, array traits/size/sources).
Zero dependencies, no browser binary. A 7-day freshness guard means
routine dev/install runs never hit AoN's servers; refresh is explicit
(--force) or via the new monthly scheduled workflow.

Removes playwright, pino and the build-report.js companion script
(metadata generation is now inline in the fetch script).
CreaturesTable: name search, monster/NPC toggle, level range slider,
and a collapsible advanced filter panel (rarity, size, alignment,
family, traits, source). Restores filters lost in the v2 rewrite
(alignment, family) and adds searchable multi-select (FilterSelect.vue)
for the long trait/source/family lists.

EncounterList: was ~80% placeholder, now fully functional — weak/base
/elite toggle per creature, count stepper, per-creature and total XP,
clear-encounter action, AoN link per creature.

ThreatBar: rebuilt as a CSS track/fill instead of manually positioned
absolute badges; scale compresses instead of overflowing past the
extreme budget.

Fixes a latent Pinia reactivity bug (closes #17, milestone v2):
IndexPage destructured store computeds directly off the setup-store
(`const xpCost = encounterStore.xpCost`), freezing their value at
mount instead of tracking party level/size changes. Everything now
goes through storeToRefs.

Also: license dialog renders real Paizo Community Use Policy text
instead of a commented-out block (closes #68); header/menu links
point at real destinations; removed dead code (unused axios boot,
example-store, stub user-store, duplicate stores/index.js, debug
console.log/CSS).
update-creatures.yml: scheduled job (1st of every month) runs the new
fetch-creatures.js --force, commits refreshed public/creatures.json +
metadata.json to both development and main, then triggers deploy.

deploy.yml: builds the Quasar SPA and publishes dist/spa (data
included) to the gh-pages branch on push to main.

Removes GitHubPagesDeploymentWorkflow.yml and build-deploy.yaml — both
yarn-based and targeting the old src/webui v1 layout, non-functional
against this codebase.
Technical changelog entry covering the data pipeline rewrite, frontend
completion, and CI changes, plus a list of known gaps not addressed in
this release (issues #62, #43, #25, #52, #51, #18).

RELEASE_NOTES.md: user-facing summary — creature/NPC counts before and
after, the move to a single polite Elasticsearch request instead of
browser automation, and the encounter-builder improvements.
Reintroduces the Umami integration main had (same website ID, same
`encounter-add-creature` bounce-workaround event from #66), gated to
production builds only via the existing ctx.prod pattern so local dev
never sends real traffic.

Since users continuously add/remove creatures with no "encounter
finished" signal to hook, tracking is built around discrete mutation
events (creature added, weak/base/elite variant changed) plus a
debounced "encounter-snapshot" (party level/size, XP cost, threat)
fired 8s after the last edit, with an immediate flush on tab-hide/
pagehide so closing right after a change isn't lost. All three call
sites are centralized in encounter-store.ts, the single place that
already mutates this state.
package.json productName/description were still the Quasar CLI
scaffold defaults ("Quasar App" / "A Quasar Project"), which is what
showed up as the browser tab title.

Also adds a Buy Me a Coffee button to the header.
GM Core's Creature XP table only defines costs from party level -4 to
+4. computeDeltaCost silently clamped anything past that to the same
edge value, so a level 1 party vs. a level 25 solo still read as
merely "Extreme" instead of the effectively unwinnable fight it is.

Per the issue owner's own resolution direction (no unofficial
extrapolated formula, just make the cap visible): encounter-store now
exports isDeltaOutOfBounds(), and EncounterList shows a warning icon +
tooltip on any creature whose delta exceeds the table's range. The
per-creature cost itself is unchanged — still RAW as written.

Party-size XP budget scaling (GM Core "Different Party Sizes", pg. 76)
was already correct — xpBudget already scales with partySize and the
per-creature cost already factors in partyLevel via the delta.

Fixes #62
maxiride and others added 14 commits July 12, 2026 17:13
Vue's whitespace-condense compiler strips a leading literal space at
the start of an element's text content, so "Trivial" and "40" rendered
back-to-back as "Trivial40". Using   instead of a plain space
survives the trim.
First automated test suite for the project. Two thin layers:

- Vitest (src/stores/encounter-store.spec.ts): encodes GM Core's rules
  tables row by row — XP budget per party size, creature XP per level
  delta, weak/elite exceptions — plus store actions and the analytics
  event contract (debounced snapshot via fake timers). happy-dom
  environment since the store registers window/document listeners.
- Playwright, chromium-only (e2e/encounter-builder.spec.ts): smoke
  coverage of browse -> filter -> add -> balance against the real dev
  server and real AoN data, with regression tests for #17 (XP
  reactivity) and #62 (out-of-bounds delta warning). Config pins
  channel:'chromium' (full browser, not chromium_headless_shell) to
  dodge a known Windows worker-close hang (microsoft/playwright#39753).

ci.yml runs lint + both suites on pushes to development/v2 and PRs to
main, uploading the Playwright report on failure.

package.json: pnpm test now runs both suites; test:unit and test:e2e
run them individually.
Four ADRs document the non-obvious decisions made this cycle so future
contributors (this repo is public) don't have to reverse-engineer the
why from commit history:

- 0001: creature data via AoN's public Elasticsearch endpoint
- 0002: monthly data refresh + GitHub Pages deploy
- 0003: privacy-first analytics with self-hosted Umami
- 0004: testing strategy — Vitest for rules logic, Playwright for E2E

README rewritten for contributors: real tech stack table, setup
instructions, script reference, a repository tour, and a "how it
works"/contributing section pointing at the ADRs. Removed the stale
README_old.md (pre-rewrite v1 instructions, Vue CLI commands that no
longer apply).

CHANGELOG.md Unreleased section updated to reflect the testing and
docs additions.
Resolves the conflicts blocking PR #72 (v2 -> main). All five direct
conflicts were main-only bugfixes/tooling changes to src/webui/ (the
old Quasar 1 / Vue 2 app: NPC redirect fix, Umami analytics tweaks,
semantic-release setup) — code with no v2 equivalent because v2 is a
full rewrite that never carried src/webui/ forward. Resolution keeps
v2's deletion; none of those fixes apply to an app that no longer
exists.

Two more main-only additions surfaced once src/webui/ conflicts were
resolved (not flagged as direct conflicts, but git staged them from
main automatically since v2 has nothing at those paths):

- .releaserc.json (root): same semantic-release config as the
  src/webui one, just relocated there by git's rename heuristic.
  Versions off src/webui/package.json, which no longer exists. Removed
  for the same reason as the src/webui copy.
- .github/workflows/build-deploy.yaml: the same stale yarn-based
  deploy workflow already removed from development's own history
  (targeting the now-nonexistent src/webui layout) — main kept an
  independent copy. Removed; superseded by deploy.yml/ci.yml/
  update-creatures.yml.

One main-only change is kept: LICENSE_AON.md carries a substantially
more complete set of Paizo/OGL book notices on main (674 vs 283
lines) — a genuine content improvement, auto-merged cleanly with no
conflict.

Root LICENSE.md (GPLv3, main-only) merges in unchanged — no conflict,
resolves the "port LICENSE.md" item from the original PR body.
Non-technical recap for the actual audience (DMs and players, not
developers): bigger creature database, automatic monthly updates,
better filters, the new out-of-bounds difficulty warning, and a
cleaner UI. No jargon — no mention of Elasticsearch, CI, or anything
implementation-specific.

Shown once automatically (localStorage flag, versioned so the next
major release can announce itself the same way) and always reachable
again from the v2 menu ("What's new"). Also repoints the menu's
"Release notes" link from the technical CHANGELOG.md to the
user-facing RELEASE_NOTES.md — a better fit for the same audience.
CreaturesTable switches to q-table's native virtual-scroll — same
footprint (fixed height, no layout shift), scrolls to load more
instead of paging, still shows the total filtered count.

Found and fixed a real bug along the way: Vue 3.5.x silently breaks
Quasar 2.18.2's QTable virtual-scroll (renders zero rows, no error —
confirmed with an isolated repro outside this app's code). Pins vue
to an exact 3.4.38, documented in ADR 0005 so a routine dependency
bump doesn't reintroduce it invisibly.
Root cause of the reported multi-second scroll freeze: each row's
QBadge x2 + QBtn + QTooltip were mounting/unmounting on every scroll
tick. Replaced with plain HTML/CSS (.badge-outline spans, a native
<button> with a title attribute) — same look, zero component
overhead. Measured: worst single blocking task 256ms -> 88ms, total
wall time for a rapid-scroll burst 19.8s -> 3.6s.

Also dropped virtual-scroll entirely. It stopped being reliable across
a long debugging session (worked, then silently rendered zero rows
after restarts/HMR churn, reproduced even from a clean production
build) — not something to ship on. All ~4,665 filtered rows now render
at once inside a fixed-height, natively-scrollable table; no DOM
virtualization, but the lightweight per-row markup keeps that cheap.
Vue stays pinned to 3.4.38 regardless (still the known-good version).
Details in ADR 0005.
Reverts the previous commit's mistake. Rendering all ~4665 rows at once
(60k+ DOM nodes) was the actual cause of the reported Firefox freeze —
not virtual-scroll.

The earlier "virtual-scroll renders zero rows" diagnosis was a false
signal from the headless automation browser used for verification: it
measures the scroll viewport a frame too early, computes an empty
visible slice, and never recomputes without a resize/scroll event. Real
browsers settle layout on first paint and render correctly. Confirmed by
a user observing rows fine in Firefox while the automation browser showed
none until a forced resize.

Restores `virtual-scroll` (~60 rows in the DOM at a time). Drops the
markRaw() on the rows array — tested, it was never the cause. Keeps the
plain-HTML badges/button (a real per-row weight reduction).

Deletes ADR 0005: its whole premise (Vue 3.5 / markRaw break
virtual-scroll) rested on the same false automation signal and would
mislead future decisions. The Vue pin it justified is addressed
separately.
The exact 3.4.38 pin from 31a59ad left pnpm-lock.yaml's specifier
(^3.4.38) out of sync with package.json's caret-less value, breaking
`pnpm install --frozen-lockfile` in CI on every run since.

The pin's justification (Vue 3.5 breaks QTable virtual-scroll) doesn't
hold up: 580ecb6 already found the underlying "zero rows" symptom was
a false reading from the automation browser used to verify it, and
upstream confirms quasar has no vue peer constraint at all while
@quasar/app-vite accepts ^3.2.29+. Bumped to latest stable (3.5.40),
which also clears a pinia@3.0.3 peer-dependency warning (wants
vue@^3.5.11, unmet by 3.4.38).

Verified: frozen-lockfile install succeeds, lint clean, 41/41 unit
tests pass, production build succeeds, and 2/8 e2e tests completed
with real passing browser timings before the suite hit this repo's
own documented Windows worker-teardown hang (ADR 0004) -- unrelated
to this change, Linux CI isn't affected.
Every Playwright test starts with a fresh browser context (empty
localStorage), so MainLayout's onMounted check always found the
`pf2e-encounters:seen-release:v2` key unset and showed the "what's
new" dialog. Its backdrop then intercepted pointer events for the
rest of the test, timing out every click and, while open, disrupting
layout enough that table rows read as hidden too.

The e2e spec predates the dialog (250c970 vs. 26e63c8/7cf97c6) and
was never updated for it; this was never caught because CI has been
unable to reach the E2E step since the lockfile mismatch introduced
in 31a59ad. Seed the localStorage key via addInitScript so the
dialog never appears -- it isn't part of the critical path this smoke
suite covers.

Verified locally: the previously-timing-out "filters by creature
type" test now passes in 4.4s.
Root-caused via Quasar source (use-virtual-scroll.js, QVirtualScroll.js):
q-table's virtual-scroll renders three sibling <tbody> elements -- a
"before" padding tbody, the real .q-virtual-scroll__content tbody with
actual data rows, and an "after" padding tbody. Each padding tbody holds
a single spacer <tr><td style="height: Npx">, which is 0px at rest (top
of scroll) -- a real DOM node, not display:none, but with a zero-size
bounding box that Playwright's visibility check reports as hidden.

`tbody tr` matches all three tbodies, so `.first()` always grabbed the
"before" spacer row regardless of dataset size -- explaining both CI
failures: the bare toBeVisible() assertion hit a genuinely zero-height
element, and searchAndAddFirst's click target had no button inside the
spacer row at all, so the locator never resolved and timed out. Tests
that queried specific content classes (.creature-link, .badge-outline)
skipped past the spacer rows by construction and always passed, which
is why the failures looked selective rather than total.

Scoped all row locators to `tbody.q-virtual-scroll__content tr` via a
new contentRows() helper. Verified locally: full suite 8/8 passing
against a live dev server (Playwright's own webServer, not the
in-app automation browser known to mismeasure virtualized layouts).

No app code changed -- CreaturesTable.vue's virtual-scroll was correct
the whole time.
AoN's Elasticsearch index links each pre/post-Remaster statblock pair via
legacy_id/remaster_id fields (e.g. Advisor in Gamemastery Guide vs NPC Core).
Derive a per-creature edition from that and expose a filter toggle so
duplicate creatures can be narrowed to one edition; creatures with only one
edition are unaffected by the toggle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The v2 rewrite dropped the project's own GPLv3 LICENSE.md entirely, and
root LICENSE_AON.md had somehow ended up containing GPLv3 text instead
of the actual Paizo Community Use Policy / OGL / per-book copyright
notices — the correct content only existed in public/LICENSE_AON.md.
LicenseDialog.vue links to the root file promising exactly that content,
so this was a live bug, not just a v1/v2 reconciliation gap.

LICENSE.md ported verbatim from main. Root LICENSE_AON.md now matches
public/LICENSE_AON.md.
@maxiride maxiride self-assigned this Jul 25, 2026
@maxiride
maxiride merged commit 6fe33fa into main Jul 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment