The bot and this website are one program. It reads the CIT forum, keeps a database of every member in step with it, answers commands in Discord, and posts reports and requests back to the forum on a member's behalf. This page walks through each of those in the order they actually happen.
The most common assumption about this setup is wrong. There is no separate bot server. The website and the Discord bot run in the same Node process, on the same machine, against the same database connection. When the site starts it connects to MongoDB first, and only once that connection succeeds does it start the Discord client.
That ordering matters. A bot that answered commands before the database was ready would tell members their account does not exist. So the bot is held back until it has something to read from.
Because they share a process, the website can call the bot directly. An admin page that needs a Discord channel list does not make an HTTP request to a bot somewhere else. It asks the client object already sitting in memory. The same works in reverse: a command typed in Discord runs the identical database code the website uses, so the two can never disagree about what a member's rank is.
Every ten minutes the bot re-reads the SAPD forum topic and brings the database back in line with it. The forum is the authority. Nothing here edits the forum roster, it only mirrors it.
Two smaller loops run alongside it. Every five minutes the bot reads who is currently online in game and matches those players to members, falling back through account name, then member name, then Discord tag when the first match misses. Also every five minutes it reads the six live ranking boards on cit.gg, two seconds apart so the requests stay inside the forum's burst limit.
Everything else on this page reads or writes one of two things. The member record is the current state of a person. The action record is one thing that happened to them.
Actions are what the promotion maths, the profile timeline and the changelog page all read from. Because each one is fingerprinted on the way in, the same forum line read on twenty consecutive sync cycles still produces exactly one action.
Most of what follows needs to know which member is typing. A Discord user id on its own means nothing to the roster, so the two have to be tied together once.
After that, !whois resolves a mention, a name or an id to a full profile, and staff can link or unlink an account by hand with !member link and !member unlink.
Training reports, patrol reports, vacation requests, promotion requests, test requests, badge requests and PUN requests all end up as a post on the CIT forum. The member never has to paste BBCode. They fill in a form, on the site or in Discord, and the bot posts it for them.
Requests go to one topic and reports to another. Which topic a post belongs in is decided by its kind, not by the form that produced it, so a vacation request raised in Discord lands in exactly the same place as one raised on the site.
Every rank up to Inspector has a minimum time in rank and a set of activity requirements. The bot recalculates progress against them every five minutes and the numbers appear on the promotion dashboard.
| Rank | Promotes to | Min days | Activity required |
|---|---|---|---|
| Trial | Recruit | 7 | 2 patrols |
| Recruit | Constable | 14 | 2 patrols, 1 training |
| Constable | Officer | 14 | 3 patrols, 1 training |
| Officer | Station Officer | 14 | 5 patrols, 3 trainings |
| Station Officer | Sergeant | 30 | 10 patrols, 1 training hosted, 1 patrol hosted |
| Sergeant | Staff Sergeant | 40 | 12 patrols, 2 trainings hosted, 2 patrols hosted |
| Staff Sergeant | Inspector | 50 | 5 trainings hosted, 5 patrols hosted |
| Inspector | Chief Inspector | 60 | 8 trainings hosted, 8 patrols hosted |
| Chief Inspector and above |
By appointment | Not scored | Given out by leadership, not earned by a count |
Scoring stops at Inspector deliberately. Chief Inspector and everything above it is staff and leadership, and those seats are decided by the Chief. Running the requirement maths on them produced a percentage nobody was ever going to act on, and put a Superintendent in a queue behind a Constable.
Below that line, a member's progress is a weighted blend of three things: time in rank, activity against the requirements above, and evaluation marks from the reports they appear in. The weighting shifts as the ranks climb. At Trial it leans on activity and time. By Inspector, evaluation marks carry roughly a third of it.
Joining runs through the same system whether the applicant starts on the website or in Discord. This used to be a separate bot with its own database. It was folded in so there is one record of an application rather than two that drift apart.
!backup pings the backup role with the current criminal count and
wanted list pulled live. !training and !patrol ping the
login role that one is about to be hosted. !wanted posts the wanted
list on its own with no role ping.
!admin. Permissions
are per route and per action, held in the database rather than in code, so what a
role can view, edit or delete can change without a deploy. Accounts left unused are
locked automatically.
| Job | Runs | What it does |
|---|---|---|
| Forum sync | Every 10 min | Changelog, then roster and forum profiles |
| Online players | Every 5 min | Who is in game, matched to member records |
| Live rankings | Every 5 min | Six ranking boards, and alerts when SAPD moves |
| Promotion recalculation | Every 5 min | Time in rank and last promotion, per member |
| Report metadata | Every 30 min | Cached patrol and training counts |
| Forum backup | Hourly | Raw BBCode of the department's posts, to disk and Discord |
| Eligibility announcement | Daily, 18:00 | Posts members who newly qualify for promotion |
| Account auto-lock | Daily, 02:00 | Locks unused staff website accounts |
| Full forum backup | Daily, 03:30 | Offset from the hourly run so the two never collide |
Most of the awkward parts of this system come from one fact: the forum rate limits hard, and it does not always say so. A good deal of the design exists to avoid making a bad situation worse.
The pattern underneath all of it is the same. Save the member's work first, tell them the truth about what happened to it, and never let a system that is outside our control turn a completed action into a lost one.