Skip to content

feat: repurpose soft-deleted dweller assets for radio recruitment - #317

Merged
ElderEvil merged 5 commits into
masterfrom
2.14-repurpose
May 25, 2026
Merged

feat: repurpose soft-deleted dweller assets for radio recruitment#317
ElderEvil merged 5 commits into
masterfrom
2.14-repurpose

Conversation

@ElderEvil

@ElderEvil ElderEvil commented May 25, 2026

Copy link
Copy Markdown
Owner
  • Radio recruit_dweller() now prefers restoring a soft-deleted dweller (reusing existing S3 image/thumbnail/bio/rarity) over creating a blank new one. Falls back to create_random when pool is empty or an override is supplied.
  • Three new RadioConfig fields: recycle_enabled, recycle_probability, recycle_min_age_days (all env-overridable via RADIO_ prefix).
  • RecruitmentResponse gains a recycled bool field; endpoint and frontend store propagate it; frontend shows a distinct toast for recycled recruits.
  • 29/29 radio service tests passing (11 new, existing tests fixed for updated tuple return types).
  • conftest: swap SQLAlchemy AsyncSession for SQLModel AsyncSession so .exec() is available in all service tests.
  • psycopg2 -> psycopg2-binary (no pg_config required in dev/CI).
  • docker-compose.infra.yml: fix postgres volume mount for PG18 (/var/lib/postgresql instead of /var/lib/postgresql/data).
  • ROADMAP: retire v2.13.2, promote items into v2.14.

Summary by CodeRabbit

  • New Features
    • Radio recruitment can now recycle soft-deleted dwellers based on configurable probability and eligibility settings.
    • The UI now displays whether a recruit was newly created or recycled from a deleted dweller.

Review Change Stack

- Radio recruit_dweller() now prefers restoring a soft-deleted dweller
  (reusing existing S3 image/thumbnail/bio/rarity) over creating a blank
  new one. Falls back to create_random when pool is empty or an override
  is supplied.
- Three new RadioConfig fields: recycle_enabled, recycle_probability,
  recycle_min_age_days (all env-overridable via RADIO_ prefix).
- RecruitmentResponse gains a recycled bool field; endpoint and frontend
  store propagate it; frontend shows a distinct toast for recycled recruits.
- 29/29 radio service tests passing (11 new, existing tests fixed for
  updated tuple return types).
- conftest: swap SQLAlchemy AsyncSession for SQLModel AsyncSession so
  .exec() is available in all service tests.
- psycopg2 -> psycopg2-binary (no pg_config required in dev/CI).
- docker-compose.infra.yml: fix postgres volume mount for PG18
  (/var/lib/postgresql instead of /var/lib/postgresql/data).
- ROADMAP: retire v2.13.2, promote items into v2.14.
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@ElderEvil, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 29 minutes and 47 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8c7df074-c5d2-484c-8ad7-0a9ac5dcb82d

📥 Commits

Reviewing files that changed from the base of the PR and between 3b0df47 and 0d41615.

⛔ Files ignored due to path filters (1)
  • backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • CHANGELOG.md
  • ROADMAP.md
  • backend/app/services/radio_service.py
  • backend/app/tests/test_api/test_radio.py
  • backend/app/tests/test_services/test_notification_integrations.py
  • backend/pyproject.toml
  • frontend/package.json
📝 Walkthrough

Walkthrough

This PR implements radio dweller recycling—a feature allowing recruitment to restore soft-deleted dwellers based on configuration and probability checks—alongside Docker/dependency infrastructure updates and roadmap documentation refreshes.

Changes

Radio Dweller Recycling Feature

Layer / File(s) Summary
Recycling configuration and response schema
backend/app/core/game_config.py, backend/app/schemas/radio.py, frontend/src/modules/radio/models/radio.ts
RadioConfig gains three new settings to control recycling (enabled flag, probability, minimum age in days), and RecruitmentResponse gains a recycled boolean field to track restored dwellers.
Core service implementation
backend/app/services/radio_service.py
recruit_dweller and manual_recruit now return (dweller, recycled) tuples; they conditionally restore soft-deleted candidates when override is None, recycling is enabled, and probability checks pass; fallback to fresh creation on conflict; notifications include recycled flag.
API endpoint integration
backend/app/api/v1/endpoints/radio.py
manual_recruit_dweller endpoint unpacks the (dweller, recycled) tuple from the service and includes the recycled flag in RecruitmentResponse construction.
Frontend store and UX
frontend/src/modules/radio/stores/radio.ts
manualRecruit action now conditionally displays a "familiar face" toast message when result.recycled is true, otherwise shows the standard recruitment message.
Test infrastructure and coverage
backend/app/tests/conftest.py, backend/app/tests/test_services/test_radio_service.py
Test session factory imports AsyncSession from sqlmodel.ext.asyncio.session; new deleted_dweller fixture creates back-dated soft-deleted candidates with RARE rarity; comprehensive test suite covers recycling success/failure paths including rarity/asset preservation, stat resets, probability/config bypass, conflict fallback, and manual recruitment propagation.

