Skip to content

Deploy: staging → main - #744

Merged
gaidheal1 merged 61 commits into
mainfrom
staging
Aug 9, 2026
Merged

Deploy: staging → main#744
gaidheal1 merged 61 commits into
mainfrom
staging

Conversation

@gaidheal1

Copy link
Copy Markdown
Member

Release summary

Features

  • Villages now grow more realistically: worker productivity, mill/bakery capacity, and building assignments scale with village population and character link points instead of fixed slot counts.
  • Mills and bakeries can now share a single building's capacity across both roles, so villages pack production buildings more efficiently.

Fixes and UX improvements

  • Fixed character work schedules so activities respect the assigned work building's actual opening hours.
  • Large wheat/flour quantities now display in tonnes instead of unreadable multi-thousand-kg figures.
  • Map: docked the building/character detail panel inside the map, added list hover feedback, and animated the selection highlight to match the palette.
  • Map: character outlines are crisper, tooltips are more concise and contextual (no more duplicate hover outlines), and the detail card is more compact with reordered facts.
  • Map: added a fly-to button on the detail card header, shrunk the tooltip's "View details" button, and fixed the cursor to default to an arrow outside of inputs.
  • Kept body/heading/button/link/caption text at mobile sizes on larger screens for more consistent typography.

Developer experience and quality

  • Automatically post a Discord announcement when a GitHub release is published.
  • Upgraded backend (Django, cryptography) and frontend (Storybook, ESLint, Vite, MapLibre, nanoid) dependencies within semver range, closing 3 open high-severity Dependabot alerts.
  • Fixed a bug that broke GitHub's release-notes changelog config (unquoted YAML severity labels).
  • Completed a full mypy typing pass across the backend.
  • Consolidated all frontend HTTP calls onto a single fetch-based stack, migrating the auth API off axios.
  • Refactored the activity timer, entity search input, and activity reward screen to pull reusable logic out of components/hooks.
  • Renamed spawn_villages to generate_villages and routed mill/bakery lookups through the new BuildingCapability model for clearer economic modeling.
  • Fixed a broken Storybook link and swapped a test's raw User model creation for the user_factory.

Technical notes

  • New migrations: economy/migrations/0004_buildingcapability.py (adds BuildingCapability model), 0005_backfill_building_capabilities.py, 0006_goodsconversionstate_activity.py, 0007_backfill_conversion_state_activity.py, 0008_alter_goodsconversionstate_activity.py.
  • No new env vars. The Discord announcement workflow (from PR development → staging #730) requires the DISCORD_RELEASE_WEBHOOK_URL secret (already configured).
  • Rollback: revert the merge commit; no destructive data migrations are included.

Test plan

  • CI passes
  • Verified on staging

claude and others added 30 commits August 4, 2026 00:44
useActivityTimer had one browser-only dependency: a beforeunload
listener warning the user before closing the tab mid-activity. Move
it into a new useUnloadWarning(active) hook and call it from
GameContext, the component that owns the timer, so the timer hook
itself has zero window references and stays platform-neutral.

Fixes #572
useEntitySearchInput reached into the DOM directly for click-outside
dismissal and typed its keydown handler against KeyboardEvent<HTMLInputElement>.
This splits it along the platform boundary, per #573:

- Hook now exposes intent-shaped actions (onSelectNext, onSelectPrevious,
  onDismiss, onCommit) instead of a DOM-typed handleKeyDown.
- The document mousedown listener for click-outside dismissal moves to
  EntitySearchInput.tsx, which owns the rootRef and translates the
  gesture into onDismiss().
- EntitySearchInput.tsx's handleKeyDown translates raw KeyboardEvents
  into the hook's semantic actions.
- window.setTimeout/window.clearTimeout -> bare setTimeout/clearTimeout.

Fixes #573
…to a pure util

ActivityRewardScreen computed the post-activity reward breakdown —
premium bonus, task bonus, total XP, and the four summary-line
phrasings — inline in its render path. Extracts that into a pure
buildActivityRewardBreakdown() function in utils/, matching the
existing buildActivityRewardToastMessage precedent.

- Added frontend/src/utils/activityRewardBreakdown.ts: takes the raw
  reward fields (activityName, xpGained, baseXp, xpMultiplier,
  taskXpMultiplier, levelUps, elapsedSeconds) and returns the
  breakdown rows, the premium-multiplier gate (isLikelyPremiumUser),
  and the assembled summary line. No React, no side effects.
- ActivityRewardScreen.tsx now calls the util and just renders the
  result; the inline calc block is gone.
- Added activityRewardBreakdown.test.ts covering zero XP, no
  multipliers, premium-only, task-only, both multipliers, all four
  summary phrasings, level-up normalization, and missing XP.
- Deliberately not merged with buildActivityRewardToastMessage —
  documented why in the new file's doc comment (different output
  shapes, mostly-disjoint inputs).

