Skip to content

Deploy: staging → main - #781

Merged
gaidheal1 merged 48 commits into
mainfrom
staging
Aug 14, 2026
Merged

Deploy: staging → main#781
gaidheal1 merged 48 commits into
mainfrom
staging

Conversation

@gaidheal1

@gaidheal1 gaidheal1 commented Aug 14, 2026

Copy link
Copy Markdown
Member

Release summary

Features

  • Village maps now show communal squares as their own distinct space on the map
  • Log time spent on a task retroactively via a new offline activity logging modal
  • Due dates now display in weeks/months when far out, not just days
  • Activity timer now stays in sync across a player's other open sessions/tabs

Fixes and UX improvements

  • Map now loads and pans more smoothly, with less stutter while dragging or zooming
  • The map centres faster after logging in, with less flicker on first load
  • Autosave the name field in the item edit modal instead of requiring manual save
  • Parent-task field stays editable, and subtasks now render as indented rows underneath it; the task edit modal now also shows a removable parent-task chip, with the due date split into separate date and time fields
  • Task timestamps (created/modified/completed) moved into a small tooltip on the edit view instead of cluttering the task summary
  • "Add a subtask" now opens the same familiar task editor used everywhere else, and only creates the subtask once you've actually given it a name
  • Standardized timer duration formatting across the app (thanks @Harshit15-coder)
  • Simplified resident line display (dropped redundant activity status) and standardized form control sizing/radius
  • Corrected save-status indicator position and added a saving-delay so it aligns with modal actions
  • Player presence is linked to character activity again — being online and active applies XP bonuses in real time, and pausing an activity briefly keeps its bonus active for a few minutes instead of dropping it instantly

Developer experience and quality

  • import_village now auto-picks an unused layout slot instead of requiring one to be specified manually
  • Character.can_link is now derived instead of stored, removing a source of stale data (Derive Character.can_link from underlying reasons instead of a single stored flag #682)
  • Added Storybook coverage for a broad set of shared UI components (lists, achievements, mode switcher, banners, search input, tutorial modal, toast manager, and layout chrome: navbar, nav drawer, footer, infobar), plus supporting decorator/mock infrastructure for auth and query-client context
  • Refactored activity-timer internals (import/dependency cleanup) and bumped mkdocs/mkdocs-material

Technical notes

  • Map rendering now only rebuilds the changed GeoJSON source instead of the whole map source per frame. Initial-centre and world-bounds map data are now prefetched right after login.
  • Re-enables previously-disabled activity XP modifier signals (handle_online_login, set_activity_active_modifiers, schedule_online_end), routed through a shared activate_link_modifier helper; stopping an activity now uses a 5-minute grace window (schedule_modifier_end) before ending its activity_active modifier, and restarting within that window refreshes the existing modifier row instead of recreating it.
  • No new migrations or env var changes in this batch.

gaidheal1 and others added 30 commits August 10, 2026 00:40
…entre

Map.tsx's walker animation loop rebuilt the entire GeoJSON source
(buildings/roads/fields/boundaries) every 16ms even though only
character positions change per frame. Split buildVillageSourceData into
buildStaticVillageFeatures (memoized, rebuilt only when features change)
and buildCharacterPointFeatures (per-frame). The animation loop is now
also gated on there actually being walking characters, instead of
running unconditionally.

Separately, useMap.ts's initial-camera logic assumed a single seeded
village and picked "the first population centre" from the full
population-centres list, which is now stale with multiple villages
imported from locations/data/. Added a lightweight
InitialMapCentreView (/map/initial-centre/) that returns just enough
(id/name/bbox) to frame the camera, preferring the population centre of
the requesting player's actively linked character
(Player.active_link) and falling back to the lowest-pk centre
otherwise. Frontend now fetches this cheaply before the bbox-scoped
viewport poll takes over as the source of truth.

Backend: locations/tests/test_initial_map_centre_view.py (3 tests).
Frontend: MapPage.test.tsx updated for the new fetchInitialMapCentre
mock; also fixed a flaky prefetch effect that used a dynamic import()
for fetchPopulationCentreMap, which could race and skip the vi.mock
when prefetching two different villages concurrently - now a normal
static import.
Watabou's village generator can export a "squares" MultiPolygon (open
outside communal space, e.g. a market square/plaza) that
import_watabou_village previously ignored entirely, like "greens" and
"prisms" still are.

