Command Reference
136 commands across 17 categories — auto-generated from source.
Showing all 136 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 — no command syntax to remember.
Browse every command by category, or look up any command's exact options with /help command:<name>.
Leveling
8 commandsServer Settings
20 commandsBot Info
5 commandsCase Management
13 commandsChannels
1 commandDashboard
1 commandEconomy
22 commandsFun & Social
10 commandsGiveaways
3 commandsLogging
1 commandModeration
12 commandsRole Management
9 commandsRolepanel
6 commandsSticky Messages
4 commandsTags
8 commandsTickets
7 commandsUtilities
6 commandsThe 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 — that 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 — 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 — 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