Infrastructure and Dependency Updates

Layer / File(s) Summary
Build and Docker configuration
backend/pyproject.toml, docker-compose.infra.yml, backend/app/api/tasks.py
Dependency switched from psycopg to psycopg2-binary; docker-compose db volume mount changed from /var/lib/postgresql/data to /var/lib/postgresql; healthcheck and dramatiq-worker command formatting reformatted; module import order adjusted to place UUID4 import before broker configuration import.

Documentation Updates

Layer / File(s) Summary
Roadmap timestamp and formatting
ROADMAP.md
Updated "Last updated" timestamp to 2026-05-25; refreshed Version Milestones table alignment; adjusted easter-egg code snippet formatting (Konami Code TypeScript and Quantum Mouse Vue templates); normalized whitespace throughout version history sections.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

A rabbit bounces through deleted dwellers so fine,
Recycling their essence with probability divine,
Soft-deleted treasures restored to serve anew,
Each RARE one remembered, their stats fresh as dew. 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main feature: repurposing soft-deleted dweller assets for radio recruitment, which aligns with the primary objective and code changes throughout the PR.
Docstring Coverage ✅ Passed Docstring coverage is 93.10% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2.14-repurpose

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/services/radio_service.py (1)

243-307: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix manual_recruit return contract mismatch.

manual_recruit now returns (dweller, recycled), but its signature/docstring still declare a single Dweller. This can break type checking and mislead callers.

