/level member
View a member's level
163 commands across 19 categories, auto-generated from source.
Showing all 163 commands
Vela works with zero configuration the moment it's invited: moderation, leveling, tickets, and the Shard economy are all active by default. Here's how to explore and customize from there.
Add the bot to your server. No setup required to get started.
A quick status check with links straight to the dashboard page for whatever you want to configure.
Fine-tune leveling, logs, tickets, roles, and more visually, with no command syntax to remember.
Browse every command by category, or look up any command's exact options with /help command:<name>.
View a member's level
Adds levels to a member
Sets a member's level
Shows the top users by level and XP
Configure leveling settings for this server
Resets a user's XP and levels
Adds a role reward based on level
Removes a role reward from a level
Add a targeted word or phrase to the server filter.
Remove a targeted word or phrase from the server filter.
Load a ready-made list of banned words.
Disable presets or completely purge the server filter configuration.
Set a new prefix
Enable prefix commands
Disable prefix commands
Enable or disable bare/prefix tag shortcuts
Set AI detection thresholds
Map an AI label to a severity tier
Add a term to the allowed slang list
Remove a term from the allowed slang list
Current heat settings and the live temperature
The current heat settings and the live temperature of the server and this channel. Every setting can be changed with /automod set, or with more explanation on the dashboard's Auto-Mod page.
/automod view-automod view
Change heat settings (anything left out stays as it is)
Changes only the options you actually pass; everything else keeps its current value. The full picture, with an explanation of every number, lives on the dashboard's Auto-Mod page. The AI check also needs a GROQ_API_KEY on the host to run; setting use_ai without one stores the choice but nothing is scanned.
/automod set enabled:true/automod set msg_heat:2 yellow_alert:40/automod set use_ai:true delete on severe is on the dashboard
Restricts a command's usage to a specific server role.
Instantly locks the entire server (Panic Mode)
Lifts Panic Mode and unlocks the server
Removes a role restriction constraint from a specific command.
Switch your VIP card tier (bot owner only).
Appeal one of your own cases to the staff team
Files an appeal against one of your own cases. The staff team decides it: from the buttons on the post, /appeals decide, or the dashboard. and you hear the outcome with any staff note. While it waits you can add context with /appeals info or pull it back with /appeals withdraw. An upheld appeal says the team reconsidered; it does not by itself undo a punishment. Deliberately open to everyone: the person punished is usually the person with the least power in the server, and gating appeals behind a role would gate them away from exactly who they exist for.
/appeals file caseid:42 reason:it was not me-appeals file 42 wrongful timeout
Your appeals and what the staff decided
Your appeals in this server: what is waiting, what was decided, and any note the staff left with the outcome.
/appeals status-appeals status
Withdraw one of your pending appeals
Pulls one of your pending appeals back. The staff post is marked withdrawn and the case stands as it was. Decided appeals cannot be withdrawn. They are history now.
/appeals withdraw id:3
Add context to one of your pending appeals
Adds context to one of your pending appeals after the fact: a name you remembered, a screenshot reference, whatever the first message missed. The staff post gains a field, so nobody has to dig. Five follow-ups per appeal.
/appeals info id:3 text:the timeout was actually my brother
The staff queue: every appeal waiting for a decision
The staff queue without opening a browser: every appeal waiting for a decision, oldest pressure first. Settle any of them with /appeals decide or the buttons on the appeal post.
/appeals list-appeals list
Settle an appeal by id, with an optional note to the member
Settles an appeal by id, from anywhere in the server, no dashboard required. The optional note travels to the member's DM with the outcome and stays on the appeal record. First decision wins: if another moderator or the dashboard already settled it, you are told rather than overwriting.
/appeals decide id:3 verdict:uphold/appeals decide id:3 verdict:deny note:evidence checked with the victim
Turn appeals on or off, and choose where they land
Turn appeals off entirely, for servers that run their own appeal process somewhere else, and choose where appeals are posted. With no channel set they land in the mod log. Turning appeals off stops new filings; appeals already waiting still get decided.
/appeals config enabled:false/appeals config channel:#appeals/appeals config enabled:true channel:#staff-appeals
Change the bot's nickname in this server
Change the bot's banner in this server
Change the bot's avatar in this server
Change the bot's bio in this server
Show detailed information about the bot
View case by case id
Views all cases linked to a member
Update case by id
Deletes case by id
Mark a case as closed
Reopen a closed case
Add a staff note to a case
Show the proof on a case, or attach more
Shows the proof on a case, or attaches more after the fact. Every link is resolved fresh when you run this rather than being read from the database, because Discord expires attachment URLs within about a day. Anything filed before an evidence channel was configured shows as expired, which is the honest answer rather than a link that 404s when someone clicks it.
/case evidence caseid:42/case evidence caseid:42 proof:<file>
View the history of a case
List all cases in this server
Search for cases with a key word
Export all cases for this server
Merge one case into another
Clear all cases for a user or the entire server
Sets the channel
View a user dashboard
Check your (or someone else's) Shard balance
Claim your daily Signal Check-In
Scavenge the void for Shards
Send Shards to another member
View the Shard leaderboard
Move Shards from your wallet into your bank
Move Shards from your bank into your wallet
Upgrade your bank to the next tier for a higher capacity
Browse what Shards can buy
Buy an item from the shop
Check your active boosts, profile title, and owned cosmetics
Equip an owned rank-card cosmetic
Set a custom profile title (1,000 Shards)
View today's 3 daily quests and your progress
Combine 3 owned cosmetics into 1 higher-rarity cosmetic
Gamble Shards on a coin-flip (48% to win, 2x payout)
Gamble Shards on a 1-100 dice roll (win on 52+, 2x payout)
Challenge another member to a Shard-wagering duel
Offer to trade an owned cosmetic with another member
Adopt your first companion (free, one per server)
Train your companion (+1 level, 100 Shards, 4h cooldown)
View your achievement progress and Sentinel Rank
Makes a death card for a user
Check your current active chat velocity temperature or view another user's heat metrics.
Interact with another user!
Roast a user with a scripted, chaotic paragraph.
Ships two members
Shows the last deleted message in this channel
Summons a user
Tarot readings
Get a Dare question
Get a truth question
Launches an advanced customizable giveaway execution
Forcibly interrupts an ongoing operational giveaway instantly
Re-runs structural lottery drawing parameters on a finalized giveaway id
enables certain logging events
bans a member
The reason reaches both the Discord audit log and the member's case history, so it is worth filling in. If the goal is to clear someone's recent messages rather than keep them out, use softban.
-ban @Ripley ban evasion/moderation ban user:@Ripley reason:raid account
Warns a member
Records a case against the member and tries to DM them the reason. Nothing is removed or restricted: a warning is a record, and it is the case history that other tooling reads.
-warn @Ripley spamming in general/moderation warn user:@Ripley reason:third strike
Kicks a member.
Timeouts a member.
Duration takes s, m, h and d, and units combine, so 30m, 2h and 1d12h are all valid. Discord caps a timeout at 7 days and so does this. Lift one early with untime.
-timeout @Ripley 2h flaming-timeout @Ripley 1d12h/moderation timeout user:@Ripley time:30m reason:cooling off
Unbans a member
Takes a user ID rather than a mention, since a banned account is no longer in the server to mention. Right-click the entry in Server Settings then Bans to copy one.
-unban 210987654321098765 appealed/moderation unban userid:210987654321098765
Bans then immediately unbans a member to clear their recent messages
A ban followed immediately by an unban. The member can rejoin straight away, but their recent messages are gone. The time argument is how far back to delete, not how long they are out, and it defaults to 7d, the maximum Discord allows.
-softban @Ripley 1d cleaning up a spam wave/moderation softban user:@Ripley time:12h
Removes a timeout on a member.
Strip a member's roles and confine them to the jail channel
Takes every role Vela can manage and leaves the jail role in its place, so the member keeps one channel to explain themselves in. That is the difference from timeout, which silences them everywhere including anywhere they could appeal. What was taken is recorded, so unjail gives back exactly that, and leaving the server does not clear it: rejoining lands them back in jail. Run jail-setup once first.
-jail @Ripley ban evasion, pending review/moderation jail user:@Ripley reason:under review
Release a member and give back the roles jail took
Gives back exactly the roles jail took, not every role they ever had. A role deleted or moved above Vela while they were inside is skipped rather than failing the whole release.
-unjail @Ripley/moderation unjail user:@Ripley reason:appealed
Set up jail, or check the setup still works
Run with no options and Vela builds the whole thing: a Jailed role, a jail channel, and a View Channel denial on every other channel so the role can only see that one. It shows the plan first and changes nothing until you run it again with confirm, because this edits permissions on every channel in the server. Pass a role you already have to use that instead, and run it with no options at any time to check the setup is still sound.
/moderation jail-setup/moderation jail-setup confirm:true/moderation jail-setup role:@Jailed channel:#appeals
Who is jailed, and whether the setup still works
Who is inside, why, and whether they get out on their own. Also re-runs the setup checks, since the most common way jail stops working is a channel created after setup that nobody sealed.
-jaillist/moderation jaillist
Ban a member for a set length of time
A ban that lifts itself. The duration takes the same units as timeout, so 30m, 2h and 7d30m all work, up to a year. The unban is recorded as its own case, and it survives a restart: the deadline lives in the database, not in a timer, so a ban that expires while Vela is offline is lifted on the next start rather than never.
-tempban @Ripley 7d raiding/moderation tempban user:@Ripley duration:24h reason:cooling off
Deletes a specified amount of messages.
Every filter narrows the same sweep rather than adding to it, so user, contains, bots, humans, attachments, images, embeds, links, stickers and mentions all combine. Pinned messages are never touched, and Discord will not bulk delete anything older than 14 days whatever the filters say. preview:true shows exactly what would go and waits for a confirmation.
-purge 50/moderation purge amount:100 bots:true/moderation purge amount:50 contains:discord.gg/moderation purge amount:100 user:@Ripley images:true/moderation purge amount:100 preview:true
Set how long members must wait between messages
Sets how long members must wait between messages in this channel, from off up to six hours. Bots, moderators and members with Manage Messages are never slowed by Discord.
/moderation slowmode setting:5 seconds/moderation slowmode setting:off-slowmode 10 seconds
Lock the current channel
Five modes, in order of severity: soft, hard, raid, announcement and panic. Give a duration to have the channel unlock itself, or lift it early with unlock. This locks one channel; lockdown covers the whole server.
-lock soft/moderation lock mode:raid duration:2h reason:raid in progress
Unlock the current channel
Starts a server lockdown
The server-wide counterpart to lock, with the same five modes. Use unlockdown to lift it.
-lockdown raid 1h/moderation lockdown mode:panic reason:mass join
Unlockdown the server
Give a role to a user
Take a role away from a user
The counterpart to `role add`. Until this existed the only way to take a role back was `role purge-members`, which strips it from every member, so undoing one mis-assigned role meant undoing everyone's.
-role remove @Ripley @Moderator/role remove user:@Ripley role:@Moderator
Give a role that removes itself after a duration
Gives a role that takes itself back. This is how most servers hand out a mute or a restriction without having to remember to undo it. The deadline is stored, not held in a timer, so it survives a restart. Re-running it on someone who already has a timer moves the deadline rather than stacking a second one.
-role temp @Ripley @Muted 2h/role temp user:@Ripley role:@Muted duration:7d
Show detailed information about a role
List all members with a role
Find roles by name
Move a role to a new position
Remove a role from all members
Clean up unused or duplicate roles
Show what a role can do
Adds a staff role
Make a role stick through a leave and rejoin
Without this, every role-based punishment is undone by leaving and rejoining. The snapshot is taken when a member leaves, because once they are gone Discord no longer remembers what they held. Roles above Vela, roles managed by an integration and the everyone role are never saved, since none of them can be given back.
/stickyrole add role:@Muted/stickyrole toggle enabled:true-stickyrole list
Stop a role sticking
Without this, every role-based punishment is undone by leaving and rejoining. The snapshot is taken when a member leaves, because once they are gone Discord no longer remembers what they held. Roles above Vela, roles managed by an integration and the everyone role are never saved, since none of them can be given back.
/stickyrole add role:@Muted/stickyrole toggle enabled:true-stickyrole list
Show the sticky role settings
Without this, every role-based punishment is undone by leaving and rejoining. The snapshot is taken when a member leaves, because once they are gone Discord no longer remembers what they held. Roles above Vela, roles managed by an integration and the everyone role are never saved, since none of them can be given back.
/stickyrole add role:@Muted/stickyrole toggle enabled:true-stickyrole list
Turn sticky roles on or off for this server
Without this, every role-based punishment is undone by leaving and rejoining. The snapshot is taken when a member leaves, because once they are gone Discord no longer remembers what they held. Roles above Vela, roles managed by an integration and the everyone role are never saved, since none of them can be given back.
/stickyrole add role:@Muted/stickyrole toggle enabled:true-stickyrole list
Stick only the listed roles, or everything
Without this, every role-based punishment is undone by leaving and rejoining. The snapshot is taken when a member leaves, because once they are gone Discord no longer remembers what they held. Roles above Vela, roles managed by an integration and the everyone role are never saved, since none of them can be given back.
/stickyrole add role:@Muted/stickyrole toggle enabled:true-stickyrole list
Create a new role panel (add roles to it afterwards)
Add a role to a panel
Remove a role from a panel
Post the panel, or update it in place if already posted
List every role panel in this server
Delete a panel and its posted message
Create or update a board
Creates a board, or updates the one already watching this channel. Several boards can live side by side, a low-threshold board for the everyday funny and a high one for the legendary, and each watches its own emoji, so a server emoji and ⭐ can feed different skies.
/starboard set channel:#starboard/starboard set channel:#hall-of-fame threshold:25 name:Hall of Fame/starboard set channel:#memes emoji:<:glow:1234567890> threshold:5
Remove a board
Removes a board and stops maintaining it. Posts already on the board stay where they are. They are part of the channel's history now.
/starboard remove channel:#starboard
Show every board and its threshold
Every board, its threshold, and how many messages have landed.
/starboard list-starboard list
Keep a channel or a role out of the boards
Excluded channels never feed a board at all; stars from members with an excluded role don't count toward anyone's threshold. The intended use being staff channels and staff roles, so moderation chatter and moderators don't dominate the sky.
/starboard exclude mode:add channel:#staff/starboard exclude mode:add role:@Moderators/starboard exclude mode:remove channel:#staff
Who shines brightest
The month's (or all-time) brightest members and the single most-starred message, with a jump link. Everyone can look; only the sky decides who's on it.
/starboard top/starboard top period:all time
Create a sticky message in a channel.
List every sticky message configured in this server.
Pause or resume a channel's sticky message without deleting it.
Delete a channel's sticky message.
Create a new Tag
Edit an existing tag
Delete a tag
List all tags
Use a tag
Add an alias to a tag
View or edit the parameters for a command tag
Set who can use a custom command tag
Create a ticket panel
Add a form field to a form-based ticket panel
Send a ticket panel message
Delete a ticket panel
List all panels
Add a user to this ticket
Remove a user from this ticket
Set yourself as AFK. Vela will notify anyone who mentions you.
View the help menu
Generate and post this week's newspaper right now
Configure the newspaper for this server
Show a passport
Choose whether other people can view your passport
Report a user's rank card for inappropriate content
Quick status check and links to configure Vela
A bot that answers nothing looks broken, and Discord's review agreed: text commands were off by default, so a reviewer who typed -help into a fresh server got silence, and the listing was declined over a prefix that worked fine once someone knew to turn it on. The silence was the bug.
/prefix disable turns them off. Servers that already chose are left exactly as they were, and the schema default said 1 all along; the code just never agreed with it.
/prefix enable and /prefix disable now recognise the state they are already in. Their "already" checks guarded on a service method that did not exist, so the guard never ran and every call rewrote the same value.
-) instead of !.
Vela outgrew its own process.
The bot and its dashboard used to be one program, which was fine until it was not: one process can only ever be one shard, and Discord requires sharding at 2,500 servers. The manager now spawns the shards and hosts the dashboard itself, so growing means changing a number. Around that sit the things a production service needs and this one did not have: a health endpoint, online backups and a written deployment.
The rest of the release is features. Commands can install to a person instead of a server, moderation gained the pieces it was missing (timed punishments, jail, sticky roles, evidence, appeals), and levelling, economy, giveaways and tickets each closed a gap competitors had and Vela did not.
shard.js is the entry point: it spawns the shards, hosts the dashboard and writes the status row, while each shard is a plain index.js that knows nothing about any of it. Routes reach guilds through a facade over broadcastEval rather than a local cache, and every database driven ticker asks Discord's own shard formula who owns a row, so giveaways end once, punishments lift on the shard that can actually lift them, and check in reminders send once however many shards are running. Shard count is VELA_SHARD_COUNT, defaulting to whatever Discord recommends.
/passport renders who you are across every server you share with Vela.
GET /health answers JSON on the dashboard port: 200 while the shards, database and status writer are alive, 503 naming the part that is not. It is registered ahead of the rate limiter so monitors are never throttled. npm run backup takes an online snapshot that is safe against the live WAL database, with timestamped files and retention. DEPLOYING.md covers the systemd unit, Docker, reverse proxy, backup cron, restore steps and updates.
/appeals file: one open appeal per case, three pending at a time. It lands in the mod log with Uphold and Deny buttons and in the dashboard's Appeals queue at the same time, and the first decision wins wherever it is made. Moderators can work entirely from Discord (/appeals list, /appeals decide, with an optional note), members can track and add to their own (/appeals status, /appeals info, /appeals withdraw), and admins can turn the whole thing off or route filings to their own channel (/appeals config). An upheld appeal deliberately lifts nothing: it is the team's record that they reconsidered, and undoing a punishment stays an explicit act with its own case.
/moderation tempban and /role temp end by themselves, from a database backed schedule that survives a restart. Jail is a containment track separate from a timeout, with setup it performs and checks itself, a dashboard page carrying the config, a live health check and the inmate roster, and channels created after setup sealed automatically. Sticky roles restore what a member had when they rejoin, so leaving and coming back no longer clears a mute.
proof attachment, reposted to a channel the server controls so it outlives Discord's signed links, which expire within about a day. Cases and mod logs both show it.
/starboard top shows the month's brightest.
/moderation slowmode with thirteen presets. /moderation purge preview:true, a dry run that reports what matched and waits behind Delete and Cancel, executing the messages captured at preview time rather than sweeping again. /role remove, and purge filters that compose.
shard.js, not index.js. Self hosters must start the manager. index.js still boots a working bot, but with no dashboard and no website. npm start points at the right one.
/automod view and /automod set replace updateheat and getheat, and the filter commands say "Added to the word filter." rather than describing a containment boundary scrub.
site.css and the behaviour in three static files cut at the original block boundaries, served with a one day cache.
/role link restrictions did nothing. resolveOverride never returns null (with no stored row it hands back a populated default), so the branch holding the only linked role check was unreachable from the day command overrides were added. Both routers were still resolving linked roles on every command and discarding the answer.
/manage/ and re-submits it with form.submit(), which has no submitter and so drops a button's own name and value. The appeals form is the only one that carries its answer that way, so Uphold and Deny arrived with no decision and were rejected. It only happened with sound on and a window at least 768px wide, which is why it looked intermittent.
member.user.username off a flattened snapshot and threw a 500 after the database write had already committed. The role panel channel picker filtered snapshot rows with isTextBased(), which plain data does not have, so the list was always empty. Shard evals now race a 15 second timeout, because discord.js never settles an eval whose shard died mid flight and the request hung forever. The manager reaps its children on every exit path instead of leaving token holding processes behind. A punishment whose lift throws is postponed 60 seconds rather than deleted, which used to turn one Discord 5xx into a permanent ban. Jail writes the inmate record before stripping roles.
guildId that was never defined, so every visit died with a ReferenceError, and the render never passed guildId to the view either. The heaviest page in the dashboard now has a test that renders it for real.
command_roles migration re-ran on every boot, and the upsert inside it stamped pre migration role lists back over anything an admin had changed since. It runs once now, flagged in bot_meta.
/pay wraps its debit, credit and ledger entry in one transaction, because a crash between them could charge a sender and never pay the recipient. Item transfers, companion training and lifetime stats got the same, and a case row now lands with its history entry.
The website, and the cards that link to it, finally look like one product.
Vela's site moved to violet a while ago. The cards the bot posts into Discord did not: they were still amber and gold, so a rank card and the page it linked to read as two different bots. That is fixed, along with a docs page that was hiding almost everything it knew.
/help does. It was forwarding only a name, a description and the raw options, and throwing away the copy-ready usage line, the prefix form and the required permission that were already being computed for all 250 entries.
softban's time argument actually measures, and that unban wants an id rather than a mention.
/zeeky, with the Vela constellation behind it drawn from real star positions.
/docs#cat-moderation opened the Quick Start tab instead of the commands, because the hash handler only understood two values. Every category and every command now has an anchor that works.
/bot nickname and /bot profile from an outdated call signature.
-warn works again.
3.1.0 stopped resolving bare subcommand names because ten of them belong to several commands at once. That was the wrong trade: it made the other 86 pay for the ten. -warn was never a guess, since exactly one command is called warn and it is moderation warn.
-warn, -ban, -timeout, -purge, -checkin and the rest are back to working the way they always did.
list, add, remove, delete, create, info, enable, member, set and clear, still reply with the full forms rather than running whichever file happened to load last. That was the actual bug in the first place.
Prefix commands stop pretending to be slash commands.
The old path handed every command a fake interaction that reported itself as a slash command so the files would run unmodified. Everything that felt cheap about prefix followed from that one lie: replies floated loose in the channel instead of answering you, ephemeral output was forwarded into ordinary messages, a slow command looked ignored, four commands errored outright, and arguments were read from the help documentation rather than from the command itself. All of it is rebuilt.
Commands can also be hidden now, so /reload is finally out of everyone's command picker.
hidden: true, and Commands/Private/ for bot-developer commands. A hidden command never reaches the global registration, /help, the docs site or the dashboard command page. It still loads and still answers to the prefix. Set DEV_GUILD_ID and hidden commands register as slash commands in that one server, so /reload keeps its autocomplete where the developers are and appears nowhere else. Commands/Private/ implies hidden and, unless the module declares its own tier, developer, so a new dev command cannot leak by forgetting a flag. reload.js moved there.
guildOwner: true, a permission tier above Administrator for commands that should answer only to the person who owns the server. Gated in both routers; bot owner and developer still bypass it.
prefixAliases: [], for a deliberate short prefix name. The loader reports a clash between two commands instead of letting load order settle it.
scripts/verify-prefix.js, 32 checks over specs, parsing, the context and routing. npm run test:prefix runs it alone.
name:value is assigned first; the rest fill in declared order, and a value that cannot belong to an option skips past it. The last unfilled string option absorbs the rest of the line.
prefixAliases.
Commands/Owner/ is now Commands/admin/. The folder holds settings for server owners and administrators, but its name read as bot owner, which is what Commands/Private/ is for. Category comes from the folder name, so those seven commands file under Admin in /help, on the docs site and on the dashboard.
Events/Levels/messageSent.js (750 lines of levelling and routing together, now 209) to Handlers/prefixRouter.js. Both routers build member permissions through one shared helper.
/reload was visible to everyone. It was registered globally like every other command, and listed in /help; the owner check only fired once someone ran it.
guildCreate re-registered the whole payload as guild-scoped commands on join, on top of the global registration, undoing the one-time cleanup that exists in ready.js for exactly this reason.
safeReply and safeError carry a branch for message-like objects that strips the flag, and the fake interaction's claim to be a slash command made that branch unreachable across all 409 call sites.
-case update 42 "he said the thing" put the reason in the case type.
https://… became a flag called https.
-list, -add and eight other names ran whichever file loaded last when they belonged to a single parent, and errored when they belonged to several.
/help was hiding 17 commands and mislabelling others. Subcommands were listed under their bare name, which every parent with a subcommand of that name shares, so list appeared once as Tags while the sticky, rolepanel and case versions had nowhere to show. Worse, that shared key was the merge target, so entries blended across parents: /tags create carried rolepanel's description and its nine options, and /tags delete described a case. Entries are per-parent now, 118 listed rose to 135, and a listed subcommand shows the path you actually type: /tags list, not /list.
hidden. It is built by walking Commands/ rather than from the registry, and only skipped the Private folder by name, so a hidden command anywhere else stayed on show.
Commands/Owner to Commands/admin left the dashboard's category label keyed on the old folder name, dropping "Server Settings" in yellow to a generated "Admin" in cyan.
Features/Safety/optionParser.js. Its eight exported wrappers had two identical branches each, making every one an unconditional pass-through; 23 files imported them and none ever called one. Three of the names those files imported were never exported at all.
The first release since 2.4 to add things rather than repair them. Three arrive together: self assign roles, a tag engine that finally behaves, and three new dashboard pages. Along the way this release closes a hole that let one server's admin take over the bot everywhere, and fixes a bug that made every role gate hand out its role whether or not it should have.
Members pick their own roles from a panel you build. No third party bot, and nothing held back behind a paywall.
Panels come in three styles. Buttons suit up to about ten roles, a dropdown scales to twenty five and gives every role a description, and reactions work the classic way for servers that prefer them.
Each panel also has a behaviour:
On top of that: a role someone must already have to use the panel, a role that bars them from it, and a cap on how many roles one member may hold from a single panel.
Build panels from the dashboard with a live preview showing what Discord will actually render, or from Discord with /rolepanel. Either way the same panel comes out.
Two things worth calling out. Role position is checked again at the moment someone clicks, not only when you build the panel, so a role that later drifts above Vela gives a real reason instead of doing nothing. And a member's roles are written in a single operation, so there is never an instant where somebody holds a contradictory set.
AI Moderation. Confidence thresholds for the severe and toxic tiers, whether severe messages are deleted, what each result from the model counts as, and a list of words the check should skip when a server in joke keeps tripping it. The page names the model in use, and says plainly when no API key is configured, because the check never runs without one.
Economy. Shards in circulation, split between carried and banked, how many members hold a balance, transfer volume over the last week, and a balance table ranked by total wealth. Gambling commands can be switched off. Balances can be adjusted for fixing mistakes and running events, and every adjustment is written to the server log.
Analytics. Moderation actions and new members as daily charts across seven or thirty days, what kind of action was taken, which moderators are busiest, the most active posters, ticket load with an average time to close, and recent changes made from the dashboard. All of it comes from activity Vela already records, so there is nothing to switch on.
Two of those settings had been readable by the bot but impossible to change without editing the database by hand: whether severe messages are deleted, and whether gambling is allowed.
{expr:} executed its contents as real JavaScript. Writing a tag needs Manage Guild, but that permission is per server, so an admin in any one server could read Vela's environment, reach the file system, and run commands on the host. That is control of the bot in every server it is in, reached from a single one.
The check that looked like a guard was not one. Results that were not numbers got discarded, but the code ran either way, and a secret could be read one character at a time through the part that did return numbers.
Expressions are now handled by a purpose built parser that understands numbers, your variables, arithmetic, comparisons, logic, brackets and the Math helpers, and nothing else. There is no route back into JavaScript. Every attack that used to work is now a test.
{if:condition|then|else} picked the right branch to show, then ran both of them. Any role change, message or moderation action sitting in the branch that was not chosen still happened.
For a verification tag this meant the gate never gated. The role went out on every single use, whether or not the member already had it. {and:}, {or:}, {default:} and {fallback:} leaked the same way and now stop early, as they always should have.
{has_role:} and {has_permission:} returned false for every kind of failure: a missing argument, an unknown member, a misspelled permission name. Gates are written as "if they have it say so, otherwise grant it", so false sends execution down the granting branch. A typo quietly turned a closed gate into an open one.
They now give the reason instead of passing it off as an answer, and the member running the tag is read directly rather than from a cache that can forget them. Being forgotten used to read as "does not have the role", which meant handing it over again.
Any command marked owner only or developer only rejected everybody, the owner included. The slash command router tested a value that is never set. The prefix path had always been right, which is why this went unnoticed.
A misspelled function used to show up in your message as plain text. A misspelled variable simply vanished. Neither counted as an error, so a broken tag produced less than intended with nothing anywhere to say why.
Problems now appear underneath the output in the tag tester, and reach the logs when a tag runs for real. Whoever triggered the tag still sees only the result.
{user.mention} also works now. It was documented and in use, but no such value existed, so it rendered as nothing at all.
{loop:}, {repeat:} and {range:} had no ceiling at all. One tag could build a twenty megabyte message instantly. Loops now stop after a thousand rounds, and output is trimmed to Discord's two thousand character limit with a note saying so, which beats the message silently failing to send.
-list and -delete, now tell you which one you might have meant. Previously they ran whichever happened to load last.
None of this is visible in Discord, but it is why the rest of the list exists.
Vela fails loudly now. Missing configuration is named and stops startup rather than surfacing later as something unrelated. A crash exits so the process manager can restart it, instead of staying up in an unknown state. Errors are written to the database, so a problem can be looked into after a restart rather than vanishing with it.
dashboard.js went from 4,971 lines to 879, its routes split across 27 modules. Tests went from 81 checks to 166, including the first the tag engine has ever had. ARCHITECTURE.md now records how the pieces fit together, which was written down nowhere.
[EventLogs] Webhook failed ... Unknown Webhook repeating forever. When a server's log webhook was deleted on Discord's side (someone removing it from Integrations, or deleting and recreating the log channel), the bot logged an error on every single event in that guild, indefinitely.
The old handler cleared webhookCache on error code 10015, which fixed nothing: the dead URL stayed in the database, so the next event rebuilt a fresh WebhookClient against the same deleted webhook and failed again. The only things that stopped it were a bot restart or the guild going quiet.
Vela now repairs itself:
1. Recreates the webhook in the log channel it already has on record (channels.logs_channel_id), writes the new URL back, and delivers the event that triggered the repair so nothing is lost. 2. If it can't (channel deleted, Manage Webhooks revoked, creation refused, or no log channel ever recorded) it switches event logs off for that guild and says so once, instead of retrying forever. Re-enable with /channel set option:logs.
No action needed from the affected server's staff, which matters because there's no way to reach them.
Guards worth naming, since each one is a way this could have gone wrong:
10015 triggers a repair. A transient 503 no longer wipes a perfectly good webhook, which would have turned a Discord blip into permanently lost logging.
Roughly two thirds of every release note was missing from the website. The /docs changelog parser kept only lines beginning with - , and discarded everything else:
CHANGELOG.md hard-wraps at ~80 columns, so any bullet spanning more than one line was cut at the wrap, so entries ended mid-sentence, e.g. "…so it takes the larger model with reasoning" with the rest silently gone.
Measured against the current file: 16,381 of 46,065 characters reached the page: 64% dropped.
CHANGELOG.md is now parsed properly by Features/Data/changelogParser.js: arbitrary section headings with colour derived from the heading, paragraphs, one level of bullet nesting, tables, blockquotes, links, and inline code/bold/italic. HTML is escaped before formatting is applied. Tables scroll inside their own container so a wide row can't push the page sideways on mobile.
108 of the 129 commands listed on /docs were named in a form nobody can type. The generator flattened every subcommand into its own top-level entry called parent.sub, so the page advertised /bot.nickname and /case.view. Discord separates a subcommand with a space, /bot nickname, so anyone copying a command straight off the docs page got nothing.
The same flattening meant cmd.options was never populated, which quietly disabled a large amount of markup that already existed in docs.ejs: the subcommand rows, the per-option chips with types and required markers, and the direct-option grid. None of it had ever rendered.
Now each command file produces one card carrying its real Discord option JSON:
reopen or caseid now finds the right command. Neither was possible before, because that data never reached the page.
/admin/changelog and the changelogs table are gone. The editor wrote three fixed buckets into a table that had drifted to 2 rows stuck on v2.1.0, and since the page now parses CHANGELOG.md directly, nothing read it any more, leaving a working editor whose output was invisible would have been a trap.
CHANGELOG.md first, so no history was lost. The page now goes back to launch, 13 releases.
autodelete and sticky_rotation.
CHANGELOG.md ships with the repo, so an unreadable file means something is badly wrong, and an empty list is the honest answer rather than silently serving stale rows.
scripts/verify-logging.js, wired into npm test (59 checks total). Drives the real manager against a webhook that fails with a genuine 10015 and asserts the loop actually stops: 10 events produce exactly one hit on the dead URL, one replacement webhook, and the other 9 delivered through it. Also covers concurrent failures, every disable path, the repair cap, and that an unrelated 503 leaves the webhook intact.
Groq shuts off llama-3.3-70b-versatile on 2026-08-16. Both call sites that used it, the AI moderation gate and /roast, would have started failing outright. Migrated ahead of the deadline.
| Call site | Was | Now |
|---|---|---|
| AI moderation gate | llama-3.3-70b-versatile | qwen/qwen3.6-27b, reasoning_effort: "none" |
/roast | llama-3.3-70b-versatile | openai/gpt-oss-120b, reasoning_effort: "low" |
The two replacements are not interchangeable here. Groq recommends either model for both use cases, but both are reasoning models and reasoning tokens are billed against the output budget. The moderation gate deliberately caps output low (it only ever emits a two-field JSON verdict). Dropping in a model that can't disable reasoning would let it spend that entire cap thinking and return truncated JSON, so moderation would fail open silently, with nothing in the logs to show it. qwen/qwen3.6-27b is the only one of the two that supports reasoning_effort: "none", so it takes the hot path.
/roast is not latency-critical, so it takes the larger model with reasoning at "low"; max_completion_tokens raised 250 → 1000 to cover reasoning plus the reply.
/roast returning full text with finish=stop.
Features/Data/aiModels.js, overridable per deployment via GROQ_MODERATION_MODEL and GROQ_ROAST_MODEL. They were hardcoded at each call site, which is why this deprecation meant hunting them down individually. The next one is a one-line change or an env var.
.env.example documents both overrides and links Groq's deprecation schedule.
npm test gains 2 checks (42 total): a blocklist that fails the build if any decommissioned Groq model is referenced anywhere in the tree, and a guard that both call sites read from the shared config and set reasoning_effort. The blocklist was confirmed to actually catch a reintroduced model rather than passing vacuously.
/sticky create now opens a modal instead of a slash option. A slash string option can't contain a newline (the user physically can't type one, Enter submits the command) so every sticky was capped at a single line regardless of type. The modal's paragraph field has no such limit.
0x2b2d31, matching nothing else in the bot). The modal now also takes a title, colour (hex, defaults to Vela violet), image URL, and footer text.
Features/Render/utils.js only looked for fonts in /usr/share/fonts/…, empty on Windows, and empty on any Linux host unless someone hand-installed the files. In practice this meant NotoColorEmoji and every custom font were never loaded from anywhere, so every rendered image (rank cards, level-ups, leaderboards, quote images) silently fell back to a system default and no emoji ever rendered in colour. The bundled fonts in Features/Render/fonts/ are now registered directly, with a fallback chain built from whatever wide-coverage fonts the host actually has for scripts the repo doesn't bundle (Cyrillic, Arabic, Hebrew, Hangul, CJK).
Intl.Segmenter, so every emoji (ZWJ sequences, skin tones, flags, keycaps) draws as one glyph in one font.
/quote rewritten to use the shared per-script text renderer instead of plain fillText:
<:name:id>) resolves to :name: instead of printing literally.
npm test gains 3 checks (40 total): bundled fonts actually register, emoji stay grouped into a single font run, and quote images render successfully across a battery of hostile input (mixed scripts, unbreakable URLs, 300-word walls of text, empty content).
Sticky messages now actually work. The feature shipped in an earlier release but had never functioned. This is the first version where /sticky does anything at all.
The audit listed this as "the handler is missing." It was worse than that; there were three independent failures, each verified by reproducing it:
/sticky create crashed on save. It built an array of stickies and passed the array to addSticky(), which reads .type / .content / .cooldown off its argument. Every column bound to undefined and the insert died on NOT NULL constraint failed: sticky_messages.type. The sticky message had already been posted to the channel by that point, so it looked like it worked and then simply never came back. Nothing was ever written to the database.
getIndex() / setIndex() queried a column named index, both a reserved SQL keyword and the wrong name (the real column was rotationIndex), so the statements failed to parse. They were dead code for an unfinished multi-sticky rotation feature; removed rather than repaired, along with the now-unused sticky_rotation table.
Events/Sticky/stickyRepost.js keeps the sticky pinned to the bottom of its channel. It sends the new copy before deleting the old one, so a failed send (rate limit, permissions pulled mid-flight) leaves the previous sticky in place instead of wiping it. Bots and webhooks are ignored so a sticky can't retrigger on itself, mentions are suppressed so it can't mass-ping on every repost, and an in-flight guard means ten rapid messages produce exactly one repost rather than ten racing deletes.
messageCreate handler in the codebase. Registering one was impossible before 2.4.2, when eventHandler still kept a single listener per event name and unbound the previous one.
/sticky list: every sticky in the server with its channel, type, cooldown, active state, and a content preview.
/sticky toggle: pause or resume a sticky without losing its content, clearing the live copy so it isn't stranded mid-backlog.
/sticky delete: removes the config and the live message. Previously only create existed, and create refuses to run when a channel already has a sticky, so the feature was a one-way door even in theory.
cooldown is now bounded (0–3600s) and channel options are restricted to text and announcement channels, so invalid input is rejected by Discord rather than at runtime.
sticky_messages (guildId, channelId): the repost handler queries this on every message in every guild, making it the hottest query in the codebase.
Database/sticky.js rewritten: synchronous like every other service in Database/, prepared statements, correct call signatures, and content stored verbatim instead of being JSON.stringify'd twice.
lastPost and lastMessageId rather than rewriting every column, so a repost can't clobber config with stale values.
npm test is now 37 checks across two suites: scripts/verify.js plus a new scripts/verify-sticky.js that exercises the full sticky lifecycle against a faked Discord channel: cooldown windows, paused stickies, corrupt embed rows, missing EmbedLinks, send-failure ordering, and 10 concurrent messages collapsing to one repost.
The repost handler briefly cached cooldown expiry in memory to avoid a query per message. It duplicated state the row already holds in lastPost and nothing invalidated it, so deleting a sticky and creating a replacement with a shorter cooldown left the old window silently suppressing reposts. The cache is gone, the row lookup is a single indexed read, which was always cheap enough that the optimisation wasn't worth a stale-state bug. Four of the sticky tests exist specifically because they caught it.
Hardening pass over the 🟠/🟡/🟣 tiers of AUDIT-2026-08-23.md. No features.
bot_meta table), but a brand-new global registration can take up to an hour to appear in every Discord client. Existing commands keep working throughout.Functions/fileLoader.js globbed process.cwd(), so pm2 start /srv/vela/index.js, a systemd unit without WorkingDirectory=, or a cron wrapper would load zero commands and zero events and come up as an inert bot with no error. Now resolved from __dirname. Verified by npm test, which loads the command tree with the cwd set outside the project.
PUT applicationGuildCommands per guild, serially, every boot: 200 guilds meant 200 rate-limited REST calls before the bot was usable. Replaced with a single global PUT applicationCommands, plus a one-time sweep that clears the stale guild-scoped registrations so commands don't appear twice.
syncGuild called guild.members.fetch() with no arguments for each guild in parallel: a complete gateway member-chunk stream per guild, on every restart. All it did was pre-create users rows, which Events/Client/UserJoin.js already creates for every joiner and which EconomyService/levelService already create on demand. The backfill now runs only on a guild's first sync.
registry.js builds at module load and require()s every command file; one of those (Commands/utils/help.js) required buildCategories/buildCommandInfo, which required registry.js straight back. Node warned six times on every boot. Both Help modules now require the registry lazily, inside the function. In practice no data was lost: the four fields the cycle dropped (permissionName, examples, subCommand, subcommand) are not exported by help.js, but the warnings are gone and the class of bug is closed.
JSON.parse calls on DB-backed TEXT columns. One malformed row would take down whatever read it, and several sat on hot paths: the messageCreate pipeline (handleHeat's AI label map), the 5-second giveaway ticker (requirements, which had no fallback at all), every log dispatch (logSettings), and every member join (welcome_config.embedJson). Added Features/Safety/safeJson.js and routed 35 call sites through it across 18 files. The remaining 7 were already correctly guarded, or are deliberate validate-by-throw (/admin/embeds/templates).
{filter:}'s comma-separated-list fallback, and /ticket panel-list's "🚨 Corrupted Fields Payload" warning. Both relied on JSON.parse throwing, which safeJson never does.
users was the worst offender: its primary key is (userId, guildId), so guildId is the second column and no leaderboard query could use it: every rank card, leaderboard page and rank lookup was a full table scan plus a sort. Now index-backed: users by guild+level+xp and guild+points, website_logs by guild+created, caseHistory by case and by guild, giveaways by active state, economy_transfers by sender, and tickets by guild. npm test asserts the query plans stay index-backed and free of temp b-tree sorts.
Features/Invites/inviteCreate.js: dead and broken. It was never loaded (it sits outside the Events/ autoload root) and its own require path was wrong (../Features/Invites/… from inside Features/Invites/), so it would have thrown if it ever had been. Events/logs/inviteCreated.js is and always was the real handler.
autodelete table: declared in db.js, referenced by nothing. Dropped only when empty; a deployment holding rows gets a warning and keeps them.
playlists / playlist_songs (owned by the music-lavalink branch, may hold real saved playlists) and sticky_messages / sticky_rotation (a real feature that's missing its handler, see below, not a schema to delete).
npm start runs node index.js instead of nodemon index.js, and nodemon moved to devDependencies where it belongs. npm run dev still uses it.
bot_meta table for bot-level (not guild-scoped) key/value state such as one-time migration flags.
scripts/verify-security.js → scripts/verify.js, now 23 checks covering this release too: foreign-cwd startup, circular-dependency absence, index coverage via EXPLAIN QUERY PLAN, schema cleanup, and safeJson edge cases.
/sticky create writes a row and nothing ever reposts it. The fix is a new messageCreate handler: a feature change, not a hardening fix, so it's not in this release.
dashboard.js reads client.guilds.cache in 42 places, every one of which would have to become a client.shard.broadcastEval. That's an architecture change, not a patch, and premature at the current guild count.
add, set, list, …) and the README setup errors from the audit remain open.
The Economy System. Vela's largest single feature: 22 subcommands under /economy, built across six phases. Shipped to production in August but never written up here, so this entry backfills it.
The currency is Shards. Everything below is per-server: balances, inventory, companions and achievements do not carry between guilds.
/economy checkin: daily Signal Check-In with a streak counter. Opt in to a DM reminder when your cooldown clears.
/economy patrol: scavenge the void on a short cooldown for a variable payout, with random events.
/economy balance, /economy archive: check anyone's balance, or the server Shard leaderboard.
/economy pay: send Shards to another member. Carries a 2% transfer tax, a 10,000/day cap, and a 7-day account-age gate to blunt alt farming.
/economy deposit, /economy withdraw, and /economy upgrade to raise your capacity through four tiers: Common (250) → Rare (1,000) → Epic (5,000) → Legendary (25,000).
/economy shop and /economy buy: 25 items across three rarities (10 common, 8 rare, 7 epic), split into 5 consumables and 20 cosmetics.
/economy inventory: active boosts, profile title, and owned cosmetics.
/economy settitle: a custom profile title for 1,000 Shards.
Cosmetics are not just inventory entries. They change how your rank card renders.
/economy equip: apply an owned cosmetic to your card.
/economy quests: 3 daily quests drawn from a pool of 6, with progress tracking and Shard rewards.
/economy craft: combine 3 owned cosmetics into 1 of a higher rarity. The sink that gives duplicate cosmetics a purpose.
Gambling is opt-out per server: admins can disable it with the gamblingEnabled setting, and every gambling command respects it.
/economy starroll: coin flip, 48% to win, 2x payout.
/economy astralwager: 1–100 dice roll, win on 52+, 2x payout. The ~2% house edge lives in the threshold, not the payout.
/economy duel: wager Shards against another member. Both players pick a stance in private (Void Strike, Starlight Guard, Ember Surge) and the winner takes the pot minus a 5% rake.
/economy trade: offer a cosmetic to another member for a cosmetic or for Shards. Both sides are re-verified at accept time.
/economy adopt: one free companion per server, from three species: Void Raven, Bioluminescent Jelly, and Star-Wisp.
/economy train: +1 companion level for 100 Shards on a 4h cooldown.
/economy achievements: 8 achievements tracking lifetime stats, which roll up into a Sentinel Rank.
/setup hub command, and a quickstart section in the docs.
Security release. Everything here comes out of the full-codebase audit in AUDIT-2026-08-23.md, which closed the whole 🔴 critical tier. No features.
vela.sid. Users simply log in again.state parameter, so an attacker could feed a victim a /auth/callback link holding the attacker's own authorization code and silently log the victim into the attacker's Discord account. /login now mints a 32-byte random state bound to the browser's session, and /auth/callback rejects anything that doesn't match it with a timing-safe comparison. The state is single-use, cleared before validation, so a replayed callback can never match twice.
/auth/callback never checked whether Discord actually returned a token. On failure, access_token was undefined, /users/@me returned an error object, and that object was written straight into req.session.user. It was truthy, so requireAuth let it through as a session with no user ID. Every response in the chain is now status-checked, and the profile is rejected unless it carries a real id.
Secure in production: it was gated on NODE_ENV === "production", and NODE_ENV is set nowhere in .env or .env.example, so production shipped a non-Secure cookie. Now secure: "auto", which derives the flag from req.secure via the already-configured trust proxy, correct behind Caddy, and still workable over plain HTTP in dev.
Database/sessionStore.js, a small store built on the better-sqlite3 handle the rest of the bot already uses. The default store never reaps expired sessions (it grows unbounded for the life of the process) and dropped every logged-in user on restart. Sessions now survive restarts and are swept every 15 minutes. No new dependency.
<script> block:
views/roles.ejs: Discord role names. Anyone with Manage Roles could plant a payload that fires in the browser of every dashboard admin who opens the roles page.
views/tags.ejs: tag content. The existing sanitiser was a no-op: .replace(/<\//g, '<\/') replaces </ with </, because '<\/' is just '</' in JavaScript.
views/tags.ejs: guild name and bot name.
jsonScript() serialiser in dashboard.js, exposed to every view via res.locals. It escapes <, >, &, U+2028 and U+2029 to \uXXXX, which keeps the output valid JSON and valid JS while making </script> impossible to express. All 10 script-embedded serialisation sites across roles.ejs, tags.ejs, commands.ejs and layout.ejs now route through it, including the four that were already hand-rolling a weaker </script>-only replace.
/economy duel: the challenger's balance was checked before a 60-second accept window and never re-checked. Because addShards() floors at zero, a challenger who spent their balance during that window staked less than the stake, possibly nothing, while the pot still paid out double. Both wallets are now re-verified at accept time, matching the contract /economy trade already used.
finally, so a crash or timeout can't strand it.
Handlers/eventHandler.js kept one listener per event name and removed the existing one before binding. Two files declare messageReactionAdd, so Events/logs/reactionAdd.js loaded second and killed Events/Newspaper/reactionTrack.js: newspaper reaction tracking had never recorded a single reaction (newspaper_top_messages was empty in production). The handler now tracks a list of listeners per event and only clears them on a genuine reload.
GROQ_API_KEY took down the entire bot. Commands/fun/roast.js built its Groq client at require() time and the SDK throws in its constructor when the key is absent, and commandHandler.js requires every command file unguarded, so all 39 commands and every event failed to load. The client is now built lazily on first use, and /roast reports the missing key instead of taking the process with it.
undici, ws, ip-address, brace-expansion), 2 moderate, and 3 low findings.
csurf's vulnerable transitive cookie dependency to ^0.7.2 via a package.json override, clearing the last 2 advisories without the downgrade to csurf@1.2.2 that npm audit fix --force wanted. csurf itself remains deprecated and is still slated for replacement. See the note below.
npm test now runs something. scripts/verify-security.js boots the real dashboard against a stub client and asserts all 16 regressions above stay fixed: OAuth state handling, session storage, cookie flags, CSRF accept and reject paths, event binding counts, XSS containment, and EJS compilation. No framework, no fixtures.
engines.node (>=18) is now declared.
csurf is unmaintained (archived 2022). Its advisories are cleared, but migrating off it changes CSRF behaviour across all 104 routes and belongs in its own change, not a security patch.
'unsafe-inline' for scripts, so it offers no second line of defence behind the XSS fixes above. Removing it means reworking the inline onmouseover= handlers throughout the views.
process.cwd() startup dependency, missing indexes, README setup errors) are untouched.
/manage/:guildId/tags page with a full visual builder, Monaco code editor, and live reference; accessible via the Manage nav
<select> element
{name.mention}, {name.id}, etc.
{give_role:} / {remove_role:} syntax using the first User param for targeting
{abs:n}: absolute value
{max:a|b}: larger of two numbers
{min:a|b}: smaller of two numbers
{capitalize:text}: capitalize first letter only
{title:text}: title-case every word
{number_format:n|decimals}: format number with locale commas; e.g. {number_format:1234567} → 1,234,567
{pad:text|width|align}: pad a string to a fixed width; align left or right (default right)
{delete_message}: queue deletion of the trigger message (prefix commands only; requires bot Manage Messages)
{add_reaction:emoji}: queue an emoji reaction on the trigger message; e.g. {add_reaction:✅}
delete_message / add_reaction wiring: context._target was never set in runTag.js; action functions received undefined target and silently did nothing; now correctly passes the interaction/message object
actionRunner.js: added delete_message and add_reaction case handlers; previously these action types had no handler and were silently dropped
{random}: corrected from "random integer min–max" (wrong) to "context float 0–1 re-rolled per invocation"; use {expr:Math.floor(Math.random()*6)+1} for integer dice rolls
{math:} vs {expr:}: documented distinction: {math:} accepts arithmetic operators only (+, -, *, /, (), digits); {expr:} is the full JS expression evaluator with Math.*, variables, and logic
{index:list|n}: removed (function does not exist)
{first:list} / {last:list}: corrected to note these split by comma, not by array index
{length:text}: noted {len:text} alias
{includes:}: noted {contains:} as alias
{clearvar:name}: added to Variables & Scope section (was missing)
{substr:text|start|length}: added alongside {slice:}; documented different semantics (length vs end-index)
{filter:} / {map:}: corrected syntax: item variable is always {m}, not a named variable argument
/manage/:guildId/channel-control) accessible to all guild admins; shows every text channel grouped by category with live heat bars and lock status badges
30m, 1h, 2d), and add a reason; channel receives an embed notice before the lock is applied
<select>
lockEmbed, unlockEmbed, lockdownEmbed, unlockdownEmbed, and panicEmbed builders; moderator is attributed as username (Dashboard)
parseDuration parser as the slash commands
wlog with channel name, profile, and reason
generateNecroLevelUpCard now accepts a vip object and applies per-tier ring decorations (owner: rainbow rings + crown + sparkles, twin: violet/fuchsia rings, favorite: silver double rings + compass dots, hg: pink rings + heart + orbiting dots), panel border gradients, badge pill, username gradient, accent bar tint, rune tint, and VIP title line
renderLeaderboard accepts an optional vip field; podium cards (rank 1–3) render scaled-down tier decorations; row cards (rank 4–10) render VIP rings; level label is tinted with the VIP glow color
messageSent.js and Commands/Level/leaderboard.js now fetch VIP data via getVipUser and pass it through to the render functions
now before declaration: const now was used before its declaration inside handleHeatCalculation; this caused a ReferenceError whenever Account Age Gate was enabled
syncGuild.js was converting timeout_duration to ms, then handleHeat.js multiplied by 1000 again, producing ~7-day timeouts instead of the configured minutes
serverHeat.set(), so server temperature never climbed past yellow in practice
isDeleteOnly zeroing heat: banned-word messages with action delete contributed zero heat, allowing unlimited spam with no escalation; now contributes base message heat
MentionEveryone permission; Discord blocks the ping anyway without it
commands.ejs and logs.ejs with // lgtm suppressions for browser-side relative-path fetches
role-select.ejs: replaced innerHTML with createElement/createTextNode
layout.ejs and bot-profile.ejs: replaced regex URL guards with explicit startsWith() chains recognized by CodeQL
admin-vip.ejs and handleHeat.js
cardService.js: replaced unbounded (\?.*)?$ with url.split('?')[0]
The Vela Era: rebranded from WAR Bot to Vela.
Dashboard Preview.
Launch.
/rank