Skip to content

development → staging - #776

Merged
gaidheal1 merged 29 commits into
stagingfrom
development
Aug 13, 2026
Merged

development → staging#776
gaidheal1 merged 29 commits into
stagingfrom
development

Conversation

@gaidheal1

Copy link
Copy Markdown
Member

User-visible improvements (UVIs)

Features

  • 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

  • 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
  • Standardized timer duration formatting across the app
  • 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

Developer experience and quality


Technical notes

No new migrations or env vars required for this batch.

Test plan

  • CI passes
  • Smoke test on staging after deploy

Generated by Claude Code

gaidheal1 and others added 29 commits August 8, 2026 15:54
storybook build has no --base option (that's Vite-only), so the
build-storybook step has been failing on every push to main since the
mkdocs conversion, leaving the deployed site stuck on a stale
pre-mkdocs Storybook-only build. The base path is already set via
viteFinal in .storybook/main.ts, so the flag was redundant anyway.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSwWkchRGye37hoCmEfDCz
Fix GitHub Pages docs deploy: remove invalid storybook --base flag
react-docgen's resolveToValue recurses infinitely on a let variable that
is conditionally assigned and then reassigned multiple times, causing
"Maximum call stack size exceeded" during `npm run build-storybook`.
Compute API_BASE_URL once via a helper instead of reassigning it.

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

Fix Storybook build: react-docgen stack overflow in config.ts
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
gaidheal1 marked this pull request as ready for review August 13, 2026 23:21
@gaidheal1
gaidheal1 merged commit 899c245 into staging Aug 13, 2026
8 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