Conversation
…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
development → staging
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
feat: add offline activity logging modal
development → staging
…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
development → staging
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>
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
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.
Release summary
Features
Fixes and UX improvements
Developer experience and quality
import_villagenow auto-picks an unused layout slot instead of requiring one to be specified manuallyCharacter.can_linkis 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)Technical notes
handle_online_login,set_activity_active_modifiers,schedule_online_end), routed through a sharedactivate_link_modifierhelper; stopping an activity now uses a 5-minute grace window (schedule_modifier_end) before ending itsactivity_activemodifier, and restarting within that window refreshes the existing modifier row instead of recreating it.