majesticdev / commandnet-plugin
Personnel & unit management system built for our community.
Package info
github.com/Spearhead-Gaming/commandnet-plugin
Type:forumify-plugin
pkg:composer/majesticdev/commandnet-plugin
Requires
- php: >=8.4
- forumify/forumify-platform: ^1.3
Requires (Dev)
- dama/doctrine-test-bundle: ^8.0
- dg/bypass-finals: ^1.9
- phpstan/phpstan: ^2.1
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.5
- slevomat/coding-standard: ^8.15
- squizlabs/php_codesniffer: ^3.13
- symfony/browser-kit: ^7.4
- symfony/css-selector: ^7.4
- zenstruck/foundry: ^2.8
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-02 04:39:51 UTC
README
Personnel & unit management for a MILSIM Arma 3 community, with one unified service-record timeline instead of history scattered across modules.
A forumify plugin for personnel and unit management, built to be compared against forumify-milhq-plugin: ranks (in promotion tracks), positions, specialties, equipment, assignments, awards, qualifications and operations with RSVP, attendance and after-action reports, plus enlistment, discharge, forms, courses, documents, promotions, Report In and AWOL detection. Everything a soldier does feeds a single append-only service-record timeline on their personnel file, rather than each module keeping its own history.
Built for a specific MILSIM community's Forumify install; not a general-purpose skeleton.
Table of contents
Overview & setup
Entities & data model
Frontend routes
Admin & permissions
Feature modules
- Rosters
- Squads and unit self-service
- Courses
- Forms
- Documents
- Equipment
- Specialties
- Enlistment
- Discharge
- Promotions
- Events and patrols
- AWOL detection
- Report In
Design & status
Exports
Integrations
π§© Requirements
- PHP 8.4 or newer
- A Forumify 1.3.x install
- MySQL (for the migrations in
migrations/) - The Symfony scheduler running, for the daily Report In check (it does nothing until enabled)
Optional, picked up automatically when installed:
- The Forumify calendar plugin: operations are mirrored onto a community calendar.
- The Discord plugin (
MajesticDev\Discord): slash commands, and mapping the forumify roles this plugin grants to Discord roles. See Integrations below.
π¦ Install
composer require majesticdev/commandnet-plugin
Then, from the Forumify install:
bin/console forumify:plugins:refresh bin/console doctrine:migrations:migrate
Upgrading to 1.1.5 adds the unit_position, squad and squad_position tables and an
optional assignment.squad_id, so run the migrations. It also adds the
command-net.units.manage_own permission; Forumify grants a new permission to no role, so give it
to the roles your unit commanders hold before expecting My Units to appear.
ποΈ Entities
Show the full entity table
| Entity | Notes |
|---|---|
SoldierProfile |
1:1 with the Forumify User, kept separate so the plugin can be removed cleanly. Rank, specialty, service number, callsign, status (active, LOA, AWOL, discharged, retired), enlistment/discharge dates. |
Unit |
Self-referencing tree (parent/children), sortable, optional commander and forumify role, optional vehicles, and the positions (billets) it is expected to hold. |
Squad |
A squad or team within a Unit; squads nest, so a team is a squad inside a squad. Carries a name and its positions, and none of a unit's command-specific fields (commander, insignia, role, vehicles). |
Roster |
A named, sortable set of units shown as a tab on the roster page. |
Rank, RankGroup |
Ranks are a sortable ladder with promotion requirements and an optional forumify role; a group is a promotion track (Enlisted, Officer, ...). |
Position, Specialty, Award, Qualification |
Sortable catalogs managed in the admin panel. A position lists the weapons its holder may use; a specialty can carry a forumify role. |
Equipment |
A primary weapon, secondary weapon or vehicle. |
Document |
A rich-text template with {placeholders} that a service record can carry. |
Assignment |
A soldier's posting to a unit (and optionally a position and a squad) over a date range. One assignment can be flagged primary. |
SoldierAward, SoldierQualification |
Join entities recording who issued what, and when. |
Operation |
An event: an OPORD with a start/end time, a type (Operation, Patrol, Fun-Day, Training, Meeting, Other), optional unit, status, and optionally a leader, a Deployment and a joiner cap. Owns its RSVPs and AARs. What the type means is in Events and patrols. |
Deployment |
A named period (name, description, start, end), normally a month, that events optionally belong to. |
OperationRSVP |
One per soldier per operation (status, and separately, attended, since intent and reality aren't the same field). |
OperationAAR |
After-action report. Many per operation by design β larger ops often get separate reports from each element lead rather than one summary. |
ReportIn |
One entry each time a soldier reports in. |
EnlistmentApplication |
A request to join; accepting it creates or restores the personnel file. |
FormDefinition, FormSubmission |
A staff-built form and a member's filled-in copy of it (answers stored as text next to the question). |
Course, CourseClass, CourseClassStudent |
A course (with prerequisites and the qualifications a pass grants), a scheduled class of it, and a soldier enrolled in a class with their result. |
ServiceRecord |
The unified timeline entry. Assignments, awards, qualifications, AARs, promotions, course passes, AWOL changes and discharges each write one; deleting the entry that created one removes it too where that applies. |
π§ Frontend routes
Show the full route table
| Route | Path | What it does |
|---|---|---|
command_net_roster |
/roster |
Active roster: one list, or a tab per roster when rosters are defined (?roster=<id>). |
command_net_units |
/units |
Org chart. |
command_net_my_units |
/command-net/my-units |
The units you command. Needs command-net.units.manage_own. |
command_net_my_unit and _edit / _create_child / _create_squad / _import / _delete |
/command-net/my-units/{id} and /edit, /create-child, /create-squad, /import, /delete |
Manage one of your units: its settings, child units, squads, an outline import, and removing it. Only for that unit's commander (or an ancestor's). |
command_net_my_squad and _edit / _create_child / _import / _delete |
/command-net/my-squads/{id} and /edit, /create-child, /import, /delete |
The same for a squad or team inside one of your units. |
command_net_qualifications |
/qualifications |
Public qualifications board. |
command_net_attendance |
/attendance |
Your attendance record, or everyone's with the right permission. |
command_net_attendance_review |
/attendance/review |
Leadership review: soldiers in your scope with filters (unit, no-show rate, miss streak). admin.attendance.view sees everyone; a unit commander (units.manage_own) sees their unit tree. |
command_net_attendance_review_soldier |
/attendance/review/{username} |
One soldier's operation-by-operation attendance, with correction controls where allowed. A member can open their own (attendance.view_own), read only. |
command_net_attendance_review_correct |
/attendance/review/{username}/rsvp/{id} (POST) |
Correct a soldier's attended / no-show mark, or clear it. admin.attendance.manage for anyone, or a commander for their tree, but never their own record. Each correction is logged (actor, soldier, operation, old and new value). |
command_net_promotions |
/promotions |
Promotion eligibility, with a Promote button for managers. |
command_net_promotions_promote |
/promotions/{id}/promote (POST) |
Promote an eligible soldier. |
command_net_report_in |
/roster/report-in (POST) |
Report in. |
command_net_report_in_delete |
/roster/report-in/{id}/delete (POST) |
Remove a report in entry. |
command_net_roster_profile |
/roster/{username} |
A soldier's personnel file: awards, qualifications, assignment history, loadout, service record timeline. |
command_net_roster_award |
/roster/{username}/award |
Issue an award (optionally with a document). |
command_net_roster_award_delete |
/roster/{username}/award/{id}/delete |
Remove an award and its service record entry. |
command_net_roster_qualification |
/roster/{username}/qualification |
Issue a qualification (optionally with a document). |
command_net_roster_qualification_delete |
/roster/{username}/qualification/{id}/delete |
Remove a qualification and its service record entry. |
command_net_roster_assignment |
/roster/{username}/assignment |
Create an assignment (closes the soldier's current primary posting if the new one is primary). |
command_net_roster_assignment_delete |
/roster/{username}/assignment/{id}/delete |
Remove an assignment and its service record entry. |
command_net_roster_service_record_document |
/roster/{username}/service-record/{id}/document |
Print view of the document on a service record entry (command-net.roster.view). |
command_net_roster_service_record_delete |
/roster/{username}/service-record/{id}/delete |
Remove a manual or otherwise-orphaned timeline entry directly. |
command_net_operations |
/operations |
Upcoming/past operations. |
command_net_patrol_new |
/patrols/new |
Post a patrol (command-net.patrols.create). You become its leader. |
command_net_patrol_edit |
/patrols/{id}/edit |
Edit a patrol (its leader, or an operations manager). |
command_net_patrol_cancel |
/patrols/{id}/cancel (POST) |
Cancel a patrol (its leader, or an operations manager). |
command_net_patrol_delete |
/patrols/{id}/delete (POST) |
Permanently delete a patrol, for test patrols and mistakes (an operations manager always; its leader only until an AAR is filed). |
command_net_patrols_mine |
/patrols/mine |
The patrols you have led, with each one's AAR state and a File AAR button. |
command_net_operation_detail |
/operations/{id} |
OPORD, roster with attendance, RSVP controls, AARs. |
command_net_operation_rsvp |
/operations/{id}/rsvp (POST) |
Set or change your own RSVP. |
command_net_operation_attendance |
/operations/{id}/attendance (POST) |
Mark a soldier attended/absent; creates the RSVP row if they never responded. |
command_net_operation_aar |
/operations/{id}/aar |
Submit an after-action report; combat service records are kept at one per soldier marked attended, however many AARs exist. |
command_net_operation_aar_delete |
/operations/{id}/aar/{aarId}/delete (POST) |
Remove a report (its submitter or an operations manager only) and every service record it wrote. |
command_net_enlist |
/enlist |
Apply to join, and see the status of your last application. |
command_net_forms |
/forms |
Open forms, and your own submissions with their status. |
command_net_form_fill |
/forms/{id} |
Fill in a form. |
command_net_squad_xml / _squad_dtd / _squad_logo |
/squad.xml, /squad.dtd, /logo.paa |
The Arma squad export (404 until enabled). |
command_net_courses |
/courses |
Upcoming classes and all courses. |
command_net_course_class |
/courses/class/{id} |
A class: its students, enrol/withdraw, and (for managers) recording results. |
command_net_course_class_enroll / _withdraw / _results |
/courses/class/{id}/enroll, /withdraw, /results (POST) |
Enrol, withdraw, and record every student's result. |
π οΈ Admin
Every catalog has a standard Forumify CRUD screen under Admin β Command Net: Personnel, Units, Ranks, Rank Groups, Rosters, Positions, Specialties, Equipment, Documents, Forms, Courses, Course Classes, Awards, Qualifications, Deployments and Operations. Form Submissions and Enlistment are review queues: opening an entry is the review screen. Enlistment Settings, Rank Settings, AWOL Settings, Report In Settings and Squad XML are single settings pages, and Personnel rows have a Discharge action. There's no separate admin screen for awards issued, qualifications earned, assignments, or service records β those are managed from the frontend personnel file instead, since they only make sense in the context of one soldier.
Units also have Import ORBAT (command-net.admin.units.manage), which builds a unit and squad
structure from a pasted outline; see Squads and unit self-service.
The Units list indents each unit under its parent so the tree is readable.
π Overview
The Command Net admin section opens on Admin β Command Net β Overview
(command-net.admin.personnel.view), a dashboard of real data:
- A Needs attention panel: pending enlistment applications, soldiers currently AWOL, and units with nobody in command.
- Personnel and unit graphs, and tiles for active and AWOL personnel.
- Quick buttons for Create Personnel, Import ORBAT and Create Operation, each shown only to someone who may use it.
ποΈ Ranks
Ranks are on by default. Turn them off under Admin β Command Net β Rank Settings
(command-net.admin.ranks.manage) for a community that doesn't use them: rank stops showing on
the roster, personnel files, org chart, attendance, courses, generated documents, the ORBAT export
and Discord commands, the Promotions page and its nav link disappear, the rank field drops off the
personnel edit form, and rank-role syncing stops running in the background. Nothing is deleted β
the Rank/Rank Group catalog, every soldier's rank, and past promotion/demotion history are all
kept, so turning it back on restores everything exactly as it was.
π Permissions
Checked as command-net.<area>.<action> (the prefix is slugged from the plugin's display name,
"Command Net" β note the hyphen, unlike the underscored route/translation names).
Show the full permission table
| Permission | Grants |
|---|---|
command-net.roster.view |
View the roster and personnel files. |
command-net.admin.rosters.view / .manage |
View / edit the roster tabs. |
command-net.admin.personnel.view / .manage |
View / edit personnel profiles, assignments, service records. |
command-net.admin.personnel.discharge |
Discharge or retire a soldier. |
command-net.admin.enlistment.view / .manage |
View / review enlistment applications, and edit Enlistment Settings. |
command-net.admin.specialties.view / .manage |
View / edit the specialty catalog. |
command-net.admin.equipment.view / .manage |
View / edit the equipment catalog. |
command-net.admin.documents.view / .manage |
View / edit the document templates. |
command-net.admin.forms.view / .manage |
View forms and submissions / edit forms and review submissions. |
command-net.forms.submit |
See and fill in open forms at /forms. |
command-net.admin.courses.view / .manage |
View / edit courses and classes, and record class results. |
command-net.courses.enroll |
See courses and enrol in classes at /courses. |
command-net.units.manage_own |
Manage the units you command (and the squads and units beneath them) from the frontend. See Squads and unit self-service. |
command-net.admin.units.view / .manage |
View / edit units. Also gates Positions β a position isn't useful outside the context of a unit's org chart, so it doesn't get its own permission branch. |
command-net.admin.ranks.view / .manage |
View / edit the rank ladder, and edit Rank Settings. |
command-net.admin.awards.view / .manage |
View / edit the award catalog and issue/remove awards. |
command-net.admin.qualifications.view / .manage |
View / edit the qualification catalog and issue/remove qualifications. |
command-net.admin.operations.view / .manage |
View / edit operations, mark attendance, remove any AAR. |
command-net.admin.deployments.view / .manage |
View / edit deployments, and run Generate Operations. |
command-net.operations.view |
View the operations list and detail pages. |
command-net.operations.rsvp |
RSVP to an operation. |
command-net.operations.submit_aar |
Submit an after-action report on any event except a patrol (a patrol's AAR is filed by its leader, an attendee or staff, see below). |
command-net.patrols.create |
Post a patrol. A migration grants it to Forumify's built-in user role, and this plugin's UserRolePermissionVoter applies that role's Command Net permissions to every account (Forumify itself only applies the role to what it protects with ACLs, not to permissions), so every member has it, logged in or not - including members imported from Discord. Remove it from the user role and give it to a narrower role to restrict posting. Super-admins have it regardless. |
command-net.qualifications.view |
View the public qualifications board. |
command-net.attendance.view_own / .view_all |
View your own attendance record on /attendance / everyone's. |
command-net.promotions.view |
View promotion eligibility on /promotions. |
command-net.reportin.submit |
Use the Report In button. |
command-net.admin.reportin.view / .manage |
See each soldier's last report in / remove report ins and edit Report In Settings. |
command-net.admin.awol.manage |
Edit AWOL Settings. |
command-net.admin.squadxml.manage |
Edit Squad XML settings. |
command-net.admin.attendance.view / .manage power the attendance review pages (see below): view
lets a member review everyone's attendance, manage lets them correct it. Unit commanders reach the
same pages through command-net.units.manage_own instead, scoped to their unit tree.
π Rosters
By default /roster is one list of every active soldier. Under Admin β Command Net β Rosters
(command-net.admin.rosters.view / .manage) staff can define named rosters, each made of
some units and arranged in the order they should appear, such as Combat units and Support. Once at
least one exists the roster page shows a tab per roster (the first is selected, or pick one with
?roster=), listing the roster's units in their order with the soldiers whose current primary
assignment is that unit, senior first. A soldier in none of a roster's units is not on it, and
child units are not folded into their parent. Delete every roster to go back to the single list.
πͺ Squads and unit self-service
A squad is a smaller group inside a unit, and squads nest: a team is a squad inside a squad. A unit or squad lists the positions (billets) it is expected to hold, such as Squad Leader or Team Lead. A position is still the shared catalog title, so listing it on a unit or squad says which billets apply there; it does not create a vacancy count or say who holds it. When creating an assignment you can optionally pick the squad the soldier is in.
Self-service. A unit's commander (or the commander of any unit above it) holds
command-net.units.manage_own and manages their own sub-tree from the site, without admin-panel
access: My Units (/command-net/my-units, also available as a menu item) lists the units
they command, and each unit's page lets them edit its settings and positions (one title per line,
with quick-fill buttons for the standard Team and Squad sets), create child units and squads,
import an outline, and delete a unit or squad that has no child units, squads or assignments left
(anything with history goes through the admin screens instead). Every one of these pages needs
manage_own. On its own that only reaches units you command; someone who also holds
command-net.admin.units.manage can use the pages on any unit. That admin grant does not give
attendance review, which is scoped to commanders only.
Outline import. Instead of creating each row by hand, paste an outline: one entry per line, indented two spaces per level.
# Lines starting with # are ignored
Detachment 7
= Commanding Officer
+ Squad 1
@Squad
+ Team 1
@Team
- A bare line creates a unit (it can only sit under another unit).
+creates a squad (or a team, inside a squad).=links a position to the nearest unit or squad, creating it in the catalog if needed.@Teamor@Squadexpands to a standard set of positions (Team: Team Lead, Medic, Team Member; Squad: Squad Leader, Squad Medic).#starts a comment.
Admins import from Admin β Command Net β Import ORBAT, where a template can be downloaded and the import can go under any unit or at the top level; commanders import under the unit they are managing. The import only builds structure. It never creates assignments, because a name in a roster sheet can't be reliably matched to a forum account, so people are still assigned one at a time.
π Courses
Staff define courses under Admin β Command Net β Courses (command-net.admin.courses.view /
.manage): a description, an optional minimum rank, prerequisite courses, and the qualifications
a pass grants. They schedule classes of a course under Course Classes: start and end, an
optional number of places and an instructor.
Members with command-net.courses.enroll see /courses, open a class and enrol or withdraw until
it starts. Enrolling checks that they are enlisted, the class has a free place, they meet the
minimum rank and have passed every prerequisite. Once a class has started, someone with
command-net.admin.courses.manage records a result for every student on the class page (passed,
failed, no-show or excused) in one go. That is final: each pass writes a course record and grants
the course qualifications the student does not already hold, and everyone is notified. To correct
a mistake, remove the entries it wrote from the personnel file.
Not included yet (MILHQ has): several instructors per class with roles, a signup window, awards as a course reward, a course image, calendar sync, and per-student service record text.
π Forms
Staff build forms under Admin β Command Net β Forms (command-net.admin.forms.view /
.manage), such as a leave request or a transfer request. Fields are written as text, one per
line, and checked when the form is saved:
type | Label | required | options
Types are text, textarea, number, boolean, date and select (only select takes
options, comma separated); the third part is required or optional. For example
select | Branch | required | Army, Navy, Air Force. Members with command-net.forms.submit see
open forms at /forms and can follow the status of what they submitted. Staff review submissions
under Admin β Command Net β Form Submissions: accept or decline with an optional note, and the
submitter is notified. Answers are saved as text next to the question, so editing a form later
never changes what was already submitted.
Not included yet (MILHQ has): a point-and-click field editor, help text per field, custom statuses, supervisor routing and using a form for enlistment.
π Documents
A document is a reusable rich-text template with {placeholders}, such as an award citation
or a promotion order (Admin β Command Net β Documents, command-net.admin.documents.view /
.manage). When issuing an award, issuing a qualification or creating an assignment from a
personnel file, you can pick one; it is shown under that entry in the service record, filled in
for the soldier and record. The placeholders ({user_name}, {user_rank}, {record_title},
...) are listed in the document editor. Values are HTML-escaped, and a placeholder that is not
recognised is left as written.
A document can also be attached when promoting or demoting: the promotions page has a document
dropdown next to Promote, and the admin personnel form has a Rank change document picker.
Next to any record's document is a Print link (command-net.roster.view) to a plain page with
a print button, so the browser's print-to-PDF does the work; there is no separate PDF download.
Not included yet: documents can not be attached to entries created any other way (a discharge, a course pass, an AWOL change), and existing promotion records are not backfilled with one.
π― Equipment
Equipment is a catalog of weapons and vehicles (Admin β Command Net β Equipment,
command-net.admin.equipment.view / .manage). Each is a primary weapon, a secondary weapon or a
vehicle. Positions list the primary and secondary weapons their holder may use, and units list the
vehicles they have. A soldier's personnel file shows a Loadout card worked out from the
position and unit of their primary assignment; nothing is stored per soldier, so changing a
position or unit changes it for everyone who holds it.
Each item can carry an Arma classname (e.g. B_MRAP_01_F), which the ORBAT export needs.
π Specialties
A specialty is a soldier's trade (Combat Medic, Radio Operator, ...): one per soldier, set on
their profile in Admin β Command Net β Personnel, and shown on their personnel file and the
roster. Unlike a position it follows them between units. Manage them under Admin β Command Net β
Specialties (command-net.admin.specialties.view / .manage). A specialty can carry a forumify
Role: a soldier holds the role of their specialty, and it is removed if it changes or they are
discharged (map it to a Discord role in the Discord plugin to keep Discord in step).
Changes to a soldier's specialty are not written to their service record.
πͺ Enlistment
Turn it on under Admin β Command Net β Enlistment Settings. Signed-in members with a
verified, non-banned account can then apply at /enlist (add it to the menu with the Command Net
menu item): callsign, Steam ID, why they want to join, experience and availability. Staff review
applications under Admin β Command Net β Enlistment (command-net.admin.enlistment.view /
.manage) and accept or decline with an optional note.
Accepting creates the personnel file (or restores a discharged or retired one), sets the enlistment date, gives the configured starting rank if they have none, posts them to the configured starting unit, writes the enlistment and assignment records, and notifies the applicant. Declining just notifies. A declined applicant can apply again; someone with a pending application, or who is already enlisted, can't.
Unlike MILHQ, the application is a fixed form rather than one built in a form builder, and it doesn't open a forum topic for recruiters; both come with a forms feature, which doesn't exist yet.
πͺ Discharge
Discharge appears on each row of Admin β Command Net β Personnel and on the personnel edit
screen, for people with command-net.admin.personnel.discharge. Pick General, Honorable,
Dishonorable or Retirement (retirement sets the status to Retired, the others to Discharged), an
effective date and an optional reason. It ends the soldier's open assignments, removes their unit,
rank and AWOL roles, and writes a discharge service record. The soldier leaves the roster, and can
no longer RSVP or report in.
Unlike MILHQ it keeps everything: rank, awards, qualifications and the full service record stay on the personnel file rather than being cleared, and it doesn't offer a final rank or new posting as part of the discharge. Enlistment can bring a discharged soldier back, keeping their history.
ποΈ Promotions
/promotions (permission command-net.promotions.view) lists active soldiers against the
requirements of the next rank up β minimum time in the previous rank and required
qualifications, both set on the target rank. Anyone with command-net.admin.personnel.manage
also gets a Promote button on eligible rows; it re-checks the requirements, changes the
rank, writes the promotion record and notifies the soldier. Skipping the requirements is a
rank edit on the admin personnel form.
Ranks can be put in a rank group (Admin β Command Net β Rank Groups), such as Enlisted or Officer. Promotion only moves up within a group, so the top of one track isn't offered the bottom of the next; ranks with no group share one ladder.
A rank can be given a forumify Role in the admin. A soldier holds the role of their current rank and loses every other rank's role on any rank change, from either the Promote button or the admin form; map those roles to Discord roles in the Discord plugin to keep Discord in step.
βοΈ Events and patrols
The community runs several kinds of event, and an event's type decides how it behaves. The
rules live in one class, Service/EventRules, which the AWOL, attendance and AAR code all ask.
| Type | Expected roster | Counts toward AWOL | Combat credit | AAR |
|---|---|---|---|---|
| Operation | The unit's members, or every active soldier | Yes | On attendance alone | Optional |
| Patrol | Only those who join | No | On attendance and a filed AAR | Required |
| Fun-Day | Only those who join | No | None | Optional |
| Training, Meeting, Other | As before | No | On attendance and a filed AAR, as before | Optional |
Only Operations count toward AWOL. Operations earn combat credit as soon as attendance is marked;
nothing was backfilled, so an Operation only gains records when its attendance is next changed.
Training, Meeting and Other keep the rule they had before types had rules (change
EventRules::creditsCombat() if trainings should stop earning combat records).
Deployments (Admin β Command Net β Deployments) are monthly containers: a name, description, start and end date. An event optionally belongs to one, set on the Operation form. The Generate Operations action on a deployment previews and then creates an Operation every Wednesday and Saturday at 2000 US Eastern (daylight saving followed) across its dates, skipping any slot that already has one, so it is safe to run twice.
Patrols are led by members. Anyone with command-net.patrols.create can post one from
/patrols/new (title, start, optional end, area, plan, optional unit, optional joiner cap) and
becomes its leader and first attendee. Members join with the normal RSVP buttons, and a cap
stops "attending" RSVPs once it is full. The leader, or an operations manager, can edit or cancel
it; patrols are never put on a calendar, so a synced calendar does not announce them twice. The
leader can mark attendance and file the AAR for their own patrol and no one else's; an attendee or
staff can also file it. Other events keep the submit_aar and operations.manage rules.
A patrol can be deleted permanently from its page: an operations manager can always, and its leader only until an AAR has been filed (once it is on the record, only staff can remove it). The patrol, its sign-ups and its reports go, and so do the combat records they earned on personnel files. That cleanup runs whenever any operation is deleted, including from the admin list, which used to leave those records behind. With the Discord plugin, the patrol's post is deleted too.
A patrol's AAR follows the community's template and must include images. The form has the
date-time group (DDHHHHRMMMYY, worked out from the patrol's start time in UTC, e.g.
261900ZSEP26), tasking, callsigns (filled in from who joined, editable), FKIA / FWIA / FMIA,
EKIA, and the report (with the objectives-met choice and notes as before), and at least one map
and one intel image - the form is refused without them. Images are JPEG, PNG, GIF or WebP, up to
8 MB each (5 per kind from Discord), and are stored with the asset storage; they are shown on the
patrol page, and deleted with the report or the patrol. Every other kind of event keeps the
original short form. Reports filed before this have none of the new fields and show as before.
AAR due date. A patrol's AAR is due 24 hours after it ends (its start, if it has no end
time). "Due" and "overdue" are worked out from the end time, that deadline and whether an AAR
exists, so nothing is stored: a cancelled patrol owes none, and filing the AAR clears the flag.
There is no penalty, only the flag, which shows on the patrol page and on My patrols
(/patrols/mine). A scheduled task (command-net:patrols:send-aar-reminders, hourly) sends the
leader a forum notification when the AAR falls due and again when it is overdue. It keeps no
state, so a run the scheduler misses skips that reminder.
A patrol can be linked to a deployment: the web form has an optional Deployment choice (newest first), and the Discord command takes the deployment's name, matched ignoring case, and refuses a name that matches nothing (listing the recent ones) instead of posting an unlinked patrol.
Discord commands (only registered when the Discord plugin is installed):
/command-net-patrol-create posts a patrol, with an optional deployment (same
patrols.create permission as the web form, checked by hand since a Discord-triggered request
has no security token to check against - see RawPermissionChecker); /command-net-patrol-list
shows upcoming patrols; /command-net-patrol-join / -leave RSVP (same operations.rsvp
permission, joiner-cap and "enlisted only" rules as the web); and /command-net-patrol-aar says
how to file the AAR, because a slash command cannot carry images. The AAR is filed by the
Submit AAR button on the patrol's Discord post (needs the Discord plugin's patrol posts and a
bot with the two-step AAR form): the bot collects the template's text and the map and intel
uploads and passes them to this command, which fetches the images from Discord's own CDN only,
checks they really are pictures, and files the report for the patrol's leader. Anyone else, or a
report missing something, gets the web form's link. All five need the caller's Discord account
linked to a forum account. New patrols (web or Discord-posted) are announced to Discord and the
AAR due/overdue reminder is echoed to a channel, mentioning the leader, by the Discord plugin's
own listeners - this plugin only exposes the PatrolReminderNotifier interface (a no-op without
Discord) for that second half.
π¨ AWOL detection
Turn it on under Admin β Command Net β AWOL Settings (command-net.admin.awol.manage), with a
number of consecutive missed operations and an optional forumify AWOL role. An active soldier
who misses that many in a row is flagged AWOL, and the flag clears when they next attend one. Only
events of type Operation where attendance was taken count (a no-show at a patrol, fun day,
training or meeting does not), only those of the soldier's current unit (or with no unit), and
only ones since they last became Active, so leave and transfers do not count against them.
An AWOL set by an admin is never cleared automatically. Every change writes an AWOL service record
and notifies the soldier. Failing to report in flags AWOL too, and is cleared by reporting in.
π‘ Report In
Soldiers with command-net.reportin.submit get a Report In button on the roster. Turn on
enforcement under Admin β Command Net β Report In Settings: a daily scheduled task
(command-net:report-in:run-checks, 08:00) flags an active soldier AWOL once they go past
the configured number of days without reporting in, and optionally warns them as the deadline
approaches. Reporting in again restores Active. It reuses the AWOL role from AWOL Settings and
writes the same audit record and notification. A soldier with no report in on file gets a
baseline entry rather than being failed, so enabling this doesn't flag everyone at once. An AWOL
set by an admin or by missed operations is never cleared by reporting in.
π§± Design notes
- Corrections happen by deletion, not editing. Awards, qualifications, assignments, service records, and AARs can all be removed but never edited in place β the same "records are facts, not form fields" rule the MILHQ plugin uses for its own history. Fix a mistake by removing the wrong entry and creating the right one.
- Service records are linked back to what created them.
ServiceRecordcarries a nullablesourceType/sourceIdpair set when an award, qualification, assignment, or AAR writes one, so deleting the source also removes the timeline entry it generated instead of leaving an orphan behind. Entries with no single source (a promotion, a discharge, a course pass) leave both null. - Attendance is separate from RSVP. A soldier's RSVP status is their stated intent;
attendedis a separate field an operations manager sets afterward, so a no-show who RSVP'd "attending" doesn't get a combat record, and someone who shows up unannounced can still get credit.
β οΈ Known gaps
The earliest features have run against a live install. Everything added since the audit (rank
groups, enlistment, discharge, specialties, equipment, documents, forms, courses, rosters, and
the fixes) has been through CI and the application tests below, but not a live install with real
data; docs/merge-and-test-plan.md says what has been tested and what to check on a staging copy
before relying on it.
- Tests are thinner than the plugin. CI runs PHPUnit, phpcs and PHPStan. The unit tests mock
the repositories. Two application tests boot the plugin in a real Forumify install on MySQL
(
tests/Application): one loads every page as an administrator, the other drives the main submit-and-review flows (enlistment, promotion, RSVP and attendance, transfers, discharge, forms, courses) and checks the database. A third checks every endpoint against the permission it needs, as a member with none, with everything but that one, and with only that one, and a fourth checks that the pages listing every soldier run the same number of queries at 3 and at 15 soldiers. A fifth checks what the main pages show a member holding each single permission (buttons, forms, report details), and a sixth checks what pages say and do: the specialty, Loadout card and record types, HTML escaping in documents, the roster tabs, the squad file against its DTD, the Rank form, and what a discharged soldier is offered and refused. The flow test also checks that the notifications those actions should send are created, and drives AWOL detection and the Report In command. A seventh reorders rows in the sortable admin tables and checks a notification appears in its recipient's bell. Email and Discord delivery of notifications, the cron trigger of the Report In task and the Discord integration are not covered. - Not in this plugin yet, although MILHQ has it: configurable statuses, a point-and-click
form builder, several instructors per course class, calendar sync for classes, and the
Discord
/award,/qualificationand/rankcommands. The section for each feature above lists what its first version leaves out. MILHQ's PERSCOM migration tool is deliberately left out; this install does not migrate from PERSCOM. Unit's Discord server id is only stored here. This plugin never reads it; the Discord plugin uses it to send a transferred soldier an invite to their new unit's server.src/Discordis analysed against stubs, not the real privateMajesticDev\Discordplugin (tests/PhpstanStubs), so a change to that plugin's signatures isn't caught here. The same stubs let the Discord commands be unit tested in CI.
πΊοΈ Squad XML
Arma reads a unit's squad page from three files at the site root. Turn them on under Admin β
Command Net β Squad XML (command-net.admin.squadxml.manage): a squad tag, name, title, web
address and email (each falls back to the community title, the site address or N/A), and an
optional .paa logo. /squad.xml then lists every enlisted soldier who has a Steam ID: the Steam
ID as the member id, their callsign (or display name) as the nick, their display name, and their
unit as the remark, senior first. /squad.dtd and /logo.paa are served alongside it. Names with
symbols such as & are escaped. The files are public and off until enabled, since they publish
members' Steam IDs; the XML is cached for 15 minutes, and saving the settings clears the cache.
πͺ ORBAT export
Admin β Command Net β ORBAT Export (command-net.admin.units.view) turns the unit tree into
an Arma 3 CfgORBAT block to copy or download as orbat.hpp. Each unit becomes a nested group
with its name, abbreviation, description, commander (callsign or display name) and their rank. A
unit's vehicles become its assets, counted by classname; vehicles without an Arma classname are
left out. Size and type come from the unit's ORBAT size and ORBAT type fields; a blank
size is worked out from how many echelons sit below the unit (squad, platoon, company, ... army)
and a blank type exports as Infantry. The side (BLUFOR, OPFOR, Independent, Civilian) is picked on
the export page. Nothing is published: the file is fetched here when a mission or mod needs
updating. The exact CfgORBAT keys have not been checked against a running Arma 3 client yet.
π Integrations
- Calendar plugin: when installed, an operation can be linked to a calendar and is mirrored as a calendar event (removed if the operation is cancelled).
- Discord plugin: the forumify roles this plugin grants (unit, rank, specialty, AWOL) can be
mapped to Discord roles in that plugin's own settings, which keeps Discord in step without any
Discord code here. It also adds slash commands:
/command-net-soldier,/command-net-unit,/command-net-promotion, and the patrol commands in Events and patrols./command-net-soldiershows the specialty and loadout, and/command-net-unitshows the unit's vehicles. AUnit's Discord server id is read by the Discord plugin, which uses it to DM a transferred soldier an invite to their new unit's server.
π€ Works well with
milsim-id-card-pluginβ if installed, a soldier's personnel file gets a button to view or create their MILSIM ID card, and the ID card plugin can use this plugin as a personnel source without ever naming it anywhere public.command-net-themeβ the frontend theme this plugin is designed to be used with; its homepage reads this plugin'sOperationrepository and online-count Twig function directly.