Fixes #575
…og config

Unquoted "Severity: critical" etc. were parsed as YAML mappings instead of
strings, breaking the generate-release-notes API with a 400 (config schema
mismatch on changelog.categories[1].labels).
useActivityTimer had one browser-only dependency: a beforeunload
listener warning the user before closing the tab mid-activity. Move
it into a new useUnloadWarning(active) hook and call it from
GameContext, the component that owns the timer, so the timer hook
itself has zero window references and stays platform-neutral.

Fixes #572
useEntitySearchInput reached into the DOM directly for click-outside
dismissal and typed its keydown handler against KeyboardEvent<HTMLInputElement>.
This splits it along the platform boundary, per #573:

- Hook now exposes intent-shaped actions (onSelectNext, onSelectPrevious,
  onDismiss, onCommit) instead of a DOM-typed handleKeyDown.
- The document mousedown listener for click-outside dismissal moves to
  EntitySearchInput.tsx, which owns the rootRef and translates the
  gesture into onDismiss().
- EntitySearchInput.tsx's handleKeyDown translates raw KeyboardEvents
  into the hook's semantic actions.
- window.setTimeout/window.clearTimeout -> bare setTimeout/clearTimeout.

Fixes #573
…ue-575-implementation

Resolves the ActivityRewardScreen.tsx conflict in favor of the base
branch's RewardBreakdown component extraction, which supersedes this
branch's buildActivityRewardBreakdown util (same calc logic, more
broadly reused — also by the unified timer's Results state). Removes
the now-redundant util and its test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSwWkchRGye37hoCmEfDCz
Adds a workflow that fires on release:published and sends an embed
(title, notes, link) to a Discord channel via a webhook stored in
the DISCORD_RELEASE_WEBHOOK_URL secret.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSwWkchRGye37hoCmEfDCz
Full pip-compile --upgrade run on requirements.in/dev-requirements.in
(patch/minor bumps only, e.g. django 5.2.16->5.2.17, cryptography
49.0.0->50.0.0), plus nanoid 3.3.16->3.3.18 in the frontend lockfile.
Fixes the 3 open high-severity Dependabot alerts (cryptography
Bleichenbacher oracle, nanoid infinite-loop bug).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSwWkchRGye37hoCmEfDCz
npm update across the frontend workspace (Storybook 10.5.6->10.5.7,
eslint, vite, maplibre-gl, axe-core, etc. — patch/minor only).
Packages with available major bumps (typescript 7, jsdom 30,
framer-motion 13, etc.) were left as-is pending separate review.
Lint and unit tests (524 tests) pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSwWkchRGye37hoCmEfDCz
…ntain permissions'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
useActivityTimer had one browser-only dependency: a beforeunload
listener warning the user before closing the tab mid-activity. Move
it into a new useUnloadWarning(active) hook and call it from
GameContext, the component that owns the timer, so the timer hook
itself has zero window references and stays platform-neutral.

Fixes #572
Extract beforeunload guard out of useActivityTimer
useEntitySearchInput reached into the DOM directly for click-outside
dismissal and typed its keydown handler against KeyboardEvent<HTMLInputElement>.
This splits it along the platform boundary, per #573:

- Hook now exposes intent-shaped actions (onSelectNext, onSelectPrevious,
  onDismiss, onCommit) instead of a DOM-typed handleKeyDown.
- The document mousedown listener for click-outside dismissal moves to
  EntitySearchInput.tsx, which owns the rootRef and translates the
  gesture into onDismiss().
- EntitySearchInput.tsx's handleKeyDown translates raw KeyboardEvents
  into the hook's semantic actions.
- window.setTimeout/window.clearTimeout -> bare setTimeout/clearTimeout.

Fixes #573
…ion-wrgq0t

Move DOM event handling out of useEntitySearchInput into its component
Extract XP/multiplier reward breakdown out of ActivityRewardScreen into a pure util
api/auth.ts was the only consumer of axios; the rest of api/ already
uses fetch via apiFetch. Ports it over and removes the dependency.

- api/auth.ts's loginUser now calls apiFetch("/auth/jwt/create/", ...)
  instead of the axios instance in api/axios.ts (deleted).
- apiFetch gains a skipAuth option: skips both the Authorization
  header and the getValidAccessToken() refresh path, for endpoints
  that are unauthenticated by design. A 401 on a skipAuth call is
  treated as "credentials rejected", not "session died" — it does not
  call handleUnauthorized() or clear storage, unlike a normal 401.
  loginUser passes skipAuth: true, since there's no session yet to
  refresh and a failed login shouldn't log out whatever's already
  stored.
- axios removed from package.json/package-lock.json.
- Added tests: api.test.ts covers skipAuth (no Authorization header,
  no handler/refresh invoked, explicitAccessToken is still ignored
  under skipAuth); auth.test.ts covers loginUser's call shape and
  error passthrough.

withCredentials determination: NOT load-bearing. DRF's
DEFAULT_AUTHENTICATION_CLASSES only has JWTAuthentication —
SessionAuthentication is commented out (progress_rpg/settings/base.py)
— so no endpoint reads the session cookie for auth; the JWT bearer
token is what actually authenticates every request. No
credentials: 'include' equivalent was needed on apiFetch.

Note for reviewers: api/auth.ts's loginUser was already dead code
before this change — grepping the frontend turns up no callers. The
app's actual login/register/password-reset flows (useLogin.ts,
useRegister.ts, usePasswordReset.ts) call fetch directly and were
never on axios, so they're unaffected by this change and out of
scope here; wiring them onto this module would be a separate,
larger change.

Fixes #576
Consolidate onto a single HTTP stack: migrate api/auth.ts off axios
Introduces a capability abstraction so a building can hold multiple
production activities (e.g. milling and baking) instead of exactly one
via building_type. Additive only: BuildingCapability rows are backfilled
from existing building_type values, and no callers (capacity_services,
tasks.py, economy_status) are changed yet - step 1 of
.claude/plans/building-capabilities-plan.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
…lity

find_mill/find_bakery and population_capacity_report's mills/bakeries
querysets now filter by capabilities__activity instead of building_type,
so a building with multiple capabilities (e.g. a communal building that
both mills and bakes) is counted for every relevant role. Test fixtures
that create mill/bakery buildings now also create the matching
BuildingCapability row. Step 2 of
.claude/plans/building-capabilities-plan.md; economy_status output is
unchanged. find_granary and field_shelter lookups stay on building_type
(deliberately excluded from capabilities, per the plan).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
…ate by activity

advance_mill_economy_tick/advance_bakery_economy_tick now iterate
buildings by capabilities__activity instead of building_type, so a
building with multiple capabilities (e.g. a communal building that both
mills and bakes) gets ticked for each one.

GoodsConversionState moves from a per-building OneToOneField to a
per-(building, activity) ForeignKey + unique constraint, with a backfill
migration mapping existing rows by their building's building_type. This
is required alongside the tick change: with a single per-building flag,
the first tick to run today would mark the building "processed" and the
second would silently skip - see .claude/plans/building-capabilities-plan.md's
Edge Cases. Added a regression test (MultiCapabilityBuildingTests)
covering exactly this: a communal building with both capabilities
produces bread on the same day, which is only possible if both ticks ran.

