-
-
Notifications
You must be signed in to change notification settings - Fork 50
[gsoc26] Mass command blog #250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,280 @@ | ||||||||||||||||
| GSoC 2026: Mass Commands: Run Shell Commands on Many Devices at Once | ||||||||||||||||
| ==================================================================== | ||||||||||||||||
|
|
||||||||||||||||
| :date: 2026-08-31 | ||||||||||||||||
| :author: Deepanshu Sahu | ||||||||||||||||
| :tags: gsoc, openwisp-controller, new-features | ||||||||||||||||
| :category: gsoc | ||||||||||||||||
| :lang: en | ||||||||||||||||
|
|
||||||||||||||||
| .. | ||||||||||||||||
| TODO: add the cover image of the project and the related metadata | ||||||||||||||||
| (:image_url:, :image_width:, :image_height: fields above), e.g. | ||||||||||||||||
| images/blog/gsoc26/mass-commands/gsoc-26-mass-commands.png | ||||||||||||||||
|
|
||||||||||||||||
| .. | ||||||||||||||||
| TODO: cover image | ||||||||||||||||
| .. image:: {static}/images/blog/gsoc26/mass-commands/gsoc-26-mass-commands.png | ||||||||||||||||
| :alt: Google Summer of Code, Mass Commands in OpenWISP | ||||||||||||||||
| :align: center | ||||||||||||||||
|
|
||||||||||||||||
| Participating in Google Summer of Code for a second time has been an | ||||||||||||||||
| equally rewarding experience. Over the past three months, I worked on my | ||||||||||||||||
| Google Summer of Code project with OpenWISP, where I had the opportunity | ||||||||||||||||
| to enhance the platform by bringing mass command execution to it. With the | ||||||||||||||||
| constant guidance, encouragement, and expertise of my mentors `Federico | ||||||||||||||||
| Capoano (nemesifier) <https://github.com/nemesifier>`_ and `Gagan Deep | ||||||||||||||||
| (pandafy) <https://github.com/pandafy>`_. I was able to explore new parts | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Fix the sentence fragment in the introduction. The sentence ends after the dependent 🤖 Prompt for AI Agents |
||||||||||||||||
| of the stack, tackle complex problems, and deliver features that let | ||||||||||||||||
| operators manage large networks with a single operation instead of | ||||||||||||||||
| repeating the same task device by device. Their insightful feedback, | ||||||||||||||||
| patience, and supportive mentorship played a crucial role in helping me | ||||||||||||||||
| grow as a developer and a contributor to open-source projects. | ||||||||||||||||
|
|
||||||||||||||||
| About the Project | ||||||||||||||||
| ----------------- | ||||||||||||||||
|
|
||||||||||||||||
| .. | ||||||||||||||||
| TODO: embed the final demo video of the project | ||||||||||||||||
| .. raw:: html | ||||||||||||||||
|
|
||||||||||||||||
| <iframe width="560" height="315" | ||||||||||||||||
| style="width:100%; height:700px;" | ||||||||||||||||
| src="https://www.youtube.com/embed/VIDEO_ID?vq=hd1080" | ||||||||||||||||
| title="OpenWISP Mass Commands demo" | ||||||||||||||||
| frameborder="0" | ||||||||||||||||
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" | ||||||||||||||||
| referrerpolicy="strict-origin-when-cross-origin" | ||||||||||||||||
| allowfullscreen> | ||||||||||||||||
| </iframe> | ||||||||||||||||
|
|
||||||||||||||||
| Running the same command on many devices used to be a manual task in | ||||||||||||||||
| OpenWISP: an operator had to open each device page and trigger the command | ||||||||||||||||
| one device at a time. That is acceptable for a handful of routers, but it | ||||||||||||||||
| does not scale to a real network with hundreds or thousands of devices. | ||||||||||||||||
|
|
||||||||||||||||
| The project makes this a single operation. Operators can now target | ||||||||||||||||
| devices by organization, device group and location, or pick them one by | ||||||||||||||||
| one from the device list, review the exact set of devices which will be | ||||||||||||||||
| affected, and then execute the command on all of them in the background, | ||||||||||||||||
| while following the progress of every single device in real time. | ||||||||||||||||
|
|
||||||||||||||||
| The work adds a set of enhancements to `openwisp-controller | ||||||||||||||||
| <https://github.com/openwisp/openwisp-controller>`_: a new model with an | ||||||||||||||||
| asynchronous execution pipeline, REST API endpoints, a multi step Django | ||||||||||||||||
| admin workflow, a WebSocket endpoint for real-time updates, and the | ||||||||||||||||
| documentation which ties everything together. | ||||||||||||||||
|
|
||||||||||||||||
| Building Mass Commands | ||||||||||||||||
| ---------------------- | ||||||||||||||||
|
|
||||||||||||||||
| The feature was built incrementally, one piece at a time. The data model | ||||||||||||||||
| and the REST API came first, so that a mass command could be created and | ||||||||||||||||
| executed in the background. The admin workflow was built on top of them, | ||||||||||||||||
| adding a guided way to compose a command, review the devices it will reach | ||||||||||||||||
| and follow the results as they arrive. The last piece was the entry point | ||||||||||||||||
| from the device list, which lets operators hand a manual selection of | ||||||||||||||||
| devices over to the very same workflow. | ||||||||||||||||
|
|
||||||||||||||||
| .. | ||||||||||||||||
| TODO: add a before/after or overview screenshot of the mass command | ||||||||||||||||
| workflow, e.g. | ||||||||||||||||
| images/blog/gsoc26/mass-commands/mass-command-overview.png | ||||||||||||||||
|
|
||||||||||||||||
| Features Implemented | ||||||||||||||||
| -------------------- | ||||||||||||||||
|
|
||||||||||||||||
| Mass Command Model and REST API | ||||||||||||||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||||||||||||
|
|
||||||||||||||||
| A new ``BatchCommand`` model keeps track of every mass command: its | ||||||||||||||||
| organization, status (*idle*, *in-progress*, *success*, *failed*), command | ||||||||||||||||
| type and input, the targets which were used, a label and optional notes, | ||||||||||||||||
| the devices it affects and the devices which had to be skipped. Each | ||||||||||||||||
| individual ``Command`` is linked back to the batch it belongs to, so the | ||||||||||||||||
| existing command execution machinery is reused as is. | ||||||||||||||||
|
|
||||||||||||||||
| The execution is fully asynchronous: the request creates the | ||||||||||||||||
| ``BatchCommand``, a Celery task resolves the targets, creates one | ||||||||||||||||
| ``Command`` per device, keeps the aggregated status of the batch updated | ||||||||||||||||
| and enqueues the individual commands, which are then executed over SSH. | ||||||||||||||||
|
Comment on lines
+98
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Describe command creation and skipped-device recording separately. These lines state that the task creates one Proposed wording-``Command`` per device, keeps the aggregated status of the batch updated
-and enqueues the individual commands, which are then executed over SSH.
+``Command`` per eligible device, records skipped devices separately, keeps
+the aggregated status of the batch updated, and enqueues the individual
+commands, which are then executed over SSH.📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||
|
|
||||||||||||||||
| Three REST API endpoints are available: | ||||||||||||||||
|
|
||||||||||||||||
| - ``POST /api/v1/controller/batch-command/execute/`` starts a mass command | ||||||||||||||||
| and returns the identifier of the batch; | ||||||||||||||||
| - ``GET /api/v1/controller/batch-command/execute/`` performs a dry run, | ||||||||||||||||
| returning the devices which would be affected without executing | ||||||||||||||||
| anything; | ||||||||||||||||
| - ``GET /api/v1/controller/batch-command/{id}/`` returns the status of a | ||||||||||||||||
| batch, the number of affected devices and the skipped devices with the | ||||||||||||||||
| reason why they were skipped. | ||||||||||||||||
|
|
||||||||||||||||
| Admin Workflow | ||||||||||||||||
| ~~~~~~~~~~~~~~ | ||||||||||||||||
|
|
||||||||||||||||
| .. | ||||||||||||||||
| TODO: add a GIF of the two step admin workflow, e.g. | ||||||||||||||||
| images/blog/gsoc26/mass-commands/mass-command-execute.gif | ||||||||||||||||
|
|
||||||||||||||||
| Mass commands can be sent from the browser through *Network Operations* > | ||||||||||||||||
| *Mass command execute*. The workflow is divided in two steps. | ||||||||||||||||
|
|
||||||||||||||||
| The first step collects the command type and its inputs, which change | ||||||||||||||||
| according to the type selected, a label to identify the mass command | ||||||||||||||||
| later, optional notes, and the targets: organization, device group and | ||||||||||||||||
| location. Using more than one target narrows the selection, so a group and | ||||||||||||||||
| a location together match only the devices which are in that group *and* | ||||||||||||||||
| at that location. Superusers can leave every target empty to reach all the | ||||||||||||||||
| devices of the system, while other users must choose at least one target | ||||||||||||||||
| and only see the command types enabled for their organizations. | ||||||||||||||||
|
|
||||||||||||||||
| The second step shows a summary of the command together with the list of | ||||||||||||||||
| the matched devices, built with the same dry run logic used by the API. | ||||||||||||||||
| Devices can be left out by unchecking them, the counter and the *Execute | ||||||||||||||||
| on N devices* button follow the selection while paging through the list, | ||||||||||||||||
| and going back keeps the form filled in. The device table is composed at | ||||||||||||||||
| request time from the device admin, so columns added by other modules such | ||||||||||||||||
| as `openwisp-monitoring | ||||||||||||||||
| <https://github.com/openwisp/openwisp-monitoring>`_ are shown here as | ||||||||||||||||
| well. | ||||||||||||||||
|
|
||||||||||||||||
| Real-Time Monitoring | ||||||||||||||||
| ~~~~~~~~~~~~~~~~~~~~ | ||||||||||||||||
|
|
||||||||||||||||
| .. | ||||||||||||||||
| TODO: add a GIF showing the results page updating in real time, e.g. | ||||||||||||||||
| images/blog/gsoc26/mass-commands/mass-command-realtime.gif | ||||||||||||||||
|
|
||||||||||||||||
| Once the mass command starts, the operator lands on the mass command page, | ||||||||||||||||
| which shows the status of the batch, how many devices are affected, the | ||||||||||||||||
| skipped devices and one row per device with its status, output and last | ||||||||||||||||
| modification time. | ||||||||||||||||
|
|
||||||||||||||||
| The page is updated in real time through a new WebSocket endpoint, | ||||||||||||||||
| ``ws/controller/batch-command/<uuid:pk>``, so there is no need to reload | ||||||||||||||||
| it to follow a rollout which affects many devices. The consumer only | ||||||||||||||||
| accepts authorized superusers and staff users who manage the organization | ||||||||||||||||
| of the mass command, and pushes two kinds of messages: one for the status | ||||||||||||||||
| of the batch and one for the result of each device. A client which | ||||||||||||||||
| connects while a mass command is already running can also request the | ||||||||||||||||
| current state and receive the results it missed, one page at a time. | ||||||||||||||||
|
|
||||||||||||||||
| The results table can be searched by device name and filtered by status, | ||||||||||||||||
| device group and location, and by organization for superusers, which makes | ||||||||||||||||
| it easy to isolate the devices which failed in a large rollout. | ||||||||||||||||
|
|
||||||||||||||||
| Execution from the Device List | ||||||||||||||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||||||||||||
|
|
||||||||||||||||
| .. | ||||||||||||||||
| TODO: add a GIF of the "Execute mass command" admin action on the | ||||||||||||||||
| device changelist, e.g. | ||||||||||||||||
| images/blog/gsoc26/mass-commands/mass-command-changelist-action.gif | ||||||||||||||||
|
|
||||||||||||||||
| Targeting by organization, group and location covers most of the common | ||||||||||||||||
| cases, but sometimes an operator simply knows which devices need the | ||||||||||||||||
| command. For this reason an *Execute mass command* action was added to the | ||||||||||||||||
| device list: the selected devices are handed over to the same two step | ||||||||||||||||
| workflow, with the organization prefilled and the target fields hidden, | ||||||||||||||||
| and they can still be excluded individually in the confirmation step. | ||||||||||||||||
|
|
||||||||||||||||
| The action is only available to users who have the permission to add mass | ||||||||||||||||
| commands, and selections spanning multiple organizations are rejected, | ||||||||||||||||
| unless the command is a system wide one. | ||||||||||||||||
|
|
||||||||||||||||
| Skipped Devices | ||||||||||||||||
| ~~~~~~~~~~~~~~~ | ||||||||||||||||
|
|
||||||||||||||||
| A device is skipped when the command cannot be created for it, for example | ||||||||||||||||
| when the device has no access credentials, or when the command type is not | ||||||||||||||||
| enabled for its organization. Skipped devices are never silently dropped: | ||||||||||||||||
| they are counted in the batch, listed in the results table with the | ||||||||||||||||
| *skipped* status and the reason as their output, and they can be isolated | ||||||||||||||||
| with the status filter. This way it is always clear why a device was not | ||||||||||||||||
| reached. | ||||||||||||||||
|
|
||||||||||||||||
| Current State | ||||||||||||||||
| ------------- | ||||||||||||||||
|
|
||||||||||||||||
| The model and the REST API have been merged into `openwisp-controller | ||||||||||||||||
| <https://github.com/openwisp/openwisp-controller>`_, while the admin | ||||||||||||||||
| workflow and the device list entry point are currently under review. The | ||||||||||||||||
| feature is documented in the official documentation, both for the admin | ||||||||||||||||
| workflow and for the REST and WebSocket APIs. | ||||||||||||||||
|
Comment on lines
+200
to
+204
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- knowledge files ---'
find /tmp/coderabbit-repo-knowledge/openwisp-openwisp-website-4735e992 \
-maxdepth 2 -type f -name '*.md' -print | sort
printf '%s\n' '--- guidance headers ---'
for f in /tmp/coderabbit-repo-knowledge/openwisp-openwisp-website-4735e992/*/*.md; do
[ -f "$f" ] || continue
printf '\n### %s\n' "$f"
head -5 "$f"
done
printf '%s\n' '--- target article ---'
cat -n content/blog/gsoc26-mass-command.rst | sed -n '180,212p'
printf '%s\n' '--- target diff/stat ---'
git diff --stat -- content/blog/gsoc26-mass-command.rst
git diff -- content/blog/gsoc26-mass-command.rst | sed -n '1,180p'
printf '%s\n' '--- local references ---'
rg -n -i --glob '*.rst' --glob '*.md' --glob '*.py' \
'batch-command|mass command|mass-command|openwisp-controller' .Repository: openwisp/OpenWISP-Website Length of output: 10697 🌐 Web query:
💡 Result: The term "Mass Command" in the context of REST APIs and data models refers to a batch execution system, specifically within the OpenWISP Controller framework [1]. It is not a standard term for an artificial intelligence model [1]. In the OpenWISP implementation, the Mass Command system allows for the asynchronous execution of commands across multiple network devices [1]. Key technical components include: 1. Data Model: A BatchCommand model acts as the parent object, which links to individual Command rows via a foreign key [1]. This architecture enables the management of organization-scoped operations across groups of devices, locations, or manual selections [1]. 2. REST API: The system exposes specific API endpoints to facilitate mass operations, including: - A /api/v1/controller/batch-command/execute/ route for triggering operations [1]. - API serializers that validate organization and device consistency [1]. - Specialized views (e.g., BatchCommandExecuteView) that support both POST requests for creating batch executions and GET requests for dry-run/previewing operations [1]. 3. Processing: The architecture utilizes a Celery-based task queue (launch_batch_command) to handle the asynchronous execution of these batch-scheduled tasks after the API validates and commits the transaction [1]. Citations:
🌐 Web query:
💡 Result: I couldn’t find an indexed match for Citations: 🌐 Web query:
💡 Result: No matching pages were found on 🏁 Script executed: #!/bin/bash
set -eu
api='https://api.github.com/repos/openwisp/openwisp-controller'
printf '%s\n' '--- pull request status ---'
for n in 1395 1420 1462; do
curl -fsSL "$api/pulls/$n" |
jq -c '{number, title, state, merged, merged_at, base: .base.ref, head: .head.ref, html_url}'
done
printf '%s\n' '--- master API URLs ---'
curl -fsSL \
'https://raw.githubusercontent.com/openwisp/openwisp-controller/master/openwisp_controller/connection/api/urls.py' |
nl -ba | grep -i -C 4 'batch\|command' || true
printf '%s\n' '--- controller repository tree entries related to docs and batch commands ---'
curl -fsSL "$api/git/trees/master?recursive=1" |
jq -r '.tree[] | select(.path | test("(^|/)(docs?|documentation)(/|$)|batch.?command|mass.?command"; "i")) | .path' |
sed -n '1,160p'
printf '%s\n' '--- official docs repository candidates ---'
for repo in openwisp/openwisp-docs openwisp/openwisp-controller; do
printf '\n## %s\n' "$repo"
curl -fsSL "https://api.github.com/repos/$repo/git/trees/master?recursive=1" |
jq -r '.tree[] | select(.path | test("mass.?command|batch.?command|controller.*development|development.*controller"; "i")) | .path' |
sed -n '1,120p'
doneRepository: openwisp/OpenWISP-Website Length of output: 2244 🏁 Script executed: #!/bin/bash
set -eu
api='https://api.github.com/repos/openwisp/openwisp-controller'
printf '%s\n' '--- master API URL file ---'
master_url='https://raw.githubusercontent.com/openwisp/openwisp-controller/master/openwisp_controller/connection/api/urls.py'
curl -fsSL "$master_url" | grep -in -C 4 'batch\|command' || printf '%s\n' 'no batch/command route text found'
printf '%s\n' '--- gsoc26-mass-commands API URL file ---'
branch_url='https://raw.githubusercontent.com/openwisp/openwisp-controller/gsoc26-mass-commands/openwisp_controller/connection/api/urls.py'
curl -fsSL "$branch_url" | grep -in -C 4 'batch\|command' || printf '%s\n' 'no batch/command route text found'
printf '%s\n' '--- PR 1395 changed files ---'
curl -fsSL "$api/pulls/1395/files?per_page=100" |
jq -r '.[] | [.status, .filename] | `@tsv`' |
grep -Ei 'docs|url|batch|command|websocket' || true
printf '%s\n' '--- branch documentation paths ---'
curl -fsSL "$api/git/trees/gsoc26-mass-commands?recursive=1" |
jq -r '.tree[] | select(.path | test("docs/|mass.?command|batch.?command"; "i")) | .path' |
sed -n '1,180p'Repository: openwisp/OpenWISP-Website Length of output: 3529 State the feature branch explicitly. PR 1395 merged into 🤖 Prompt for AI AgentsSources: Path instructions, MCP tools |
||||||||||||||||
|
|
||||||||||||||||
| You can follow the development process and explore the implementation | ||||||||||||||||
| details in the following pull requests: | ||||||||||||||||
|
|
||||||||||||||||
| - `Mass Command model and REST APIs for async command execution | ||||||||||||||||
| <https://github.com/openwisp/openwisp-controller/pull/1395>`_ (merged) | ||||||||||||||||
| - `Django admin workflow for mass commands with real-time monitoring | ||||||||||||||||
| <https://github.com/openwisp/openwisp-controller/pull/1420>`_ | ||||||||||||||||
| - `Mass command execution from the device changelist selection | ||||||||||||||||
| <https://github.com/openwisp/openwisp-controller/pull/1462>`_ | ||||||||||||||||
|
|
||||||||||||||||
| My Experience | ||||||||||||||||
| ------------- | ||||||||||||||||
|
|
||||||||||||||||
| Last year's GSoC with OpenWISP was an amazing experience, and being able | ||||||||||||||||
| to do it a second time has been an absolute pleasure. Coming back to a | ||||||||||||||||
| codebase and a community I already knew allowed me to move faster, focus | ||||||||||||||||
| on the design decisions which really mattered, and take on a project which | ||||||||||||||||
| touches the whole stack, from the data model up to the user interface. | ||||||||||||||||
|
|
||||||||||||||||
| Working again with `Federico Capoano (nemesifier) | ||||||||||||||||
| <https://github.com/nemesifier>`_ and `Gagan Deep (pandafy) | ||||||||||||||||
| <https://github.com/pandafy>`_ has been the most valuable part of the | ||||||||||||||||
| program. Their reviews were detailed and demanding in the best possible | ||||||||||||||||
| way: they pushed me to think about multi-tenancy and permissions, database | ||||||||||||||||
| query efficiency, idempotency of background tasks, and the small details | ||||||||||||||||
| of the user experience which make a feature actually usable in production. | ||||||||||||||||
| I learned a lot along the way, and I am well aware that there is still a | ||||||||||||||||
| lot left to learn. | ||||||||||||||||
|
|
||||||||||||||||
| The hardest parts were making the confirmation step reuse the device | ||||||||||||||||
| admin, keeping the state of the wizard consistent across steps and entry | ||||||||||||||||
| points, and making the real-time updates reliable without flooding the | ||||||||||||||||
| browser or the database. Solving these problems taught me how to break a | ||||||||||||||||
| large feature into pieces which can be reviewed and merged one at a time. | ||||||||||||||||
|
|
||||||||||||||||
| Beyond the code, taking part in the discussions of the community was once | ||||||||||||||||
| again something I genuinely enjoyed. I am looking forward to staying | ||||||||||||||||
| around and helping the community more in the coming years. | ||||||||||||||||
|
|
||||||||||||||||
| What's Next? | ||||||||||||||||
| ------------ | ||||||||||||||||
|
|
||||||||||||||||
| The next step is making mass commands more resilient to failure cases. A | ||||||||||||||||
| mass command runs on many devices at once, so failures are not the | ||||||||||||||||
| exception but a normal part of a large rollout: devices which are offline, | ||||||||||||||||
| connections which time out, background tasks which are interrupted while a | ||||||||||||||||
| batch is running. I want the feature to recover from these situations on | ||||||||||||||||
| its own and always leave the batch in a consistent state, so that an | ||||||||||||||||
| operator can trust the result of a rollout without checking device by | ||||||||||||||||
| device. | ||||||||||||||||
|
|
||||||||||||||||
| My goal is to get all the features of this project merged into master, so | ||||||||||||||||
| that mass commands feature can ship completely in the next release of | ||||||||||||||||
| openwisp next year. | ||||||||||||||||
|
|
||||||||||||||||
| Another extension of this work is the command model itself, together with | ||||||||||||||||
| its admin view. The way recent commands are exposed today comes with a | ||||||||||||||||
| number of limitations, and now that commands are also created in bulk | ||||||||||||||||
| these limitations are more visible. Reworking the model and giving it a | ||||||||||||||||
| proper admin view would make it much easier to look up what was executed | ||||||||||||||||
| on a device, when, and with which result, both for single and mass | ||||||||||||||||
| commands. | ||||||||||||||||
|
|
||||||||||||||||
| Finally, this work can benefit other modules. Mass commands are built on | ||||||||||||||||
| the same idea of batch execution used by the `firmware upgrader | ||||||||||||||||
| <https://github.com/openwisp/openwisp-firmware-upgrader>`_, and the two | ||||||||||||||||
| can converge: what was learned here about targeting devices, tracking a | ||||||||||||||||
| batch and following its progress in real time can be applied there to make | ||||||||||||||||
| batch upgrades more resilient as well. | ||||||||||||||||
|
|
||||||||||||||||
| Beyond this project, I plan to keep maintaining what I built during both | ||||||||||||||||
| editions of the program, fixing bugs, reviewing contributions and helping | ||||||||||||||||
| new contributors onboard. Open source has given me a lot over these two | ||||||||||||||||
| years, and I intend to keep giving back to the OpenWISP community for a | ||||||||||||||||
| long time. | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Complete the media placeholders before publication.
The cover image, demo video, screenshots, and GIFs are still commented TODO blocks. The video also uses
VIDEO_ID. Replace these placeholders with the final assets before publishing the post.Also applies to: 37-49, 79-82, 116-118, 145-147, 170-173
🤖 Prompt for AI Agents