Proposed fix
     async def manual_recruit(
         db_session: AsyncSession,
         vault_id: UUID4,
         caps_cost: int | None = None,
         override: DwellerCreateCommonOverride | None = None,
-    ) -> Dweller:
+    ) -> tuple[Dweller, bool]:
         """
         Manually recruit a dweller for caps.
@@
         Returns:
-            Newly recruited dweller
+            Tuple of (dweller, recycled) where recycled=True means the recruit
+            was restored from a soft-deleted dweller.

As per coding guidelines, backend/app/**/*.py: "Add type hints for new Python functions; prefer UUID4/Pydantic types where applicable".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/radio_service.py` around lines 243 - 307, The
manual_recruit function currently returns (dweller, recycled) but its signature
and docstring claim it returns a single Dweller; update the function signature
to reflect the actual return type (e.g., -> tuple[Dweller, bool] or
Tuple[Dweller, bool] using typing) and adjust the docstring Returns section to
describe both values (newly recruited Dweller and recycled flag). Locate
manual_recruit in RadioService (and references to RadioService.recruit_dweller)
and change any type annotations or imports as needed to satisfy static typing
(add Tuple import if using typing.Tuple or use built-in tuple[...]); ensure
callers expecting a single Dweller are reviewed/updated or note that the
function now returns a (dweller, recycled) pair.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/api/tasks.py`:
- Line 10: Shorten the inline comment on the import of app.core.dramatiq so it
stays under 120 characters and uses double quotes; update the comment attached
to the import statement "import app.core.dramatiq" (the comment explaining
broker configuration/set_broker) to a more concise phrase or split it across two
lines immediately above the import (e.g., a brief one-line comment like "Ensure
dramatiq broker is configured (calls set_broker)" or a two-line doc comment) so
the line length and quoting conventions are respected.

In `@backend/pyproject.toml`:
- Line 33: The pyproject lists both synchronous PostgreSQL drivers
("psycopg>=3.3.4,<4" and "psycopg2-binary>=2.9.12") but the app and migrations
use asyncpg (see backend/app/core/config.py and backend/app/alembic/env.py), so
remove the unused sync driver entries from backend/pyproject.toml; search for
any imports/usages of "psycopg" or "psycopg2" to confirm no runtime/tooling
depends on them, delete the redundant dependency lines, and if you need to keep
one for a documented tooling reason, add a brief comment explaining why it
remains.

In `@ROADMAP.md`:
- Around line 522-531: The snippet uses double quotes and semicolons which
violate frontend TypeScript style; update the import string, the ref
initialiser, and any line-terminating semicolons to use single quotes and no
semicolons: change import { useMagicKeys } from "`@vueuse/core`"; to use single
quotes and remove the trailing semicolon, change const sequence = ref(""); to
const sequence = ref('') without a semicolon, and remove the trailing semicolons
from the useMagicKeys destructure line and the watch(...) closing line so that
useMagicKeys, ArrowUp/ArrowDown/ArrowLeft/ArrowRight/b/a, sequence, and watch
follow the project's single-quote/no-semicolon conventions.

---

Outside diff comments:
In `@backend/app/services/radio_service.py`:
- Around line 243-307: The manual_recruit function currently returns (dweller,
recycled) but its signature and docstring claim it returns a single Dweller;
update the function signature to reflect the actual return type (e.g., ->
tuple[Dweller, bool] or Tuple[Dweller, bool] using typing) and adjust the
docstring Returns section to describe both values (newly recruited Dweller and
recycled flag). Locate manual_recruit in RadioService (and references to
RadioService.recruit_dweller) and change any type annotations or imports as
needed to satisfy static typing (add Tuple import if using typing.Tuple or use
built-in tuple[...]); ensure callers expecting a single Dweller are
reviewed/updated or note that the function now returns a (dweller, recycled)
pair.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 195cd221-1a55-40f2-96aa-01a10429635e

📥 Commits

Reviewing files that changed from the base of the PR and between 245949d and 3b0df47.

⛔ Files ignored due to path filters (1)
  • backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • ROADMAP.md
  • backend/app/api/tasks.py
  • backend/app/api/v1/endpoints/radio.py
  • backend/app/core/game_config.py
  • backend/app/schemas/radio.py
  • backend/app/services/radio_service.py
  • backend/app/tests/conftest.py
  • backend/app/tests/test_services/test_radio_service.py
  • backend/pyproject.toml
  • docker-compose.infra.yml
  • frontend/src/modules/radio/models/radio.ts
  • frontend/src/modules/radio/stores/radio.ts

Comment thread backend/app/api/tasks.py
import app.core.dramatiq # noqa: F401 - ensures broker is configured (set_broker) when dramatiq CLI imports this module
from pydantic import UUID4

import app.core.dramatiq # noqa: F401 - ensures broker is configured (set_broker) when dramatiq CLI imports this module

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Shorten the inline comment to stay within 120 characters.

Line 10 appears to exceed the configured Python line-length limit. Split the comment across lines or shorten it.

✂️ Suggested edit
-import app.core.dramatiq  # noqa: F401 - ensures broker is configured (set_broker) when dramatiq CLI imports this module
+import app.core.dramatiq  # noqa: F401
+# Ensures broker is configured (set_broker) when dramatiq CLI imports this module.

As per coding guidelines, **/*.py: Use 120 character line length and double quotes in Python.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import app.core.dramatiq # noqa: F401 - ensures broker is configured (set_broker) when dramatiq CLI imports this module
import app.core.dramatiq # noqa: F401
# Ensures broker is configured (set_broker) when dramatiq CLI imports this module.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/tasks.py` at line 10, Shorten the inline comment on the
import of app.core.dramatiq so it stays under 120 characters and uses double
quotes; update the comment attached to the import statement "import
app.core.dramatiq" (the comment explaining broker configuration/set_broker) to a
more concise phrase or split it across two lines immediately above the import
(e.g., a brief one-line comment like "Ensure dramatiq broker is configured
(calls set_broker)" or a two-line doc comment) so the line length and quoting
conventions are respected.

Comment thread backend/pyproject.toml Outdated
Comment thread ROADMAP.md Outdated
ElderEvil added 4 commits May 26, 2026 00:21
… sites

test_api/test_radio.py and test_notification_integrations.py were calling
recruit_dweller() / manual_recruit() and assigning the result directly to
a Dweller variable, broken by the tuple return introduced in the repurpose
feature. Updated both to unpack (dweller, _).
- radio_service.py: fix manual_recruit return type annotation -> tuple[Dweller, bool]
  and update docstring Returns section to describe both values.
- pyproject.toml: remove unused sync PG drivers (psycopg, psycopg2-binary);
  app and Alembic use asyncpg exclusively (confirmed by grep + env.py).
- ROADMAP.md: fix TS code snippet to use single quotes and no semicolons,
  matching the project's frontend style conventions.
- tasks.py comment: skipped (line is 120 chars, within limit; ruff passes).
- backend/pyproject.toml: 2.13.1 -> 2.14.0
- frontend/package.json: 2.13.1 -> 2.14.0
- backend/uv.lock: regenerated for v2.14.0
- CHANGELOG.md: added [2.14.0] release entry
@ElderEvil
ElderEvil merged commit 484ff61 into master May 25, 2026
4 checks passed
@ElderEvil
ElderEvil deleted the 2.14-repurpose branch June 3, 2026 19:47
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.

1 participant