Add a "square" Subzone.usage choice and a _import_squares helper
(watabou_import.py) that imports it the same way _import_fields
already imports "fields" - one LandArea wrapping the union of the
polygons, one Subzone per polygon - refactored the shared logic into
_import_polygon_subzones. Unlike a crops Subzone, a square has no
FieldCrop growth cycle or other economy behaviour attached; it's
purely a map feature.

Wire it into both map views (PopulationCentreMapView,
MapViewportView): they now query Subzone with
usage__in=["crops", "square"] instead of usage="crops" alone, since
SubzoneFeatureSerializer already returns None for every crop_* field
when there's no attached FieldCrop.

Frontend: styledPolygonFeatures gives usage="square" subzones a
distinct warm/paved fill (geojson.tsx) instead of falling through to
the crops-green/building-grey styling, and the tooltip labels them
"Square" instead of the raw bookkeeping name.
When --x/--y are omitted, import_village now picks the first
VILLAGE_LAYOUT slot with no existing PopulationCentre on it, instead
of requiring coordinates to be hand-picked. Passing only one of --x/--y
now raises a clear CommandError instead of silently doing something
unintended.

This lets ad-hoc imports outside the setup_world/import_villages
pipeline (e.g. trying out a village file not in locations/data/) claim
spare grid space without colliding with the pipeline's own slots -
though it's not persistent across a setup_world rerun, which wipes and
reimports only locations/data/'s contents.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6BX7dFJWn2xMYn9qggdos
GameContext now primes useInitialMapCentre's and useMapWorldBounds's
query cache entries as soon as fetch_info resolves, instead of only
firing them once the player navigates to the map page. Both are
cheap, one-shot fetches, so warming them at login lets MapPage skip a
network round-trip on mount for players who go on to open it.

Extracted the {queryKey, queryFn, staleTime, gcTime} for each into
exported query-option objects in useMap.ts, reused by both the hooks
and GameContext's prefetch, so the two can't drift onto different
cache keys.

Deliberately doesn't prefetch /map/viewport/: it needs a bbox only the
mounted map component can produce, and starts a 2s poll once enabled -
prefetching it at login would poll map data in the background for
every session regardless of whether the player ever opens the map.

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

perf: stop rebuilding map source per frame, fix stale initial centre, prefetch at login
feat: import watabou "squares" as communal-space Subzones
feat: auto-pick an unused village_layout slot for import_village
# Conflicts:
#	frontend/src/pages/MapPage/MapPage.tsx
Replaces the truncation-prone `points_today` badge with three easy daily
goals (logged in, completed an activity, 3+ minutes recorded) plus a
one-off AP bonus for clearing all three in a day.

- progression.daily_goals: live goal-state computation and idempotent
  bonus award, gated by a new DailyGoalAward(player, date) row
- Wired into both activity-completion paths (ActivityTimer.complete and
  offline logging), not just the timer flow
- GameSettings.daily_goals_completion_bonus_ap controls the bonus amount
- MeViewSet.daily_goals replaces today_points with the full goal state
- Removed the now-dead PlayerCharacterLink.player_time_today/points_today
- Frontend: DailyGoalsBadge replaces TodayPointsBadge

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014hFSsXRtBgoJX7HTqdmTEb
Replace map-view badge with daily goals + completion bonus
fix: standardize timer duration formatting
can_link was a single BooleanField written independently by signals.py,
spawn_characters/generate_characters, admin actions, and link_services,
so any one writer could clobber another's reason with no way to tell
why a character ended up unlinkable.

