Skip to content

[feature] Persistent Mass Upgrades - schema, retry backoff, batch state and Beat scanner#436

Open
Eeshu-Yadav wants to merge 34 commits into
gsoc26-persistent-scheduled-upgradesfrom
issues/417-persistence-schema-fields
Open

[feature] Persistent Mass Upgrades - schema, retry backoff, batch state and Beat scanner#436
Eeshu-Yadav wants to merge 34 commits into
gsoc26-persistent-scheduled-upgradesfrom
issues/417-persistence-schema-fields

Conversation

@Eeshu-Yadav

@Eeshu-Yadav Eeshu-Yadav commented May 22, 2026

Copy link
Copy Markdown
Contributor

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #417, closes #422, closes #423, closes #424.

Related to #379 (parent — Persistent Mass Upgrades, Phase 1).

Description of Changes

Today, when a mass upgrade hits an offline device the operation goes from in-progress to failed once Celery's auto-retries run out (about 10 minutes). The admin then has to chase down each failed device by hand. Fine for one device, painful at thirty, unmanageable at three hundred.

This PR makes the upgrader stop giving up. Offline devices transition to a new pending status with a backoff-scheduled retry time, a periodic Beat task picks them up later and re-dispatches the upgrade, and the batch stays active until every device is either upgraded or cancelled. Four sub-issues land together because the new state only makes sense when its schema (#417), its producer (#422), the code that has to recognise it (#423), and the Beat scanner that actually retries it (#424) all ship in one piece — splitting them either leaves the enum dead or quietly breaks the existing batch failure path.

What this PR makes possible:

  • A mass upgrade no longer abandons offline devices. They sit in pending with a scheduled next-retry time, and a Beat task runs every ten minutes (configurable) picking up due retries and re-dispatching the upgrade. Each retry follows an exponential backoff (10m → 20m → 40m → ... capped at 12h, ±25% jitter) until the device comes back or the admin cancels.
  • An admin can cancel a pending operation the same way they can cancel an in-progress one, from the admin or the REST API.
  • A second mass upgrade targeting the same device will not double-flash it; the concurrent-upgrade guard now treats pending as a blocking state.
  • A device deactivated mid-pending short-circuits its retry: the operation transitions to failed rather than attempting to flash a device that's been decommissioned.
  • The admin batch page correctly shows "X complete, Y pending" on first paint and on every live WebSocket update, with both renders driven by the same count so the page does not flicker between formats when the socket connects.
  • Once the operation has moved away from idle, the is_persistent flag is locked in for the lifetime of the run — an admin can no longer flip the policy halfway through a batch.
  • The device-firmware REST endpoint accepts an is_persistent flag, so single-device upgrades can opt into persistence too (off by default for standalone upgrades).
  • Admins are notified about batches that still have pending devices after a configurable period.
  • With openwisp-monitoring installed, a device coming back online retries its pending operations immediately, without waiting for the next Beat scan.

Screenshots :
image
Screenshot from 2026-06-03 15-25-11
image
Screenshot from 2026-06-03 16-52-52
Screenshot from 2026-06-03 15-28-15
image
Screenshot from 2026-06-03 15-30-11
image

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 12460c3b-c7df-4dd7-8287-737697ef083d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements persistent offline device retries for firmware upgrades. When a batch upgrade targets an offline device, instead of failing after ~10 minutes, operations marked as persistent transition to a pending state with exponential-backoff retry scheduling. A Celery Beat task scans for due pending operations every 10 minutes, and a health-status signal from openwisp-monitoring dispatches retries when devices come online. Admins enable/disable persistence at batch creation time and monitor pending counts and retry state through the admin UI, REST API, and real-time WebSocket updates. The implementation spans model persistence fields and pending status, batch aggregation logic, task orchestration, admin/API extensions, WebSocket consumer updates, frontend status constants and styling, comprehensive test coverage across models/tasks/handlers/notifications/WebSocket, and user documentation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • #417 — Establishes the persistence model fields, pending status, and batch-to-child propagation that this PR fully implements
  • #422 — Defines the exponential backoff retry scheduling and persistent failure-handler branch that this PR completes with signal wiring and task dispatch
  • #423 — Outlines the batch status aggregation, cancellation, and concurrency-guard updates that this PR realizes across model queries, WebSocket consumers, and serializers
  • #424 — Specifies the check_pending_upgrades Beat task and retry_pending_upgrade worker that this PR implements with idempotent concurrency handling
  • #427 — Requests admin UI enhancements to surface is_persistent and retry-state fields, which this PR delivers through form updates, admin column/filter additions, and serializer field exposure
  • #430 — Requests supplementary documentation for the persistent-upgrade feature, which this PR includes via user docs for pending status, settings, and persistent-mass-upgrades guide

Possibly related PRs

  • openwisp/openwisp-firmware-upgrader#325 — This PR extends the batch progress WebSocket publisher and consumers introduced in #325 to emit and handle a new pending count field alongside completed/total.
  • openwisp/openwisp-firmware-upgrader#377 — Both PRs extend the WebSocket/UI progress plumbing (e.g., BatchUpgradeProgressPublisher.publish_batch_status) to surface pending counts and status alongside existing completion tracking.
  • openwisp/openwisp-firmware-upgrader#435 — Both PRs update openwisp_firmware_upgrader/admin.py deletion permission logic to treat "in-progress" operations as a membership check against IN_PROGRESS_STATUS rather than single-value equality.

Suggested reviewers

  • pandafy
  • asmodehn
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately summarizes the PR's main scope: persistent mass upgrades with schema changes, retry backoff scheduling, batch state updates, and Beat scanner implementation.
Linked Issues check ✅ Passed All four linked issues (#417–424) are satisfied: schema fields and propagation (417), recoverable failure handler with backoff (422), batch aggregation and guards including pending support (423), and Beat scanner with retry worker (424) are all implemented and tested.
Out of Scope Changes check ✅ Passed All changes are scoped to persistent mass-upgrade implementation: model fields, failure handling, batch state, WebSocket/API exposure, admin UIs, migrations, and comprehensive tests. No unrelated refactoring or tangential features.
Bug Fixes ✅ Passed This PR is a feature implementation (persistent mass upgrades), not a bug fix. All linked issues are tagged [feature:gsoc26]; the title and description describe new capabilities. The custom check a...
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering all major aspects of the changes including checklist completion, issue references, detailed change description, visual evidence, and impact explanation.

✏️ 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 issues/417-persistence-schema-fields

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.

@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from ace6c8c to 5119b30 Compare May 23, 2026 10:57
@Eeshu-Yadav Eeshu-Yadav marked this pull request as ready for review May 23, 2026 11:21
Copilot AI review requested due to automatic review settings May 23, 2026 11:21
@Eeshu-Yadav

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kilo-code-bot

kilo-code-bot Bot commented May 23, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 2 Issues Found | Recommendation: Merge with follow-up

The persistent mass-upgrade pipeline is solid: atomic status claims (retry_pending_upgrade mutex prevents double-flash), correct pending handling in batch status aggregation (no premature success/failed), tenant-scoped cancel/delete via ProtectedAPIMixin, immutable is_persistent enforced on creation paths, and broad test coverage (handlers, tasks, notifications, selenium). The websockets/JS layer escapes all server-derived data and guards against missing pending/NaN. No new critical bugs or security issues found in the changed code.

The only outstanding items are two deploy-time migration index locks, both already noted inline. Consider a maintenance window or a RunSQL CREATE INDEX CONCURRENTLY for these.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 45 next_retry_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated upgradeoperation table during deploy (already commented inline).
openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py 18 last_reminder_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated batchupgradeoperation table during deploy (already commented inline). Consider a maintenance window or a concurrent index via RunSQL.
Files Reviewed
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py - 1 issue
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py - 1 issue
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js
  • docs/user/persistent-mass-upgrades.rst
  • docs/user/rest-api.rst
  • docs/user/settings.rst
  • docs/user/upgrade-status.rst
  • docs/user/websocket-api.rst

Fix these issues in Kilo Cloud

Previous Review Summaries (5 snapshots, latest commit b6f5543)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit b6f5543)

Status: 2 Issues Found | Recommendation: Merge with follow-up

The persistent mass-upgrade pipeline is solid: atomic status claims, conditional re-checks before dispatch, an immutable is_persistent flag, tenant-scoped cancel/delete guards, and broad test coverage (handlers, tasks, notifications, selenium). The only outstanding items are two deploy-time migration index locks, both already noted inline. The previously reported REST API documentation gap is now resolved — is_persistent/pending are documented in rest-api.rst.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 45 next_retry_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated upgradeoperation table during deploy (already commented inline).
openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py 18 last_reminder_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated batchupgradeoperation table during deploy (already commented inline). Consider a maintenance window or a concurrent index via RunSQL.
Files Reviewed
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py - 1 issue
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py - 1 issue
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js
  • docs/user/persistent-mass-upgrades.rst
  • docs/user/rest-api.rst
  • docs/user/settings.rst
  • docs/user/upgrade-status.rst
  • docs/user/websocket-api.rst

Fix these issues in Kilo Cloud

Previous review (commit a6f27f0)

Status: 3 Issues Found | Recommendation: Merge with follow-up

The persistent mass-upgrade pipeline is solid: atomic status claims, conditional failed/in-progress re-checks before dispatch, an immutable is_persistent, tenant-scoped cancel/delete guards, and broad test coverage (handlers, tasks, notifications, selenium). No critical bugs or security issues were found in the changed code. Remaining items are deploy-time migration notes and a documentation gap.

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 45 next_retry_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated upgradeoperation table during deploy (already commented inline).
openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py 18 last_reminder_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated batchupgradeoperation table during deploy. Same class as 0018:45; consider a maintenance window or a concurrent index via RunSQL (newly commented inline).
docs/user/rest-api.rst 215 REST API reference not updated: POST /build/{id}/upgrade/ omits the new is_persistent/upgrade_all body params, and the operation status lists omit pending (file is outside the diff, so noted here only).
Other Observations (not in diff)

The operation_update payload doc in docs/user/websocket-api.rst still omits is_persistent, retry_count and next_retry_at, which UpgradeOperationSerializer now pushes over the device channel.

The Beat scanner (tasks.py) dispatches one retry_pending_upgrade task per due pending operation with no cap/batching; for a very large offline fleet this can fan out thousands of broker publishes per tick — a follow-up to stream/batch the dispatch would be welcome, but it is not a correctness bug.

Files Reviewed (18 files)
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py - 1 issue
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py - 1 issue
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js
  • docs/user/persistent-mass-upgrades.rst
  • docs/user/settings.rst
  • docs/user/upgrade-status.rst
  • docs/user/websocket-api.rst

Fix these issues in Kilo Cloud

Previous review (commit fa67c1f)

Status: 2 Issues Found | Recommendation: Merge with follow-up

The persistence pipeline is well structured: atomic status claims, conditional failed/in-progress re-checks before dispatch, immutable is_persistent, and solid test coverage (handlers, tasks, notifications, selenium). The previously raised race in retry_pending_upgrade() and the JS pending/cancel rendering have been addressed. Remaining items are a deploy-time migration note (already commented inline) and a documentation gap.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 45 Adding next_retry_at with db_index=True builds a regular blocking index during deploy instead of a concurrent one (already commented inline).
docs/user/rest-api.rst 215 REST API reference was not updated: POST /build/{id}/upgrade/ still omits the new is_persistent/upgrade_all body params, and the operation status lists omit pending.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
docs/user/websocket-api.rst 82 The operation_update payload doc still omits is_persistent, retry_count and next_retry_at, which UpgradeOperationSerializer now includes in the device-channel push.

Note: the earlier reminder-task concern is no longer valid — only persistent operations can reach pending (guarded in _recoverable_failure_handler), so send_pending_upgrade_reminders filtering on upgradeoperation__status="pending" is implicitly scoped to persistent batches.

Files Reviewed (18 files)
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py - 1 issue
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js
  • docs/user/rest-api.rst - 1 issue
  • docs/user/websocket-api.rst
  • docs/user/upgrade-status.rst
  • docs/user/persistent-mass-upgrades.rst
  • docs/user/settings.rst

Fix these issues in Kilo Cloud

Previous review (commit 1b86bff)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 4
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/tasks.py 172 Reminder notifications are not limited to persistent batches, so a non-persistent batch with a pending child can still emit pending_upgrade_reminder.
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 45 Adding next_retry_at with db_index=True will build a regular blocking index during deploy instead of a concurrent one.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
docs/user/rest-api.rst 215 The REST API reference was not updated for the new public persistence surface: POST /build/{id}/upgrade/ still omits is_persistent, and the operation status lists still omit pending.
docs/user/websocket-api.rst 76 The WebSocket reference still omits the new is_persistent, retry_count, and next_retry_at fields returned in operation payloads.
Files Reviewed (21 files)
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tasks.py - 1 issue
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py - 1 issue
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-progress.css
  • docs/user/settings.rst
  • docs/user/upgrade-status.rst
  • docs/user/websocket-api.rst - 1 issue
  • docs/user/rest-api.rst - 1 issue
  • docs/user/persistent-mass-upgrades.rst
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_handlers.py

Fix these issues in Kilo Cloud

Previous review (commit 7f53d39)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 36 Adding next_retry_at with db_index=True creates a blocking index during deploy instead of a concurrent one.
openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js 204 Batch progress UI behavior changed, but the existing Selenium suite was not updated to cover the new pending-count rendering.
docs/user/settings.rst 27 Retry/status behavior is already documented, but the new persistent retry settings and pending state are not documented in this PR.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
docs/user/settings.rst 27 The docs still describe only the immediate Celery retry flow; they do not mention the newly added persistent retry settings (OPENWISP_FIRMWARE_UPGRADER_PERSISTENT_RETRY_*).
docs/user/upgrade-status.rst 12 The status reference still omits the new pending operation state and its cancellation semantics.
docs/user/websocket-api.rst 134 The batch websocket payload documentation still omits the new pending field in batch_state and batch_status messages.
Files Reviewed (7 files)
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py - 1 issue
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js - 1 issue
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/websockets.py

Fix these issues in Kilo Cloud


Reviewed by glm-5.2-20260616 · Input: 275.9K · Output: 6K · Cached: 296.6K

@coderabbitai coderabbitai Bot added the enhancement New feature or request label May 23, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR lays the model-layer groundwork for “persistent” mass upgrades by extending the upgrade/batch operation schema, adding a new pending operation status, propagating batch persistence to child operations, and enforcing immutability of the persistence flag after launch.

Changes:

  • Added persistent flags to batch and per-device upgrade operations, plus retry_count and next_retry_at fields for retry scheduling.
  • Introduced pending to UpgradeOperation.STATUS_CHOICES and propagated batch.persistent into child operations during creation.
  • Added unit tests for defaults, propagation, DB index presence, and immutability guards; added corresponding migrations (including sample app migration).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
openwisp_firmware_upgrader/base/models.py Adds persistence fields, pending status, batch→child propagation, and immutability validation in clean().
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_persistent_and_more.py Introduces DB fields and extends status choices at the schema level.
openwisp_firmware_upgrader/tests/test_models.py Adds tests for new field defaults, propagation behavior, DB index flag, and immutability validation.
tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py Mirrors the migration in the sample app used by the test project.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 843 to 847
("failed", _("failed")), # failed at late stage or can't reconnect
("cancelled", _("cancelled")), # cancelled by the user
("aborted", _("aborted")), # aborted due to prerequisites not met
("pending", _("pending")), # offline device; waiting for periodic retry
)
("failed", _("failed")), # failed at late stage or can't reconnect
("cancelled", _("cancelled")), # cancelled by the user
("aborted", _("aborted")), # aborted due to prerequisites not met
("pending", _("pending")), # offline device; waiting for periodic retry
Comment on lines +870 to +874
persistent = models.BooleanField(
default=False,
verbose_name=_("persistent"),
help_text=_(
"if enabled, the operation stays pending and retries "
Comment on lines +571 to +575
persistent = models.BooleanField(
default=True,
verbose_name=_("persistent"),
help_text=_(
"if enabled, the mass upgrade keeps retrying "

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@openwisp_firmware_upgrader/base/models.py`:
- Around line 617-631: When checking immutability for the persistent field in
the model (inside the BatchUpgradeOperation instance logic), use the persisted
DB status instead of the in-memory self.status so callers can't flip status to
"idle" during the same edit; fetch the stored status via
load_model("BatchUpgradeOperation").objects.values_list("status",
flat=True).get(pk=self.pk) (similar to the existing persistent fetch) and change
the gate from self.status != "idle" to stored_status != "idle" before comparing
self.persistent to the persisted persistent and raising the ValidationError.

In `@openwisp_firmware_upgrader/tests/test_models.py`:
- Around line 606-608: Replace hard-coded app-label string patching with
model-bound patching: import or reference the UpgradeOperation model class and
use mock.patch.object(UpgradeOperation, "upgrade", return_value=None) instead of
mock.patch(f"{self.app_label}.models.UpgradeOperation.upgrade", ...); do the
same for the other occurrence that patches UpgradeOperation.upgrade (the second
instance mentioned) so tests remain resilient to model swapping.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4eaa450b-cfb3-458c-8956-a3dc2e9dbe2a

📥 Commits

Reviewing files that changed from the base of the PR and between f9f1e6a and 6595d4f.

📒 Files selected for processing (4)
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: copilot-pull-request-reviewer
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/base/models.py
🧠 Learnings (3)
📚 Learning: 2026-02-21T20:21:02.014Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:02.014Z
Learning: In Django migrations within this repo, avoid adding explicit related_name to ForeignKey fields when the default reverse accessor (<model>_set) is acceptable. Rely on Django’s default naming convention to keep migrations concise and maintainable. Only specify related_name if you have a concrete, needed override for reverse relations.

Applied to files:

  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py
📚 Learning: 2026-02-23T21:36:22.028Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: tests/openwisp2/sample_firmware_upgrader/migrations/0001_initial.py:61-67
Timestamp: 2026-02-23T21:36:22.028Z
Learning: In the sample_firmware_upgrader app within tests, avoid creating new migration files for small changes since the sample is for demonstration and testing. Review migrations with this context: the sample's initial migration may differ from the main app and need not be strictly aligned. If you change models for the demo, keep migrations minimal and document why divergence exists; don’t require parity with the main app migrations in PRs focused on the sample.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_models.py
🔇 Additional comments (4)
openwisp_firmware_upgrader/tests/test_models.py (1)

586-602: LGTM!

Also applies to: 609-630, 634-673

openwisp_firmware_upgrader/base/models.py (1)

461-461: LGTM!

Also applies to: 571-579, 846-846, 870-894, 896-913

openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_persistent_and_more.py (1)

13-67: LGTM!

tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py (1)

13-67: LGTM!

Comment thread openwisp_firmware_upgrader/base/models.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_models.py Outdated
@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from 6595d4f to 4dba3f9 Compare May 23, 2026 11:43

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's looking good, please batch in more work or it will be hard for me to get an idea of whether we're really going in the right direction.

blank=True,
null=True,
)
persistent = models.BooleanField(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is more consistent with other openwisp modules:

Suggested change
persistent = models.BooleanField(
is_persistent = models.BooleanField(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated

("failed", _("failed")), # failed at late stage or can't reconnect
("cancelled", _("cancelled")), # cancelled by the user
("aborted", _("aborted")), # aborted due to prerequisites not met
("pending", _("pending")), # offline device; waiting for periodic retry

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

make sure this is cancellable and add a test for this: pending upgrades can be cancled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added

status = models.CharField(
max_length=12, choices=STATUS_CHOICES, default=STATUS_CHOICES[0][0]
)
persistent = models.BooleanField(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is more consistent with other openwisp modules:

Suggested change
persistent = models.BooleanField(
is_persistent = models.BooleanField(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would include the minimum behavior that makes the new state coherent:

  • BatchUpgradeOperation.progress_report: pending must not be counted as completed.
  • BatchUpgradeOperation.calculate_and_update_status(): a batch with pending child operations should remain active, not become success.
  • UpgradeOperation.cancel(): pending operations should be cancellable.
  • UpgradeOperation.upgrade(): an existing pending operation should block a second upgrade for the same device, the same way in-progress does.

I would also update the BatchUpgradeOperation.clean() immutability check to use the stored database status, not self.status, otherwise a caller can set status="idle" and change is_persistent in the same edit.

Please rename persistent to is_persistent consistently on both BatchUpgradeOperation and UpgradeOperation.

For tests, I would add a batch with a mix of success and pending child operations. The expected behavior should be: progress does not count pending as completed, batch status remains active, pending operation can be cancelled, and another upgrade for the same device is blocked while the pending operation exists.

@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch 2 times, most recently from 45fad3d to 6d84175 Compare May 26, 2026 15:48
@openwisp-companion

Copy link
Copy Markdown

Multiple Test Failures Detected

Hello @Eeshu-Yadav,
(Analysis for commit b32e764)

There are multiple test failures in your commit:

  1. Test Failures: Several tests failed, indicated by FAILED (failures=2, errors=1). The specific tests that failed are listed in the logs, including test_save_device_after_credentials_deleted, test_save_device_with_deleted_devicefirmware, and test_api_batch_upgrade_list_filter_org.

Fix: Please review the failing tests and their corresponding code to identify the root cause of the failures. Common issues include incorrect test setup, assertion errors, or unexpected behavior in the code being tested.

  1. Syntax Warnings: Multiple SyntaxWarning: invalid escape sequence warnings were reported in openwisp_firmware_upgrader/upgraders/openwrt.py and openwisp_firmware_upgrader/tests/test_openwrt_upgrader.py. These warnings indicate improperly escaped backslashes in regular expressions or string literals.

Fix: Escape backslashes in string literals and regular expressions by using double backslashes (\\) or by using raw strings (e.g., r'...'). For example, r'\{' or '\\{'.

@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from b32e764 to f910f6a Compare May 26, 2026 19:17
@openwisp-companion

Copy link
Copy Markdown

The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (1/3).

@nemesifier

Copy link
Copy Markdown
Member

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from f910f6a to 741148f Compare May 27, 2026 05:44
name="next_retry_at",
field=models.DateTimeField(
blank=True,
db_index=True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: This migration adds a blocking index on upgradeoperation.next_retry_at

AddField(... db_index=True) will create the index with regular DDL during the deploy. On populated installations that can block writes to upgradeoperation, so this index should be moved into a separate non-atomic concurrent migration, for example with AddIndexConcurrently.

if (completedInfo.length > 0) {
completedInfo.text(`${data.completed} out of ${data.total}`);
let pending = data.pending !== undefined ? data.pending : 0;
completedInfo.text(`${data.completed} complete, ${pending} pending`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: This UI change is not covered by the existing browser test suite

The batch page now renders a new "X complete, Y pending" status string, but the Selenium coverage in openwisp_firmware_upgrader/tests/test_selenium.py still only exercises the older batch-status flow. Because this PR changes user-visible batch progress behavior and the project already has Selenium tests, a browser regression test should be added before merge.

@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from 741148f to 509a56a Compare May 27, 2026 06:04
@openwisp-companion

Copy link
Copy Markdown

The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (1/3).

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's going in the right direction, next steps:

  • admin
  • docs: start adding a mention of this feature in the intro page, let's add a dedicated page for persistent upgrades

Keep pushing to this branch, I need to see the whole thing before I am confident in merging, if I merge now, reviewing the diff for other fragments will hide details that make my life as a reviewer harder. Go ahead, keep iterating, pushing, advancing.

Update the PR title and description when due, add screenshots when there's UI changes.

batch_status,
stats["completed"],
stats["total_operations"],
stats["pending"],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

check if the docs need to be updated due to this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes docs updated related to this


def clean(self):
super().clean()
if not self._state.adding:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

move this to a private method which should have a docstring summarizing what validation it enforces, we have this kind of pattern pretty much everywhere in openwisp

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

moved to _validate_is_persistent_immutable in both classes

completedInfo.text(`${data.completed} complete, ${pending} pending`);
} else {
completedInfo.text(`${data.completed} out of ${data.total}`);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

add assertions in the existing selenium tests for this logic please

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated

)
PERSISTENT_RETRY_DISPATCH_JITTER = getattr(
settings, "OPENWISP_FIRMWARE_UPGRADER_PERSISTENT_RETRY_DISPATCH_JITTER", 300
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These need to be documented. Are you sure we can't add one setting with a dictionary for all of them to avoid bloating the docs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated it

CELERY_BROKER_URL = "memory://"
CELERY_RESULT_BACKEND = "cache+memory://"

CELERY_BEAT_SCHEDULE = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we have issues in ansible-openwisp2 and docker-openwisp to add these?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

i have opened the issues in the ansible and docker repos :
openwisp/docker-openwisp#623
openwisp/ansible-openwisp2#621

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (1)
openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js (1)

95-101: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pending should not be rendered as fully complete.

Line 100 makes pending operations display as 100%, which conflicts with pending being a non-terminal state.

Suggested fix
 const STATUSES_WITH_FULL_PROGRESS = new Set([
   FW_UPGRADE_STATUS.SUCCESS,
   FW_UPGRADE_STATUS.FAILED,
   FW_UPGRADE_STATUS.ABORTED,
   FW_UPGRADE_STATUS.CANCELLED,
-  FW_UPGRADE_STATUS.PENDING,
 ]);
🤖 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 `@openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js`
around lines 95 - 101, STATUSES_WITH_FULL_PROGRESS currently includes
FW_UPGRADE_STATUS.PENDING which causes pending upgrades to render as 100%;
remove FW_UPGRADE_STATUS.PENDING from the STATUSES_WITH_FULL_PROGRESS set in
upgrade-utils.js (keep SUCCESS, FAILED, ABORTED, CANCELLED) and ensure any UI
logic that checks STATUSES_WITH_FULL_PROGRESS continues to treat PENDING as a
non-terminal state so it renders incremental progress rather than full progress.
🤖 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 `@docs/user/settings.rst`:
- Around line 114-115: Update the docs entry that currently states reminders are
sent as ``generic_message`` to instead reference the actual notification type
``pending_upgrade_reminder`` so docs match the code; specifically, change the
wording in settings.rst to mention ``pending_upgrade_reminder`` (the type used
in notify.send(type="pending_upgrade_reminder", ...)) and ensure the description
clarifies that this notification is sent to the batch's organization admins and
all superusers.

In `@docs/user/upgrade-status.rst`:
- Around line 55-58: Update docs/user/upgrade-status.rst to clarify that the
"pending" upgrade state is considered an active/non-terminal state for deletion
guards: add a concise note in the User Actions paragraph (and mirror it in the
"Deleting Upgrade Operations" section) stating that pending operations cannot be
deleted immediately and are guarded the same as in-progress operations
(cancellation is allowed but direct deletion is blocked until they leave
pending). Use the term "pending" exactly and reference the deletion guard
behavior so readers don’t assume immediate deletion is permitted.

In `@openwisp_firmware_upgrader/api/serializers.py`:
- Around line 170-180: The current update method raises a ValidationError
whenever "is_persistent" is in validated_data regardless of batch state; change
the condition to only reject changes when the batch is not in the "idle" state
(e.g., if "is_persistent" in validated_data and instance.status != "idle": raise
the same ValidationError). Update the check inside the update(self, instance,
validated_data) method to gate the immutability on instance.status instead of
unconditionally blocking, keeping the existing error payload and returning
super().update(...) otherwise.

In `@openwisp_firmware_upgrader/apps.py`:
- Around line 69-96: The notification type display strings passed to
register_notification_type are not marked for translation; update the
verbose_name, verb, and email_subject values in the two
register_notification_type calls so they use the gettext_lazy alias _
(preserving placeholders like {site.name} and {notification.target})—i.e., wrap
the current literal strings for the "pending_upgrade_reminder" and
"persistent_upgrade_failed" registrations (which reference models
BatchUpgradeOperation, UpgradeOperation, Device) with _(...) to make them
translatable.
- Around line 165-168: The current try/except in connect_monitoring_signals()
swallows any ImportError when importing
openwisp_monitoring.device.signals.health_status_changed and can mask unrelated
errors; change the handler to only ignore a missing openwisp_monitoring
package/module (use ModuleNotFoundError or inspect the exception.name/message)
and re-raise other ImportError cases so real issues inside openwisp_monitoring
surface. Specifically, update the import block that references
health_status_changed to catch ModuleNotFoundError (or check e.name contains
"openwisp_monitoring" / "openwisp_monitoring.device.signals") and return only in
that case, otherwise re-raise the exception.

In `@openwisp_firmware_upgrader/base/models.py`:
- Around line 746-747: The progress_report function currently wraps f-strings in
_() so gettext cannot extract static msgids; change the two translatable strings
to use placeholder-based messages passed to _() (e.g. _("%(completed)s complete,
%(pending)s pending") and _("%(completed)s out of %(total)s")) and then format
them with .format() or % using the stats dict/ self.total_operations (refer to
progress_report, stats and self.total_operations) so translations can be
extracted.
- Around line 1117-1120: Wrap the user-facing log string in Django's translation
function so it is marked translatable (e.g. use gettext/gettext_lazy as _).
Locate the log call in the persistent-retry code path (the multi-line f-string
that includes {cause}, {self.retry_count}, and {self.next_retry_at}) and change
the string to _(f"...") (ensure you import gettext/gettext_lazy from
django.utils.translation at the top). Keep the f-string interpolation for cause,
self.retry_count and self.next_retry_at, but wrap the entire message with the
translation helper so Django i18n picks it up.

In `@openwisp_firmware_upgrader/settings.py`:
- Around line 27-38: The current assignment to PERSISTENT_RETRY_OPTIONS replaces
the entire options dict when
settings.OPENWISP_FIRMWARE_UPGRADER_PERSISTENT_RETRY_OPTIONS is provided, which
can drop required keys; change it to start from the default dict (base_delay,
multiplier, jitter, max_delay, dispatch_jitter, signal_jitter) and update/merge
with any keys present in
settings.OPENWISP_FIRMWARE_UPGRADER_PERSISTENT_RETRY_OPTIONS so partial
overrides only replace specific keys; locate the PERSISTENT_RETRY_OPTIONS
variable in settings.py and implement merging (e.g., copy default then
.update(override)) using those exact symbol names.

In
`@openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-progress.css`:
- Around line 1-3: Remove the global override of the CSS variable
--ow-color-danger in the :root block; instead either delete the :root {
--ow-color-danger: var(--ow-color-primary); } rule entirely or scope a
component-specific variable (e.g. .firmware-upgrader { --upgrader-color-danger:
var(--ow-color-primary); }) and update any uses within the firmware upgrader
styles to reference the new local variable; ensure no other files rely on a
global remap of --ow-color-danger.

In
`@openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js`:
- Around line 203-208: The new user-facing strings in the completedInfo.text
calls are hard-coded in English; update the two branches that set
completedInfo.text (the branch using `${data.completed} complete, ${pending}
pending` and the else branch `${data.completed} out of ${data.total}`) to use
the project's JS translation helper (e.g., gettext/_.gettext/gettextCatalog
depending on your app) and format/interpolate data.completed, pending and
data.total via that function so the messages are localizable while preserving
the same content and variables.

In `@openwisp_firmware_upgrader/tasks.py`:
- Around line 170-179: In send_pending_upgrade_reminders the
BatchUpgradeOperation update sets last_reminder_at during the optimistic "claim"
step before recomputing pending_count, which means if pending_count becomes zero
due to concurrent changes you skip notify.send(...) but have already stamped
last_reminder_at and suppressed future reminders; change the flow so you only
update last_reminder_at after confirming pending_count > 0 and just before
calling notify.send(...). Concretely: use
BatchUpgradeOperation.objects.filter(pk=batch.pk).filter(due_condition).select_for_update()/claim
logic to test claim, compute pending_count from batch.upgradeoperation_set, and
only call
BatchUpgradeOperation.objects.filter(pk=batch.pk).update(last_reminder_at=timezone.now())
immediately before notify.send(...) (and only if pending_count > 0) to avoid
false suppression by due_condition.

---

Outside diff comments:
In `@openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js`:
- Around line 95-101: STATUSES_WITH_FULL_PROGRESS currently includes
FW_UPGRADE_STATUS.PENDING which causes pending upgrades to render as 100%;
remove FW_UPGRADE_STATUS.PENDING from the STATUSES_WITH_FULL_PROGRESS set in
upgrade-utils.js (keep SUCCESS, FAILED, ABORTED, CANCELLED) and ensure any UI
logic that checks STATUSES_WITH_FULL_PROGRESS continues to treat PENDING as a
non-terminal state so it renders incremental progress rather than full progress.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ffd1009-a023-4cb5-a1cb-755c988695dc

📥 Commits

Reviewing files that changed from the base of the PR and between ca39bb8 and bdd6a17.

📒 Files selected for processing (30)
  • docs/user/intro.rst
  • docs/user/settings.rst
  • docs/user/upgrade-status.rst
  • docs/user/websocket-api.rst
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-progress.css
  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-selected-confirmation.css
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/websockets.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
docs/**/*.rst

📄 CodeRabbit inference engine (AGENTS.md)

Update docs when behavior, settings, public APIs, setup steps, or supported versions change

Files:

  • docs/user/intro.rst
  • docs/user/websocket-api.rst
  • docs/user/upgrade-status.rst
  • docs/user/settings.rst
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/settings.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_models.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Place imports at the top of the file. Only defer imports when necessary (e.g., Django model imports inside functions or methods where the app registry is not yet ready)
Avoid unnecessary blank lines inside function and method bodies
Mark user-facing strings for translation with Django i18n helpers in Django code
Write comments and docstrings only when they explain why code is shaped a certain way. Put comments before the relevant code block instead of scattering them inside it

Files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/settings.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_models.py
🧠 Learnings (6)
📚 Learning: 2026-02-21T20:21:02.014Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:02.014Z
Learning: In Django migrations within this repo, avoid adding explicit related_name to ForeignKey fields when the default reverse accessor (<model>_set) is acceptable. Rely on Django’s default naming convention to keep migrations concise and maintainable. Only specify related_name if you have a concrete, needed override for reverse relations.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
📚 Learning: 2026-02-23T21:36:22.028Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: tests/openwisp2/sample_firmware_upgrader/migrations/0001_initial.py:61-67
Timestamp: 2026-02-23T21:36:22.028Z
Learning: In the sample_firmware_upgrader app within tests, avoid creating new migration files for small changes since the sample is for demonstration and testing. Review migrations with this context: the sample's initial migration may differ from the main app and need not be strictly aligned. If you change models for the demo, keep migrations minimal and document why divergence exists; don’t require parity with the main app migrations in PRs focused on the sample.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
📚 Learning: 2026-06-02T08:16:00.439Z
Learnt from: asmodehn
Repo: openwisp/openwisp-firmware-upgrader PR: 362
File: tests/openwisp2/urls.py:10-38
Timestamp: 2026-06-02T08:16:00.439Z
Learning: In this repository, treat `SAMPLE_APP` as a conventionally truthy environment variable: code should use the established pattern `os.environ.get("SAMPLE_APP", False)` and rely on the resulting value’s truthiness (truthy string → enabled). During code reviews, avoid recommending changes to explicit boolean parsing (e.g., checking for `

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/settings.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_models.py
📚 Learning: 2026-02-24T00:04:04.187Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-options.css:11-13
Timestamp: 2026-02-24T00:04:04.187Z
Learning: In openwisp-firmware-upgrader, prefer hiding empty submit rows via CSS (e.g., `#upgradeoperation_form` .submit-row { display: none; }) rather than removing elements in templates. This keeps template logic simple and can improve rendering performance. Apply this pattern to CSS files in openwisp_firmware_upgrader/static/firmware-upgrader/css/*.css for consistent behavior across upgrade-option related styles.

Applied to files:

  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-progress.css
  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-selected-confirmation.css
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_models.py
📚 Learning: 2026-03-07T01:16:15.471Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:15.471Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. For the UpgradeOperationAdmin.change_view, model-level has_view_permission(request) suffices to show breadcrumbs for batch, and object-level permission checks are not required.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
🔇 Additional comments (18)
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py (1)

1-67: LGTM!

openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py (1)

1-24: LGTM!

tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py (1)

1-67: LGTM!

tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py (1)

1-27: LGTM!

openwisp_firmware_upgrader/tests/test_models.py (1)

10-22: LGTM!

Also applies to: 593-988, 1067-1077

openwisp_firmware_upgrader/admin.py (1)

128-135: LGTM!

Also applies to: 139-139, 227-227, 238-238, 253-253, 397-398, 404-404, 413-432, 439-441, 517-524, 528-528, 542-542, 555-555, 809-810

openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html (1)

82-85: LGTM!

openwisp_firmware_upgrader/api/serializers.py (1)

88-127: LGTM!

Also applies to: 133-145

openwisp_firmware_upgrader/api/views.py (1)

98-98: LGTM!

Also applies to: 103-107

openwisp_firmware_upgrader/tests/test_admin.py (1)

203-283: LGTM!

Also applies to: 2131-2176

openwisp_firmware_upgrader/websockets.py (1)

254-260: LGTM!

Also applies to: 268-268, 336-336, 506-513, 525-525

openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-selected-confirmation.css (1)

22-42: LGTM!

openwisp_firmware_upgrader/tests/test_websockets.py (1)

403-409: LGTM!

Also applies to: 705-787

openwisp_firmware_upgrader/tests/test_selenium.py (1)

994-1045: LGTM!

tests/openwisp2/settings.py (1)

3-3: LGTM!

Also applies to: 191-200

openwisp_firmware_upgrader/tests/test_tasks.py (1)

1-164: LGTM!

openwisp_firmware_upgrader/tests/test_handlers.py (1)

1-163: LGTM!

openwisp_firmware_upgrader/tests/test_notifications.py (1)

1-209: LGTM!

Comment thread docs/user/settings.rst Outdated
Comment thread docs/user/upgrade-status.rst Outdated
Comment thread openwisp_firmware_upgrader/api/serializers.py Outdated
Comment thread openwisp_firmware_upgrader/apps.py
Comment thread openwisp_firmware_upgrader/apps.py
Comment on lines +1117 to +1120
f"All immediate retries exhausted: {cause}. "
f"Scheduled persistent retry #{self.retry_count} "
f"at {self.next_retry_at}.",
save=False,

Copy link
Copy Markdown

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

Mark persistent-retry log text for translation.

This new log line is user-facing in operation logs but is not wrapped with Django i18n helpers.

Suggested fix
-            self.log_line(
-                f"All immediate retries exhausted: {cause}. "
-                f"Scheduled persistent retry #{self.retry_count} "
-                f"at {self.next_retry_at}.",
-                save=False,
-            )
+            self.log_line(
+                _(
+                    "All immediate retries exhausted: %(cause)s. "
+                    "Scheduled persistent retry #%(retry_count)s at %(next_retry_at)s."
+                )
+                % {
+                    "cause": cause,
+                    "retry_count": self.retry_count,
+                    "next_retry_at": self.next_retry_at,
+                },
+                save=False,
+            )

As per coding guidelines, for Django pull requests all user-facing strings must be marked as translatable using Django i18n.

🤖 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 `@openwisp_firmware_upgrader/base/models.py` around lines 1117 - 1120, Wrap the
user-facing log string in Django's translation function so it is marked
translatable (e.g. use gettext/gettext_lazy as _). Locate the log call in the
persistent-retry code path (the multi-line f-string that includes {cause},
{self.retry_count}, and {self.next_retry_at}) and change the string to _(f"...")
(ensure you import gettext/gettext_lazy from django.utils.translation at the
top). Keep the f-string interpolation for cause, self.retry_count and
self.next_retry_at, but wrap the entire message with the translation helper so
Django i18n picks it up.

Comment thread openwisp_firmware_upgrader/settings.py Outdated
Comment thread openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-progress.css Outdated
Comment thread openwisp_firmware_upgrader/tasks.py Outdated
Resolve issues raised in review of the persistent mass upgrades work:

- Only stamp last_reminder_at after confirming a batch still has pending
  children, so a concurrent drain no longer suppresses future reminders.
- Gate the batch serializer's is_persistent immutability on the stored
  status so idle batches can still toggle the flag, matching the model's
  clean() and the error message.
- Drop the global :root override of --ow-color-danger (a real, defined
  openwisp-utils token) that repainted unrelated danger UI; failed bars
  resolve to the danger colour directly.
- Mark notification type strings, the progress_report text and the batch
  completion JS as translatable.
- Narrow the monitoring import guard to ModuleNotFoundError so real import
  errors inside openwisp-monitoring surface instead of being swallowed.
- Merge PERSISTENT_RETRY_OPTIONS overrides onto the defaults to avoid
  missing-key crashes on partial overrides.
- Correct the reminder notification type name and document that pending
  operations are guarded against deletion.

Related to #379
@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from a92a594 to baeef7c Compare June 8, 2026 12:37
@nemesifier

Copy link
Copy Markdown
Member

@coderabbitai any outstanding issue? Please one last full review.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== admin.py is_persistent fields ==="
grep -n "is_persistent\|retry_count\|next_retry_at\|IN_PROGRESS_STATUS\|pending" openwisp_firmware_upgrader/admin.py | head -60

echo ""
echo "=== serializers.py ==="
cat openwisp_firmware_upgrader/api/serializers.py

echo ""
echo "=== settings.py ==="
cat openwisp_firmware_upgrader/settings.py

echo ""
echo "=== tasks.py beat tasks ==="
grep -n "check_pending\|send_pending\|retry_pending\|CELERY_BEAT\|beat" openwisp_firmware_upgrader/tasks.py | head -30

echo ""
echo "=== test settings beat schedule ==="
grep -n "CELERY_BEAT" tests/openwisp2/settings.py

echo ""
echo "=== docs settings.rst ==="
grep -n "PERSISTENT\|pending" docs/user/settings.rst | head -40

echo ""
echo "=== docs upgrade-status.rst ==="
grep -n "pending\|Pending" docs/user/upgrade-status.rst | head -20

echo ""
echo "=== models.py - _recoverable_failure_handler, cancel, pending guard ==="
grep -n "pending\|_recoverable_failure\|CANCELLABLE\|concurrent\|handle_health\|notify_on_failed\|from_db\|_calculate_next" openwisp_firmware_upgrader/base/models.py | head -60

echo ""
echo "=== Check for max retry count cap ==="
grep -n "max_retry\|MAX_RETRY\|retry_count.*>\|> retry_count\|retry_count.*max\|max.*retry_count" openwisp_firmware_upgrader/base/models.py openwisp_firmware_upgrader/tasks.py openwisp_firmware_upgrader/settings.py 2>/dev/null

echo ""
echo "=== Check apps.py for signal/notification wiring ==="
grep -n "connect\|register\|signal\|notify\|health" openwisp_firmware_upgrader/apps.py | head -40

echo ""
echo "=== Check websockets.py ==="
grep -n "pending\|completed\|publish_batch" openwisp_firmware_upgrader/websockets.py | head -30

echo ""
echo "=== Check views.py for is_persistent ==="
grep -n "is_persistent" openwisp_firmware_upgrader/api/views.py

echo ""
echo "=== Check for any missing migrations dependency chain ==="
grep -n "dependencies" openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
grep -n "dependencies" openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py

echo ""
echo "=== Check if BatchUpgradeConfirmationForm includes is_persistent ==="
grep -n "is_persistent\|BatchUpgradeConfirmationForm" openwisp_firmware_upgrader/admin.py | head -20

echo ""
echo "=== Check for clean() extra query issue ==="
grep -n "def clean\|_state.adding\|from_db\|is_persistent" openwisp_firmware_upgrader/base/models.py | head -30

Length of output: 22346


🏁 Script executed:

#!/bin/bash
echo "=== Full admin.py ==="
cat openwisp_firmware_upgrader/admin.py

echo ""
echo "=== Full models.py excerpt - clean and _recoverable_failure_handler ==="
grep -n "" openwisp_firmware_upgrader/base/models.py | grep -A30 "def clean\|_recoverable_failure_handler\|def cancel\|concurrent\|handle_health"

Length of output: 51584



@nemesifier Here is the final review report. All four medium-severity issues from the previous review have been resolved. Only low-priority items remain.


Final Review Report — PR #436

✅ All Previously Identified Medium Issues: Resolved

# Issue Status
1 is_persistent not in Django admin ✅ Fixed — added to UpgradeOperationAdmin (list/detail/filter) and BatchUpgradeOperationAdmin (list/detail/filter); BatchUpgradeConfirmationForm exposes it at launch time
3 New settings undocumented ✅ Fixed — docs/user/settings.rst documents OPENWISP_FIRMWARE_UPGRADER_PERSISTENT_RETRY_OPTIONS and OPENWISP_FIRMWARE_UPGRADER_PERSISTENT_REMINDER_PERIOD
4 is_persistent absent from REST API serializers ✅ Fixed — BatchUpgradeSerializer, UpgradeOperationSerializer, and DeviceUpgradeOperationSerializer all expose is_persistent; retry_count/next_retry_at are read-only; both serializers block is_persistent changes via update()
7 Beat schedule wiring ✅ Fixed — tests/openwisp2/settings.py wires check_pending_upgrades (600 s) and send_pending_upgrade_reminders (604800 s)

🟡 Remaining Low-Priority Items

1. signal_jitter undocumented in settings.rst

PERSISTENT_RETRY_OPTIONS in settings.py ships with a signal_jitter: 120 default key (used by handle_health_status_changed to add randomness when the monitoring signal fires). The settings.rst documentation mentions the other five keys but does not mention signal_jitter. Worth adding a line.

2. BatchUpgradeOperationAdmin: is_persistent always read-only

readonly_fields on BatchUpgradeOperationAdmin includes is_persistent unconditionally. The model's clean() permits changing it while status == "idle", but the admin never exposes an editable widget for it — meaning administrators cannot toggle it before launching. This is a very minor UX gap; the confirmation form covers the common case. If editable-before-launch is desired in the change view, is_persistent would need to be removed from readonly_fields and a conditional get_readonly_fields used instead.

3. Extra DB query in UpgradeOperation.clean() (pre-existing low-priority)

_validate_is_persistent_immutable issues one SELECT per call on existing instances. No change since last review; still low priority at scale.

4. retry_count unbounded (acknowledged by design)

The delay is capped at max_delay (12 h default), so the effective retry window doesn't grow, but the counter itself has no ceiling. No action required unless a future PR wants to add an optional max-retries-to-failed path.


Summary

The implementation is complete and well-tested. The core data model, retry pipeline, admin surfaces, REST API serializers, WebSocket integration, documentation, and Celery Beat schedule are all correctly in place. The only remaining actionable item worth tracking is the missing signal_jitter entry in settings.rst. Everything else is either by design or negligible.

@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from fa33375 to 19847b7 Compare June 9, 2026 18:58
…ests #429

- time_travel context manager in tests/base.py freezes timezone.now() so
  the backoff, Beat-scan and reminder-cadence tests are deterministic.
- test_persistence_loop_offline_then_back_online drives the loop: an
  offline op pends, the Beat scan retries it once due, it pends again with
  a later next_retry_at, and succeeds once the device is reachable again.
- Selenium: the mass-upgrade confirmation page renders the is_persistent
  checkbox pre-checked and submitting propagates it to the batch and its
  children; a pending child renders the orange progress indicator.

Related to #429
@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from 19847b7 to b404531 Compare June 9, 2026 19:12

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great to see so much progress here! See my comments below to continue improving the solution.

Comment thread openwisp_firmware_upgrader/tasks.py Outdated
)
operation.save()
return
operation.save()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think retry_pending_upgrade() still has a race after the atomic claim. The task flips pending to in-progress, loads the operation, then later calls operation.save() on that stale instance before dispatching upgrade_firmware. If an admin cancels the operation between the claim and this save, the save can write status="in-progress" back over cancelled and the task will still enqueue the upgrade. The deactivated-device branch can similarly overwrite a concurrent cancellation with failed. I would avoid saving the stale instance with all fields, make the failed transition conditional on status="in-progress", and re-check that the row is still in-progress immediately before dispatching.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed — the deactivated branch flips to failed only via a conditional filter(status="in-progress").update(...), the dispatch save uses update_fields=["log"] so it never rewrites status, and we re-check the row is still in-progress before dispatch and again at the top of upgrade_firmware.

Comment thread openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js Outdated
ABORTED: gettext("aborted"),
CANCELLED: gettext("cancelled"),
IN_PROGRESS: gettext("in progress"),
PENDING: gettext("pending"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Pending operations are cancellable in the model and REST API, and the new docs say they can be cancelled from the admin too, but the live operation/device page only renders the cancel button for FW_STATUS_GROUPS.IN_PROGRESS. A pending retry shown on the admin page has no cancel action, so the UI does not expose the backend behavior. I would treat pending as an active cancellable status here, render it with the pending progress class, and add a Selenium regression for the pending case.

@Eeshu-Yadav Eeshu-Yadav Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done , added a CANCELLABLE group (in-progress + pending), the cancel button renders for pending with the pending class, and added a selenium regression.

model_name="batchupgradeoperation",
name="is_persistent",
field=models.BooleanField(
default=True,

@nemesifier nemesifier Jun 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Existing BatchUpgradeOperation rows are backfilled as is_persistent=True, even though every historical mass upgrade was non-persistent before this migration. This is visible in the admin/API for old rows and can change behavior for any queued idle batch that is launched after deploy. Let's backfill existing batches as False, then alter the model default to True for new batches. The sample app migration at tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py should mirror the same pattern .

@Eeshu-Yadav Eeshu-Yadav Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done,backfilled existing batches as False then altered the default to True; the sample migration mirrors it.

``next_retry_at`` has elapsed and re-dispatches them
- A device deactivated while pending is set to ``failed`` and not retried

**User Actions**: Pending operations can be cancelled the same way as

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This new section says pending operations can be cancelled, but the cancellation section later still says cancellation is possible only while the status is in-progress. I would update that bullet to mention both in-progress and pending.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated the cancellation bullet and the cancel-button line to mention both in-progress and pending.

@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch 4 times, most recently from 5cc6274 to 1b86bff Compare June 17, 2026 12:35
)
qs = (
BatchUpgradeOperation.objects.filter(
status="in-progress", upgradeoperation__status="pending"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Reminder task can notify non-persistent batches

This queryset only filters on status="in-progress" plus pending children, so any batch that happens to have a pending child will emit pending_upgrade_reminder even when batch.is_persistent is False. The new reminder flow is documented and tested as persistence-specific behavior, so this should also constrain the batch queryset with is_persistent=True.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from 1b86bff to fa67c1f Compare June 17, 2026 13:55
@openwisp-companion

Copy link
Copy Markdown

The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (1/3).

@nemesifier

Copy link
Copy Markdown
Member

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py (1)

16-24: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Index last_reminder_at to avoid periodic reminder scans degrading at scale.

Line 19 defines last_reminder_at without an index, but the reminder task filters on this field repeatedly (send_pending_upgrade_reminders due-condition query). This can become a hot table scan path as batch rows increase.

💡 Suggested fix
         migrations.AddField(
             model_name="batchupgradeoperation",
             name="last_reminder_at",
             field=models.DateTimeField(
                 blank=True,
+                db_index=True,
                 help_text="timestamp of the last pending-upgrade reminder fired for this batch, null if no reminder has been sent yet",
                 null=True,
                 verbose_name="last reminder at",
             ),
         ),

Also mirror this in:

  • openwisp_firmware_upgrader/base/models.py (last_reminder_at = models.DateTimeField(..., db_index=True, ...))
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
🤖 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
`@openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py`
around lines 16 - 24, The `last_reminder_at` DateTimeField in the migration
lacks a database index, which causes performance degradation when the reminder
task repeatedly filters on this field during `send_pending_upgrade_reminders`
operations at scale. Add `db_index=True` parameter to the DateTimeField
definition in the migration file. Additionally, apply the same change by adding
`db_index=True` to the `last_reminder_at` field definition in the base model
class in the models file, and mirror the change in the test sample migration
file to keep them synchronized.
🤖 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 `@openwisp_firmware_upgrader/api/serializers.py`:
- Around line 115-145: Add an update() method to
DeviceUpgradeOperationSerializer that prevents is_persistent from being modified
after creation, mirroring the existing protection in
UpgradeOperationSerializer's update() method. Additionally, add status, log, and
progress to the read_only_fields tuple in the Meta class of both
UpgradeOperationSerializer and DeviceUpgradeOperationSerializer to explicitly
mark these worker-controlled fields as immutable through the API.

In `@openwisp_firmware_upgrader/base/models.py`:
- Around line 1115-1116: The user-facing strings in the two self.log_line calls
are not wrapped with Django i18n translation helpers, preventing them from being
extracted for localization. Wrap both string arguments passed to self.log_line
calls with Django's gettext translation function (typically imported as
underscore or gettext_lazy). For the f-string containing the cause variable on
line 1115, ensure the translation function wraps the entire f-string expression,
and do the same for the plain string literal on line 1116 to mark both messages
as translatable.

---

Outside diff comments:
In
`@openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py`:
- Around line 16-24: The `last_reminder_at` DateTimeField in the migration lacks
a database index, which causes performance degradation when the reminder task
repeatedly filters on this field during `send_pending_upgrade_reminders`
operations at scale. Add `db_index=True` parameter to the DateTimeField
definition in the migration file. Additionally, apply the same change by adding
`db_index=True` to the `last_reminder_at` field definition in the base model
class in the models file, and mirror the change in the test sample migration
file to keep them synchronized.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6bcaa621-fa97-41a4-a9e6-aed72ed17265

📥 Commits

Reviewing files that changed from the base of the PR and between ca39bb8 and fa67c1f.

⛔ Files ignored due to path filters (5)
  • docs/images/1.3/persistent-mass-upgrade-batch.png is excluded by !**/*.png
  • docs/images/1.3/persistent-mass-upgrade-confirm.png is excluded by !**/*.png
  • docs/images/1.3/persistent-upgrade-notifications.png is excluded by !**/*.png
  • docs/images/1.3/persistent-upgrade-operation-pending.png is excluded by !**/*.png
  • docs/images/1.3/persistent-upgrade-pending-changelist.png is excluded by !**/*.png
📒 Files selected for processing (34)
  • docs/index.rst
  • docs/user/intro.rst
  • docs/user/persistent-mass-upgrades.rst
  • docs/user/settings.rst
  • docs/user/upgrade-status.rst
  • docs/user/websocket-api.rst
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-progress.css
  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-selected-confirmation.css
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/websockets.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
docs/**/*.rst

📄 CodeRabbit inference engine (AGENTS.md)

Update docs when behavior, settings, public APIs, setup steps, or supported versions change

Files:

  • docs/index.rst
  • docs/user/intro.rst
  • docs/user/persistent-mass-upgrades.rst
  • docs/user/websocket-api.rst
  • docs/user/upgrade-status.rst
  • docs/user/settings.rst
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/api/views.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/base/models.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Place imports at the top of the file. Only defer imports when necessary (e.g., Django model imports inside functions or methods where the app registry is not yet ready)
Avoid unnecessary blank lines inside function and method bodies
Mark user-facing strings for translation with Django i18n helpers in Django code
Write comments and docstrings only when they explain why code is shaped a certain way. Put comments before the relevant code block instead of scattering them inside it

Files:

  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/api/views.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/base/models.py
🧠 Learnings (7)
📚 Learning: 2026-02-21T20:21:02.014Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:02.014Z
Learning: In Django migrations within this repo, avoid adding explicit related_name to ForeignKey fields when the default reverse accessor (<model>_set) is acceptable. Rely on Django’s default naming convention to keep migrations concise and maintainable. Only specify related_name if you have a concrete, needed override for reverse relations.

Applied to files:

  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
📚 Learning: 2026-06-02T08:16:00.439Z
Learnt from: asmodehn
Repo: openwisp/openwisp-firmware-upgrader PR: 362
File: tests/openwisp2/urls.py:10-38
Timestamp: 2026-06-02T08:16:00.439Z
Learning: In this repository, treat `SAMPLE_APP` as a conventionally truthy environment variable: code should use the established pattern `os.environ.get("SAMPLE_APP", False)` and rely on the resulting value’s truthiness (truthy string → enabled). During code reviews, avoid recommending changes to explicit boolean parsing (e.g., checking for `

Applied to files:

  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/api/views.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-23T21:36:22.028Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: tests/openwisp2/sample_firmware_upgrader/migrations/0001_initial.py:61-67
Timestamp: 2026-02-23T21:36:22.028Z
Learning: In the sample_firmware_upgrader app within tests, avoid creating new migration files for small changes since the sample is for demonstration and testing. Review migrations with this context: the sample's initial migration may differ from the main app and need not be strictly aligned. If you change models for the demo, keep migrations minimal and document why divergence exists; don’t require parity with the main app migrations in PRs focused on the sample.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
📚 Learning: 2026-06-15T20:19:28.326Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 437
File: openwisp_firmware_upgrader/tests/test_admin.py:1072-1147
Timestamp: 2026-06-15T20:19:28.326Z
Learning: In this codebase (OpenWISP projects, including openwisp-firmware-upgrader), prefer test methods that are fully self-contained: each test should do its own setup and make its own explicit assertions within the test body. Avoid introducing shared assertion helpers or parameterized helper methods that abstract away assertions across tests, even if it leads to some duplication.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
📚 Learning: 2026-02-24T00:04:04.187Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-options.css:11-13
Timestamp: 2026-02-24T00:04:04.187Z
Learning: In openwisp-firmware-upgrader, prefer hiding empty submit rows via CSS (e.g., `#upgradeoperation_form` .submit-row { display: none; }) rather than removing elements in templates. This keeps template logic simple and can improve rendering performance. Apply this pattern to CSS files in openwisp_firmware_upgrader/static/firmware-upgrader/css/*.css for consistent behavior across upgrade-option related styles.

Applied to files:

  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-selected-confirmation.css
  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-progress.css
📚 Learning: 2026-03-07T01:16:15.471Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:15.471Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. For the UpgradeOperationAdmin.change_view, model-level has_view_permission(request) suffices to show breadcrumbs for batch, and object-level permission checks are not required.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
🪛 ast-grep (0.43.0)
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py

[info] 62-73: use help_text to document model columns
Context: models.CharField(
choices=[
("in-progress", "in progress"),
("success", "success"),
("failed", "failed"),
("cancelled", "cancelled"),
("aborted", "aborted"),
("pending", "pending"),
],
default="in-progress",
max_length=12,
)
Note: Security best practice.

(model-help-text)

tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py

[info] 62-73: use help_text to document model columns
Context: models.CharField(
choices=[
("in-progress", "in progress"),
("success", "success"),
("failed", "failed"),
("cancelled", "cancelled"),
("aborted", "aborted"),
("pending", "pending"),
],
default="in-progress",
max_length=12,
)
Note: Security best practice.

(model-help-text)

openwisp_firmware_upgrader/tasks.py

[info] 164-164: use secrets package over random package
Context: random.uniform(0, jitter)
Note: [CWE-330].

(avoid-random-python)

openwisp_firmware_upgrader/base/models.py

[info] 1061-1061: use secrets package over random package
Context: random.uniform(1 - jitter, 1 + jitter)
Note: [CWE-330].

(avoid-random-python)


[info] 1081-1081: use secrets package over random package
Context: random.uniform(0, jitter)
Note: [CWE-330].

(avoid-random-python)

🔇 Additional comments (32)
openwisp_firmware_upgrader/tests/test_models.py (3)

10-22: LGTM!


593-988: LGTM!


1067-1076: LGTM!

openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js (1)

10-10: LGTM!

Also applies to: 20-20, 35-35, 63-69, 86-86, 99-103

openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js (1)

252-255: LGTM!

openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js (1)

203-216: LGTM!

openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-progress.css (1)

52-55: LGTM!

openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-selected-confirmation.css (1)

22-42: LGTM!

openwisp_firmware_upgrader/websockets.py (1)

254-260: LGTM!

Also applies to: 268-269, 336-337, 506-513, 525-525

docs/index.rst (1)

42-42: LGTM!

docs/user/intro.rst (1)

14-16: LGTM!

openwisp_firmware_upgrader/admin.py (6)

128-139: LGTM!


227-239: LGTM!

Also applies to: 248-254


397-407: LGTM!


413-444: LGTM!


517-528: LGTM!

Also applies to: 538-555


807-812: LGTM!

openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html (1)

82-85: LGTM!

openwisp_firmware_upgrader/api/serializers.py (1)

86-96: LGTM!

Also applies to: 170-180

openwisp_firmware_upgrader/api/views.py (1)

98-107: LGTM!

openwisp_firmware_upgrader/tests/test_admin.py (1)

203-283: LGTM!

Also applies to: 2131-2177

openwisp_firmware_upgrader/tests/test_api.py (1)

10-10: LGTM!

Also applies to: 672-695, 1045-1065, 1949-1973, 2167-2229

openwisp_firmware_upgrader/tests/test_websockets.py (1)

403-409: LGTM!

Also applies to: 705-787

openwisp_firmware_upgrader/tests/test_selenium.py (1)

278-311: LGTM!

Also applies to: 378-411, 1062-1135

openwisp_firmware_upgrader/settings.py (1)

27-40: LGTM!

openwisp_firmware_upgrader/apps.py (1)

3-3: LGTM!

Also applies to: 30-35, 65-97, 128-132, 160-180

openwisp_firmware_upgrader/tasks.py (1)

2-13: LGTM!

Also applies to: 35-46, 115-205

openwisp_firmware_upgrader/tests/base.py (1)

2-2: LGTM!

Also applies to: 11-11, 27-33

openwisp_firmware_upgrader/tests/test_tasks.py (1)

1-14: LGTM!

Also applies to: 64-74, 85-271

openwisp_firmware_upgrader/tests/test_handlers.py (1)

1-163: LGTM!

openwisp_firmware_upgrader/tests/test_notifications.py (1)

1-209: LGTM!

tests/openwisp2/settings.py (1)

3-3: LGTM!

Also applies to: 191-201

Comment on lines +115 to +145
read_only_fields = ("retry_count", "next_retry_at")

def update(self, instance, validated_data):
if "is_persistent" in validated_data:
raise serializers.ValidationError(
{
"is_persistent": _(
"is_persistent cannot be changed after the upgrade "
"operation has been saved."
)
}
)
return super().update(instance, validated_data)


class DeviceUpgradeOperationSerializer(serializers.ModelSerializer):
class Meta:
model = UpgradeOperation
fields = ("id", "device", "image", "status", "log", "progress", "modified")
fields = (
"id",
"device",
"image",
"is_persistent",
"retry_count",
"next_retry_at",
"status",
"log",
"progress",
"modified",
)
read_only_fields = ("retry_count", "next_retry_at")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect operation serializers and their API view usage without executing repository code.
# Expected: if update-capable views use these serializers, task-owned fields are read-only
# and both serializers reject `is_persistent` updates.

rg -n -C4 --type=py \
  'class .*UpgradeOperation.*(View|ViewSet)|serializer_class = (UpgradeOperationSerializer|DeviceUpgradeOperationSerializer)' \
  openwisp_firmware_upgrader/api/views.py

rg -n -C8 --type=py \
  'class (UpgradeOperationSerializer|DeviceUpgradeOperationSerializer)|read_only_fields|def update' \
  openwisp_firmware_upgrader/api/serializers.py

Repository: openwisp/openwisp-firmware-upgrader

Length of output: 6955


🏁 Script executed:

rg -n --type=py 'UpgradeOperationSerializer|DeviceUpgradeOperationSerializer' openwisp_firmware_upgrader/

Repository: openwisp/openwisp-firmware-upgrader

Length of output: 3086


🏁 Script executed:

rg -n --type=py 'class (UpgradeOperationListView|UpgradeOperationDetailView|DeviceUpgradeOperationListView|DeviceUpgradeOperationDetailView)' -A 10 openwisp_firmware_upgrader/api/views.py

Repository: openwisp/openwisp-firmware-upgrader

Length of output: 1423


Add is_persistent update guard to DeviceUpgradeOperationSerializer for consistency.

UpgradeOperationSerializer guards against is_persistent changes via its update() method, but DeviceUpgradeOperationSerializer lacks this protection despite exposing the same field. While current API views are read-only (ListAPIView, RetrieveAPIView), add the guard for consistency and to prevent unintended mutation if update-capable endpoints are added in the future. Also consider explicitly marking status, log, and progress as read-only across both serializers since these are worker-controlled fields.

🤖 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 `@openwisp_firmware_upgrader/api/serializers.py` around lines 115 - 145, Add an
update() method to DeviceUpgradeOperationSerializer that prevents is_persistent
from being modified after creation, mirroring the existing protection in
UpgradeOperationSerializer's update() method. Additionally, add status, log, and
progress to the read_only_fields tuple in the Meta class of both
UpgradeOperationSerializer and DeviceUpgradeOperationSerializer to explicitly
mark these worker-controlled fields as immutable through the API.

Comment on lines 1115 to 1116
self.log_line(f"Detected a recoverable failure: {cause}.\n", save=False)
self.log_line("The upgrade operation will be retried soon.")

Copy link
Copy Markdown

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

Mark recoverable-retry log lines for translation.

Line 1115 and Line 1116 add user-facing log messages without Django i18n wrapping, so they won’t be extracted for localization.

💡 Suggested fix
-            self.log_line(f"Detected a recoverable failure: {cause}.\n", save=False)
-            self.log_line("The upgrade operation will be retried soon.")
+            self.log_line(
+                _("Detected a recoverable failure: %(cause)s.") % {"cause": cause},
+                save=False,
+            )
+            self.log_line(_("The upgrade operation will be retried soon."))

As per coding guidelines, for Django pull requests all user-facing strings must be marked as translatable using Django i18n helpers.

📝 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
self.log_line(f"Detected a recoverable failure: {cause}.\n", save=False)
self.log_line("The upgrade operation will be retried soon.")
self.log_line(
_("Detected a recoverable failure: %(cause)s.") % {"cause": cause},
save=False,
)
self.log_line(_("The upgrade operation will be retried soon."))
🤖 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 `@openwisp_firmware_upgrader/base/models.py` around lines 1115 - 1116, The
user-facing strings in the two self.log_line calls are not wrapped with Django
i18n translation helpers, preventing them from being extracted for localization.
Wrap both string arguments passed to self.log_line calls with Django's gettext
translation function (typically imported as underscore or gettext_lazy). For the
f-string containing the cause variable on line 1115, ensure the translation
function wraps the entire f-string expression, and do the same for the plain
string literal on line 1116 to mark both messages as translatable.

Source: Coding guidelines

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The images have to be moved to the docs branch:
https://github.com/openwisp/openwisp-firmware-upgrader/tree/docs/docs/images

Also, 1.3 is openwisp 26, your work will end up in 1.4 (openwisp 27)

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I found a few outstanding issues that need to be addressed before merging.

Comment thread docs/user/persistent-mass-upgrades.rst Outdated
"or the operation is cancelled"
),
)
last_reminder_at = models.DateTimeField(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This field is used by send_pending_upgrade_reminders() to find due batches on every Beat run, but it is not indexed. On installations with many historical mass upgrades this can become a recurring scan before the pending-child join. I agree with CodeRabbit here: please add db_index=True on the model field and mirror it in 0019_batchupgradeoperation_last_reminder_at.py and the sample migration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added db_index=True on last_reminder_at and mirrored it in 0019 and the sample 0006 migration.

Comment thread openwisp_firmware_upgrader/api/serializers.py Outdated
@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch 2 times, most recently from ff4d389 to a6f27f0 Compare June 23, 2026 18:35
name="last_reminder_at",
field=models.DateTimeField(
blank=True,
db_index=True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: last_reminder_at is added with db_index=True, so Django emits a blocking CREATE INDEX (no CONCURRENTLY) on the populated batchupgradeoperation table during deploy.

This is the same class of deploy-time lock already flagged on next_retry_at (migration 0018, line 45). The AddField itself is safe (null=True, no backfill), but on a large table the index build takes a brief ACCESS EXCLUSIVE lock. Consider running this migration during a maintenance window, or building the index concurrently via a separate RunSQL operation (Django cannot emit CONCURRENTLY from db_index).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from a6f27f0 to b6f5543 Compare June 23, 2026 19:30
@openwisp-companion

Copy link
Copy Markdown

Test Failure in Firmware Upgrader

Hello @Eeshu-Yadav,
(Analysis for commit b6f5543)

There is one test failure:

  • Test Failure: The test test_upgrader_with_unsupported_upgrade_options in openwisp_firmware_upgrader.tests.test_selenium failed because it expected 1 UpgradeOperation with empty upgrade_options but found 0.

  • Fix: Review the test setup and the UpgradeOperation creation logic within test_upgrader_with_unsupported_upgrade_options to ensure that an UpgradeOperation with empty upgrade_options is correctly created and persisted before the assertion.

@Eeshu-Yadav Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from b6f5543 to 8c60dc3 Compare June 23, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request python Pull requests that update Python code

Projects

Development

Successfully merging this pull request may close these issues.

3 participants