economy_status now prints every conversion state on a building instead of
assuming one. Step 3 of the plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
…lan)

Sums link_points across every player link a character has ever had
(active and historical), mirroring the existing Player.total_link_points.
This will drive link-scaled worker productivity in a later step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
worker_capacity_present() weights each present worker by
1 + capped(total_link_points / SCALE), added alongside workers_present
rather than replacing it, and wired into the three economy conversion
ticks plus population_capacity_report. Unlinked NPCs still contribute
exactly 1, so existing behavior is unchanged until a character is
actually linked and played.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
Wire population_estimation.starting_population() into
planning_services.settlement_plan() at the end of both generation paths
(watabou_import.import_watabou_village and spawn_villages), logging the
recommended granary/milling/baking/farming building counts. Compute-and-log
only - doesn't change which buildings get created yet, so the recommended
numbers can be validated against real village files before generation
behavior changes in a later step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
Renames the management command file/tests and every call site
(call_command, imports, the spawn_villages_task Celery task) plus
comments and docs referencing the old name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
gaidheal1 and others added 28 commits August 9, 2026 18:52
…lan)

Sums link_points across every player link a character has ever had
(active and historical), mirroring the existing Player.total_link_points.
This will drive link-scaled worker productivity in a later step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
worker_capacity_present() weights each present worker by
1 + capped(total_link_points / SCALE), added alongside workers_present
rather than replacing it, and wired into the three economy conversion
ticks plus population_capacity_report. Unlinked NPCs still contribute
exactly 1, so existing behavior is unchanged until a character is
actually linked and played.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
Wire population_estimation.starting_population() into
planning_services.settlement_plan() at the end of both generation paths
(watabou_import.import_watabou_village and spawn_villages), logging the
recommended granary/milling/baking/farming building counts. Compute-and-log
only - doesn't change which buildings get created yet, so the recommended
numbers can be validated against real village files before generation
behavior changes in a later step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
Renames the management command file/tests and every call site
(call_command, imports, the spawn_villages_task Celery task) plus
comments and docs referencing the old name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
Farming/milling/baking buildings are now staffed against
settlement_plan's workers_needed estimate instead of a flat random 2-3
per building, greedily filling one building to MAX_WORKERS_PER_BUILDING
before moving to the next of the same role - a village with too few
eligible working-age residents ends up understaffed rather than evenly
thin everywhere, which is what actually makes the "struggling at spawn"
arc real. A building holding multiple capabilities (e.g. a packed
"communal" building) is staffed to the larger of its activities' demand,
since a present worker counts fully toward every activity their building
holds at once. Granary is excluded from worker assignment entirely - no
economy tick reads its worker presence. Every other work building type
(inn, market, hall, ...) keeps the original flat random assignment,
since there's no demand model for those roles.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
SettlementPlan gains combine_milling_and_baking: true whenever
resident_count is at or below the new SMALL_SETTLEMENT_POPULATION_THRESHOLD
(30). watabou_import now shares one "communal" building for milling and
baking whenever a settlement is small, even if there'd be enough
building slots for two dedicated ones - two half-empty production
buildings isn't a better outcome than one shared one for a small
village. Falls back to sharing regardless of population whenever there
genuinely isn't room for two dedicated buildings, same as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxvVvCwZw9HrxFcspgbmSK
Grain and flour figures stay in kg indefinitely otherwise, which gets
unreadable once a granary/mill is dealing in thousands of kg. Bread
stays loaf-counted regardless of size, and signed deltas stay in plain
kg - only unsigned wheat/flour quantities/rates over 1000kg switch.
Storage and interval-report sections listed one column/row per
building, which gets unreadably wide once a world has more than a
couple of villages - both now aggregate per population centre instead.