Replace it with a read-only property derived from independent reasons:
manual reservation (new Character.is_reserved field, surfaced in admin
as "Reserved"), age (Character.is_underage), and active link
(Character.is_npc's existing check). Leaves a documented extension
point for the population-centre lock from #681.

Since can_link is no longer a DB column, add
CharacterQuerySet.linkable()/CharacterManager as the SQL-level
equivalent for the filter/exists-check call sites (users/utils.py,
character_services.character_has_available, CharacterFilter).

Also:
- Drop the mark_as_canlink admin action in favour of ticking the new
  "Reserved" checkbox directly; add a CanLinkListFilter since a
  property can't sit in ModelAdmin.list_filter directly.
- Remove the now-unnecessary can_link recompute-on-link-change signal
  plumbing in character/signals.py - nothing to keep in sync once it's
  computed live.
- Migration drops the can_link column and adds is_reserved (no
  backfill - there's no way to recover which characters were
  historically reserved, which is the bug this fixes).

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

feat: derive Character.can_link instead of storing it (#682)
Fixes multi-session desync (#759): if a player has more than one
session open (two tabs, phone + laptop), starting, labelling, or
submitting an activity in one had no effect on the others, since
each session's timer state was purely local React state.

Backend:
- Add gameplay.utils.broadcast_activity_timer(timer), reusing the
  existing player_{id} channel group and send_group_message/"action"
  message pattern from gameplay/signals.py and utils.control_timers.
- Call it after every timer mutation: start/pause/reset/complete
  (BaseTimerViewSet), and set_activity/label_activity/complete
  (ActivityTimerViewSet), plus both auto-complete paths in tasks.py
  (disconnect grace period and the stale-connection sweep) so a tab
  left open elsewhere is told the timer already closed out.
- No consumer changes needed: TimerConsumer's existing generic
  "action" handler already relays any {"type": "action", ...} group
  message straight to the socket.

Frontend:
- Extend WebSocketActionMessage with the new "activity_timer_update"
  action and its data payload.
- Wire the previously-stubbed handleGlobalWebSocketEvent dispatcher
  with an onActivityTimerUpdate option.
- WebSocketContext calls activityTimer.loadFromServer on receipt,
  mirroring GameContext's existing bootstrap load (same
  limitSeconds/is_premium resolution). loadFromServer just
  overwrites local state, so a session echoing its own update is a
  harmless no-op.

Tests: broadcast_activity_timer call verified for start/complete/
set_activity/label_activity and both auto-complete Celery tasks.
feat: sync activity timer across a player's open sessions
The name field was the only field in the shared edit modal (used by
Tasks, Skills, Projects, Categories, and Activities panels) that
required an explicit "Save" click, unlike the due-date and
parent-task fields, which already autosave on blur/change.

- Name now autosaves on blur/Enter, matching the due-date pattern.
  The explicit "Save" button is gone since there's nothing left to
  save manually; "Cancel" is now just "Close".
- A panel-local "Saved"/"Couldn't save" indicator (bottom-right of
  the modal) reports the outcome of any autosave in the modal,
  including the Tasks panel's due-date and parent-task fields. Chose
  a lightweight indicator scoped to the modal over the existing
  global toast system, since that toast is a large, centered,
  attention-grabbing element meant for occasional events (e.g.
  level-ups) — too heavy for a frequent, low-stakes autosave
  confirmation.
- onEdit (and the per-panel handleEdit implementations) now accept
  optional onSuccess/onError callbacks, forwarded into each
  mutation's mutate() call so failures surface as a warning instead
  of failing silently.
…rows

- TasksPanel: the "Parent task" select was hidden entirely once a task
  had a parent, with no way to revert it. It now always renders (still
  disabled when the task itself has subtasks, to prevent nesting).
- PlayerItemList: child tasks were rendered nested inside the parent's
  own <li>, structurally and visually boxed inside it. They now render
  as independent rows, siblings of other top-level items, indented via
  a width/margin modifier so the right edge still lines up with the
  parent's.
- gameplay/tasks.py: fix a circular import (gameplay.utils ->
  gameplay.services.xp_modifiers -> gameplay.tasks -> gameplay.utils)
  that crashed web/celery on startup, by deferring the
  broadcast_activity_timer import into the two functions that use it.

Fixes #765.
fix: keep parent-task field editable and render subtasks as indented rows
…with modal actions

- right: sp.$padding-base was a two-value shorthand (invalid for a
  single-value offset), so the browser dropped the declaration and the
  indicator fell back to its static (bottom-left) position, overlapping
  the Close button. Use $spacing-md instead.
- Debounce "Saving…" by 150ms so fast autosaves go straight to "Saved"
  instead of flashing the interim state.
- Move the indicator into the actions row and vertically center it with
  the Close/Delete buttons instead of anchoring to the modal's bottom
  edge; bump contrast (border, stronger shadow, bolder text) so it reads
  clearly without changing its size.

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

# Conflicts:
#	frontend/src/components/TasksPanel/TasksPanel.tsx
Autosave the name field in PlayerItemList's edit modal
Scale due-date formatting smoothly on both sides of now: days (2-6),
then weeks[, leftover days] out to 8 weeks, then months, matching the
pattern already used by "last worked on". Future dates beyond ~6
months still fall back to an absolute date; past dates keep counting
months uncapped.

Closes #760

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extend formatDueAt with weeks/months granularity
BuildingDetail.tsx's residentLine() was simplified in 37f09c6 to drop
the " — idle"/" — walking" activity-status suffix, but the tests
weren't updated to match, leaving BuildingDetail.test.tsx and
Map.test.tsx failing on development.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mur33yGv419VMfAaKxjaGS
Adds a "Log activity" flow (issue #704) for recording work done
outside a running timer: a "+" entry point next to the activities
summary opens a modal to search/enter a task, split hours+minutes
duration, and a completion date/time (capped at today via the native
date picker). Submits to the existing backend endpoint and surfaces
its XP-eligibility messaging — the backend remains authoritative on
whether XP is actually awarded; the client only does obvious
sanity-checks (positive duration, required fields).

Also fixes two things this surfaced along the way:
- Input.tsx dropped the native `required` attribute (kept
  aria-required) — it was silently blocking form submission in favor
  of the browser's own validation UI, overriding the app's styled
  error messages.
- Modal.module.scss's .modalContent had overflow-x: hidden with no
  horizontal padding, clipping the focus ring on any full-width input
  flush against its edge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mur33yGv419VMfAaKxjaGS
gaidheal1 and others added 14 commits August 13, 2026 22:48
…eation

- Display a parent task chip in the task edit modal (with remove/select),
  and split the due-date input into separate date/time fields (#762).
- Move task timestamp display (created/modified/completed) out of the
  inline summary into a clock-icon tooltip on the edit modal's title row
  (#764).
- Rework "Add a subtask": clicking it now opens the same task edit/detail
  modal used everywhere else, seeded with a blank, unsaved draft, instead
  of adding a "Subtask of X" chip before the task input (#774). The draft
  is only actually created once its name has been genuinely edited (via
  usePlayerItemModal's existing no-op-edit guard); closing the modal
  without editing the name discards the draft with no API call.
- Add PlayerItemList `hiddenItemIds` (keep an item deep-linkable without
  rendering it as a row) and `onModalClose` (notify the caller which item's
  modal just closed) to support the draft-subtask flow.
Parent task chip, due-date split, timestamp tooltip, subtask creation
…ivity

Re-enables handle_online_login, set_activity_active_modifiers, and
schedule_online_end (disabled in 870afd2 pending premium-XP isolation),
routing all modifier creation through the shared activate_link_modifier
helper (extended with a scope param so it can target PLAYER as well as
CHARACTER modifiers).

Stopping an activity no longer ends its activity_active modifier
immediately - ends_at is pushed out by a 5-minute grace window via
schedule_modifier_end, and starting another activity within that window
refreshes the same modifier row instead of dropping and reactivating it.

Adds progression.ap.get_productivity (baseline constant x live active
XpModifier multiplier) and Character.get_productivity as the authored,
live-read productivity signal called for in the issue - deliberately not
derived from lifetime AP total.

Closes #750

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8Siy6MEuhLq9kCa3ujBwf
Adds stories for the pure, prop-driven component gaps identified in #749:
PlayerItemList, List/Li, Achievements, ModeSwitcher, EntitySearchInput,
TutorialModal, WaitlistForm, StaticBanner, BackToTopButton, ToastManager.

Also adds the minimal decorator infra these turned out to need:
- withQueryClient (global): a fresh, retry-off QueryClient per story, for
  components that call TanStack Query hooks directly or transitively
  (EntitySearchInput, TutorialModal both do, despite being listed as
  context-free in the issue's audit).
- withGameContext + testUtils/mockGameContext: a minimal GameContext mock
  for EntitySearchInput's indirect useGame() dependency (via useFeatureFlag).

ActivityInput was left out of this batch - it now pulls in useGame,
useSupportFlow, useFeatureFlag and timer state, which is bucket-2/3-level
complexity, not the "prop-driven, no context" component the issue described.
Stories Navbar, NavDrawer, Footer, and Infobar - the layout chrome
called out as bucket 3 in #749, step 4 of its suggested approach.

- Split AuthContext's raw context into context/authContext.ts,
  mirroring the existing GameContext.tsx/gameContext.ts split, so it
  can be mounted directly in stories without pulling in AuthProvider's
  real bootstrap/fetch logic.
- Add testUtils/mockAuthContext.ts (mockAuthContextValue({authenticated,
  ...overrides})) and .storybook/decorators/withAuthContext.tsx,
  following bucket-1's conventions.
- Navbar.stories.tsx: LoggedOut, LoggedIn, WithAnnouncements (play test
  opens the popover), WithMapEnabled - wrapped in MemoryRouter + a
  seeded QueryClient (appConfig, announcements) + mock
  AuthContext/GameContext.
- NavDrawer.stories.tsx: LoggedIn (play test clicks close, asserts
  onClose fires), LoggedOut, WithMapEnabled, Closed.
- Footer.stories.tsx: LoggedOut (play test asserts no Admin Panel
  link), LoggedIn, StaffUser (play test asserts the link appears).
- Infobar.stories.tsx: Default (play test asserts name/level),
  PremiumPlayer, Loading, NoPlayer (renders null).

Verified via eslint, tsc --noEmit, and storybook build (all clean),
plus vitest on AuthContext.test.tsx and the Navbar/NavDrawer unit
tests (24 passing) to confirm the context split didn't break anything.
test:storybook (Playwright-driven play functions/a11y) still can't run
in this sandbox - no sudo for Chromium's system deps - run
locally/CI before merging.

Remaining gap per #749: bucket 2 (data-fetching panels - Categories/
Projects/Skills/Tasks/ActivitiesPanel, ComingSoonPanel, NotesPanel,
DetailSurface, SupportFlow screens, UnifiedTimerHome, ActivityTimeline,
CurrentActivity, CharacterCurrentActivity) and the deferred
ActivityInput. Per the issue's own plan, step 5 is to revisit whether
those need separate stories given PlayerItemList's existing coverage.

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

feat: Storybook coverage — bucket 1 + layout chrome (#749)
Re-enable player-presence XP modifiers and add live character productivity
The hour/minute duration inputs shared a .row class with the
completed-date/time row. The small-screen media query flipped all
.row elements to flex-direction: column, stacking the duration
fields instead of keeping them side by side. Scope the column
layout to the date/time row only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@gaidheal1
gaidheal1 deployed to staging - celery-staging August 14, 2026 10:43 — with Render Active
@gaidheal1
gaidheal1 deployed to staging - celery-beat-staging August 14, 2026 10:43 — with Render Active
gaidheal1 and others added 3 commits August 14, 2026 11:45
Publishing an Announcement now pushes a WebSocket "announcement_published"
event to online players, which invalidates the announcements/unread-count
queries so the Navbar badge appears immediately instead of only on refresh
or remount.

- Announcement.save() broadcasts on the unpublished->published transition
- publish_selected_announcements admin action saves rows individually
  (not queryset.update()) so the broadcast actually fires
- frontend wires the new action into handleGlobalWebSocketEvent /
  WebSocketContext to invalidate the relevant react-query keys

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

feat: broadcast announcement badge updates in real time
@gaidheal1
gaidheal1 deployed to staging - celery-beat-staging August 14, 2026 11:13 — with Render Active
@gaidheal1
gaidheal1 deployed to staging - celery-staging August 14, 2026 11:13 — with Render Active
@gaidheal1
gaidheal1 merged commit f5a5139 into main Aug 14, 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.

3 participants