Also fix the verdict's "went unfed" check: it was based on peak hunger
value observed, which can include hunger a character already had
entering the run (e.g. from the real Celery beat schedule ticking this
same economy in the background) rather than anything caused during the
simulated window. Now based on whether any character actually missed a
meal on a simulated day.
Village capacity sizing: link-scaled productivity, plan-driven capabilities, demand-aware worker assignment
…d hover affordance

Fades the selected-building/character outline in via a paint-opacity
transition instead of snapping instantly, swaps the hardcoded #ffb703
for the semantic accent color, and adds a dimmer hover-only preview
ring on buildings/characters. Also fixes the hover rings rendering at
full opacity instead of their intended dimmer resting state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a `compact` prop to the shared List component (tighter item
padding/gap) and applies it to BuildingDetail's residents/workers
lists, which are nested inside an already-dense detail panel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011H8vdMQa7b5tHiz32seC6v
…l character tooltips

- A character standing inside a building was triggering both the
  building's and the character's hover outline at once, since MapLibre
  fires per-layer mousemove handlers independently for overlapping
  features. The buildings-fill hover handler now checks whether the
  same point also hits the "characters" layer and defers to it,
  mirroring the existing click-priority pattern.
- Character map tooltips now show a single status line - "[Activity]
  at [Building]", "[Activity] outside", "Walking to [Building]", or
  "Walking outside" - instead of separate Currently/Lives at/Works at
  lines.
- CharacterPointFeatureSerializer exposes current_location_type and
  destination_location_type, resolving through Node.interior_space as
  well as Node.building so characters in interior rooms (not just at a
  building's entrance node) resolve to that building instead of
  reading as "outside".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011H8vdMQa7b5tHiz32seC6v
Show "Currently" above Age/Sex in CharacterDetail, and reduce the
non-modal DetailCard variant's max-width from 280px to 200px.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011H8vdMQa7b5tHiz32seC6v
Also read "home" instead of "house" when the character is at or
walking to their own residence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011H8vdMQa7b5tHiz32seC6v
Cursor is inherited, so without an explicit default, hovering any
plain text (tooltip copy, list items, etc.) showed the browser's
text-selection I-beam. Sets cursor: default on body and restores
cursor: text only on actual text inputs/textareas/contenteditable
elements.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011H8vdMQa7b5tHiz32seC6v
…ails button

Header now has a target-icon button (secondary variant, same size as
Close) that recenters the map on whichever entity is selected. Also
fixes Button's icon prop rendering unsized (no CSS constrained its
SVG), and adds a small Button size used for the tooltip's View
details action.
…e 768px

Most body text read as too large on wider viewports; drop the md/lg
font-size escalation from the shared type scale so text stays the
same size at every breakpoint instead of growing past 768px.
Extracts MapDetailCard from DetailCard so the docked panel portals into
Map's own wrapper (via a new DetailSurface `container` prop) and positions
relative to the map instead of the viewport, fixing it sitting far too high
on the page. It stays docked top-right through a wider range of narrow
viewports before falling back to the mobile bottom sheet, and sits closer to
the zoom controls. DetailCard itself is now always a centered modal.

Also gives the character/building lists in the detail card a stronger hover
effect (List's existing background/border hover rule was nearly invisible).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011H8vdMQa7b5tHiz32seC6v
…-polish

Feat/map selection highlight polish
…'s hours

generate_day hardcoded a fixed 8:00-17:00 work window regardless of the
character's actual work building, while movement (target_role_for) already
read the building's real open_time/close_time. For a late-closing building
like the inn (06:00-23:00), this meant a worker was still physically at
the inn well into the evening but their scheduled activity had already
fallen through to the fixed leisure block, showing "Relaxing" instead of a
work activity. Extracted the building-hours lookup into a shared
work_hours_for helper and use it to size the work blocks (and push
dinner/leisure/wind-down after work actually ends) in generate_day too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…vity

Fix character activity schedule to respect the assigned work building…
@gaidheal1 gaidheal1 self-assigned this Aug 9, 2026
@gaidheal1
gaidheal1 merged commit 9f26258 into main Aug 9, 2026
11 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.

2 participants