fix UI text and command leaderboard links

This commit is contained in:
Franz Rolfsvaag 2026-06-17 21:56:20 +02:00
parent 0cf7408225
commit 471218a79d
18 changed files with 230 additions and 47 deletions

View File

@ -122,9 +122,9 @@ This file tracks larger Lumi work that cannot safely be completed in one pass. K
- Add examples for fields that expect URLs, model names, provider names, paths, selectors, or structured values. - Add examples for fields that expect URLs, model names, provider names, paths, selectors, or structured values.
- Ensure labels and helper text are suitable for non-technical admins without removing important admin-level specificity. - Ensure labels and helper text are suitable for non-technical admins without removing important admin-level specificity.
- Review localization/translation keys if present so simplified wording remains consistent across languages. - Review localization/translation keys if present so simplified wording remains consistent across languages.
## Done ## Done
- 2026-06-17: Fixed user-facing Economy spelling, restored `/stats/{username}` Compare toggling, linked Top commands run leaderboard entries to `/commands`, and bumped core/plugin patch versions.
- 2026-06-17: Fixed custom command Edit buttons and `/commands` Copy Link / expand buttons with delegated handlers, clipboard fallback, and v0.1.5 patch bump. - 2026-06-17: Fixed custom command Edit buttons and `/commands` Copy Link / expand buttons with delegated handlers, clipboard fallback, and v0.1.5 patch bump.
- 2026-06-17: Fixed repo-based core updates deleting `data/update-cache/repo` during apply, added a verification guard, and bumped core package version to v0.1.4. - 2026-06-17: Fixed repo-based core updates deleting `data/update-cache/repo` during apply, added a verification guard, and bumped core package version to v0.1.4.
- 2026-06-17: Bumped core package version to v0.1.3. - 2026-06-17: Bumped core package version to v0.1.3.

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "lumi-bot", "name": "lumi-bot",
"version": "0.1.5", "version": "0.1.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lumi-bot", "name": "lumi-bot",
"version": "0.1.5", "version": "0.1.6",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.12", "adm-zip": "^0.5.12",
"better-sqlite3": "^11.5.0", "better-sqlite3": "^11.5.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "lumi-bot", "name": "lumi-bot",
"version": "0.1.5", "version": "0.1.6",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {

View File

@ -1,6 +1,6 @@
# Birthday Plugin # Birthday Plugin
Standalone Lumi plugin for birthday profile settings, chat commands, Discord birthday announcements, and optional echonomy birthday gifts. Standalone Lumi plugin for birthday profile settings, chat commands, Discord birthday announcements, and optional Economy birthday gifts.
## Install ## Install
@ -39,5 +39,5 @@ Plugin settings are stored in `plugin_settings` with `plugin_id = 'birthday'`.
- Accepted date formats are `YYYY/MM/DD` and `MM/DD`. - Accepted date formats are `YYYY/MM/DD` and `MM/DD`.
- Dash-separated dates and `DD/MM` dates are rejected. - Dash-separated dates and `DD/MM` dates are rejected.
- Default privacy is `limited`. - Default privacy is `limited`.
- If echonomy is not loaded, birthday announcements still work and gifts are skipped. - If Economy is not loaded, birthday announcements still work and gifts are skipped.
- Automatic gifts are delivered once per birthday occurrence. Manual gifts are claimed once per birthday occurrence. - Automatic gifts are delivered once per birthday occurrence. Manual gifts are claimed once per birthday occurrence.

View File

@ -1,7 +1,7 @@
{ {
"id": "birthday", "id": "birthday",
"name": "Birthday", "name": "Birthday",
"version": "0.1.0", "version": "0.1.1",
"description": "Birthday profiles, announcements, lookup commands, and optional birthday currency gifts.", "description": "Birthday profiles, announcements, lookup commands, and optional birthday currency gifts.",
"main": "index.js" "main": "index.js"
} }

View File

@ -4,7 +4,7 @@
<div class="section-header"> <div class="section-header">
<div> <div>
<h1>Birthdays</h1> <h1>Birthdays</h1>
<p class="command-subtitle">Birthday announcements, profile display, and optional echonomy gifts.</p> <p class="command-subtitle">Birthday announcements, profile display, and optional Economy gifts.</p>
</div> </div>
</div> </div>
<div class="table-wrap"> <div class="table-wrap">
@ -12,7 +12,7 @@
<tbody> <tbody>
<tr><th>Discord client</th><td><%= diagnostics.discordAvailable ? (diagnostics.discordReady ? "Ready" : "Available") : "Unavailable" %></td></tr> <tr><th>Discord client</th><td><%= diagnostics.discordAvailable ? (diagnostics.discordReady ? "Ready" : "Available") : "Unavailable" %></td></tr>
<tr><th>Announcement channel</th><td><%= diagnostics.channel.message %></td></tr> <tr><th>Announcement channel</th><td><%= diagnostics.channel.message %></td></tr>
<tr><th>Echonomy</th><td><%= diagnostics.echonomyAvailable ? "Available" : "Unavailable" %></td></tr> <tr><th>Economy</th><td><%= diagnostics.echonomyAvailable ? "Available" : "Unavailable" %></td></tr>
<tr><th>Current plugin date</th><td><%= diagnostics.currentDate.year %>-<%= String(diagnostics.currentDate.month).padStart(2, "0") %>-<%= String(diagnostics.currentDate.day).padStart(2, "0") %></td></tr> <tr><th>Current plugin date</th><td><%= diagnostics.currentDate.year %>-<%= String(diagnostics.currentDate.month).padStart(2, "0") %>-<%= String(diagnostics.currentDate.day).padStart(2, "0") %></td></tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,6 +1,6 @@
{ {
"pluginId": "echonomy-framework", "pluginId": "echonomy-framework",
"pluginName": "Echonomy Framework", "pluginName": "Economy Framework",
"platformKeys": { "platformKeys": {
"discord": "platform_discord", "discord": "platform_discord",
"twitch": "platform_twitch", "twitch": "platform_twitch",
@ -11,7 +11,7 @@
"id": "root", "id": "root",
"trigger": "coins", "trigger": "coins",
"name": "Coins", "name": "Coins",
"description": "Root command for the Echonomy currency system.", "description": "Root command for the Economy currency system.",
"level": "public", "level": "public",
"platforms": ["discord", "twitch", "youtube"], "platforms": ["discord", "twitch", "youtube"],
"triggerKey": "command_root", "triggerKey": "command_root",

View File

@ -240,7 +240,7 @@ module.exports = {
const responses = Object.values(config.responses || {}); const responses = Object.values(config.responses || {});
res.render(path.join(__dirname, "views", "echonomy.ejs"), { res.render(path.join(__dirname, "views", "echonomy.ejs"), {
title: "Echonomy Framework", title: "Economy Framework",
config, config,
user, user,
isAdmin, isAdmin,
@ -733,7 +733,7 @@ module.exports = {
}); });
web.mount(`/plugins/${PLUGIN_ID}`, router, { web.mount(`/plugins/${PLUGIN_ID}`, router, {
label: "Echonomy", label: "Economy",
role: "public", role: "public",
section: "plugins" section: "plugins"
}); });

View File

@ -1,7 +1,7 @@
{ {
"id": "echonomy-framework", "id": "echonomy-framework",
"name": "Echonomy Framework", "name": "Economy Framework",
"version": "0.2.6", "version": "0.2.7",
"description": "Cross-platform currency framework with shared balances and extensible hooks.", "description": "Cross-platform currency framework with shared balances and extensible hooks.",
"main": "index.js" "main": "index.js"
} }

View File

@ -1,13 +1,13 @@
{ {
"pluginId": "echonomy-framework", "pluginId": "echonomy-framework",
"pluginName": "Echonomy Framework", "pluginName": "Economy Framework",
"provider": "stats.js", "provider": "stats.js",
"profile": { "profile": {
"title": "Echonomy", "title": "Economy",
"emptyMessage": "No currency activity yet." "emptyMessage": "No currency activity yet."
}, },
"leaderboards": { "leaderboards": {
"title": "Echonomy", "title": "Economy",
"emptyMessage": "No currency activity yet." "emptyMessage": "No currency activity yet."
} }
} }

View File

@ -131,7 +131,7 @@
<section class="card"> <section class="card">
<div class="section-header"> <div class="section-header">
<div> <div>
<h1>Echonomy Framework</h1> <h1>Economy Framework</h1>
<p class="command-subtitle">Unified, cross-platform currency tooling and stats.</p> <p class="command-subtitle">Unified, cross-platform currency tooling and stats.</p>
<div class="echonomy-currency"> <div class="echonomy-currency">
<% if (config.currency.icon) { %> <% if (config.currency.icon) { %>

View File

@ -1,6 +1,6 @@
{ {
"pluginId": "echonomy-games", "pluginId": "echonomy-games",
"pluginName": "Echonomy Games", "pluginName": "Economy Games",
"commands": [ "commands": [
{ {
"id": "hotpotato", "id": "hotpotato",

View File

@ -138,7 +138,7 @@ module.exports = {
mystery: getGameStatsView(db, "mystery") mystery: getGameStatsView(db, "mystery")
}; };
res.render(path.join(__dirname, "views", "games.ejs"), { res.render(path.join(__dirname, "views", "games.ejs"), {
title: "Echonomy Games", title: "Economy Games",
config, config,
responses, responses,
responsesByGame, responsesByGame,
@ -331,7 +331,7 @@ module.exports = {
}); });
web.mount(`/plugins/${PLUGIN_ID}`, router, { web.mount(`/plugins/${PLUGIN_ID}`, router, {
label: "Echonomy Games", label: "Economy Games",
role: "admin", role: "admin",
section: "plugins" section: "plugins"
}); });
@ -811,7 +811,7 @@ async function handleHotPotato({ ctx, db }) {
const config = getConfig(db); const config = getConfig(db);
const framework = getFramework(); const framework = getFramework();
if (!framework) { if (!framework) {
await ctx.reply("Echonomy framework is not available."); await ctx.reply("Economy framework is not available.");
return true; return true;
} }
if (!config.hotpotato.enabled || !config.hotpotato.platforms[ctx.platform]) { if (!config.hotpotato.enabled || !config.hotpotato.platforms[ctx.platform]) {
@ -1065,7 +1065,7 @@ async function handleCoinflip({ ctx, db }) {
const config = getConfig(db); const config = getConfig(db);
const framework = getFramework(); const framework = getFramework();
if (!framework) { if (!framework) {
await ctx.reply("Echonomy framework is not available."); await ctx.reply("Economy framework is not available.");
return true; return true;
} }
if (!config.coinflip.enabled || !config.coinflip.platforms[ctx.platform]) { if (!config.coinflip.enabled || !config.coinflip.platforms[ctx.platform]) {
@ -1148,7 +1148,7 @@ async function handleMystery({ ctx, db }) {
const config = getConfig(db); const config = getConfig(db);
const framework = getFramework(); const framework = getFramework();
if (!framework) { if (!framework) {
await ctx.reply("Echonomy framework is not available."); await ctx.reply("Economy framework is not available.");
return true; return true;
} }
if (!config.mystery.enabled || !config.mystery.platforms[ctx.platform]) { if (!config.mystery.enabled || !config.mystery.platforms[ctx.platform]) {

View File

@ -1,7 +1,7 @@
{ {
"id": "echonomy-games", "id": "echonomy-games",
"name": "Echonomy Games", "name": "Economy Games",
"version": "0.1.5", "version": "0.1.6",
"description": "Cross-platform mini-games that use the Echonomy currency framework.", "description": "Cross-platform mini-games that use the Economy currency framework.",
"main": "index.js" "main": "index.js"
} }

View File

@ -177,8 +177,8 @@
<section class="card"> <section class="card">
<div class="section-header"> <div class="section-header">
<div> <div>
<h1>Echonomy Games</h1> <h1>Economy Games</h1>
<p class="command-subtitle">Mini-games that spend and reward coins via the Echonomy framework.</p> <p class="command-subtitle">Mini-games that spend and reward coins via the Economy framework.</p>
</div> </div>
<span class="badge <%= frameworkReady ? 'discord' : 'youtube' %>"> <span class="badge <%= frameworkReady ? 'discord' : 'youtube' %>">
<%= frameworkReady ? 'Framework connected' : 'Framework missing' %> <%= frameworkReady ? 'Framework connected' : 'Framework missing' %>

View File

@ -1,7 +1,10 @@
const fs = require("fs");
const path = require("path");
const { db } = require("./db"); const { db } = require("./db");
const { getSetting } = require("./settings"); const { getSetting } = require("./settings");
const { getEnabledPlatformIds } = require("./platforms"); const { getEnabledPlatformIds } = require("./platforms");
const { getPluginLeaderboards } = require("./plugin-stats"); const { getPluginLeaderboards } = require("./plugin-stats");
const { getPlugins, pluginsDir } = require("./plugins");
const coreProviders = new Map(); const coreProviders = new Map();
const coreOrder = []; const coreOrder = [];
@ -215,7 +218,7 @@ async function handleTopCommand({ ctx, settings }) {
const list = board.rows const list = board.rows
.slice(0, 5) .slice(0, 5)
.map((entry, index) => { .map((entry, index) => {
const label = entry.username || entry.label || entry.name || "Unknown"; const label = entry.label || entry.username || entry.name || "Unknown";
return `${index + 1}) ${label} (${entry.value})`; return `${index + 1}) ${label} (${entry.value})`;
}) })
.join(" | "); .join(" | ");
@ -282,6 +285,7 @@ function normalizeRows(rows) {
return { return {
username: row.username || row.user || row.label || row.name || null, username: row.username || row.user || row.label || row.name || null,
label: row.label || row.username || row.name || null, label: row.label || row.username || row.name || null,
href: row.href || row.url || null,
value: formatNumber(value), value: formatNumber(value),
rawValue: Number(value) || 0 rawValue: Number(value) || 0
}; };
@ -322,12 +326,13 @@ function buildPluginProviders({ limit }) {
} }
function slugify(value) { function slugify(value) {
return (value || "") const slug = (value || "")
.toString() .toString()
.toLowerCase() .toLowerCase()
.replace(/[^a-z0-9]+/g, "-") .replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "") .replace(/^-+|-+$/g, "")
.trim(); .trim();
return slug || "command";
} }
function normalizeProviderId(value) { function normalizeProviderId(value) {
@ -400,13 +405,19 @@ function buildCommandUsageRows(limit) {
if (!tableExists("command_usage")) { if (!tableExists("command_usage")) {
return { rows: [], emptyMessage: "No command usage recorded yet." }; return { rows: [], emptyMessage: "No command usage recorded yet." };
} }
const commandIndex = buildCommandIndex();
const prefix = getSetting("command_prefix", "!");
const rows = db const rows = db
.prepare("SELECT command_id, count FROM command_usage ORDER BY count DESC LIMIT ?") .prepare("SELECT command_id, count FROM command_usage ORDER BY count DESC LIMIT ?")
.all(limit) .all(limit)
.map((row) => ({ .map((row) => {
label: formatCommandId(row.command_id), const command = commandIndex.get(row.command_id);
return {
label: command?.label || formatCommandId(row.command_id, prefix),
href: command?.href || null,
value: row.count value: row.count
})); };
});
return { rows, rowType: "command" }; return { rows, rowType: "command" };
} }
@ -425,12 +436,153 @@ function buildCurrencyRows(limit) {
return { rows, valueLabel: getCurrencyLabel() }; return { rows, valueLabel: getCurrencyLabel() };
} }
function formatCommandId(commandId) { function buildCommandIndex() {
const prefix = getSetting("command_prefix", "!");
const index = new Map();
const addEntry = (ids, label, commandPageId) => {
const cleanLabel = normalizeCommandDisplay(label, prefix);
const href = `/commands#cmd-${slugify(commandPageId)}`;
ids
.map((id) => (id || "").toString().trim())
.filter(Boolean)
.forEach((id) => {
if (!index.has(id)) {
index.set(id, { label: cleanLabel, href });
}
});
};
addEntry(["top"], `${prefix}top`, "top");
for (const option of getTopCommandOptions()) {
const subcommand = normalizeSubcommand(option.id);
if (subcommand) {
addEntry([`top:${subcommand}`], `${prefix}top ${subcommand}`, `top:${subcommand}`);
}
}
if (tableExists("custom_commands")) {
const customRows = db
.prepare("SELECT trigger FROM custom_commands WHERE enabled = 1 ORDER BY trigger")
.all();
for (const row of customRows) {
const trigger = normalizeCommandTrigger(row.trigger);
if (trigger) {
addEntry([`custom:${trigger}`], `${prefix}${trigger}`, `custom:${trigger}`);
}
}
}
for (const plugin of listCommandPlugins()) {
const manifestPath = path.join(plugin.path, "cmds.json");
const manifest = readJsonSafe(manifestPath);
if (!manifest || !Array.isArray(manifest.commands)) {
continue;
}
const pluginSettings = getPluginSettingsMap(plugin.id);
for (const command of manifest.commands) {
if (!command || !command.trigger) {
continue;
}
const override = command.triggerKey ? pluginSettings[command.triggerKey] : "";
const trigger = normalizeCommandTrigger(override, command.trigger);
if (!trigger) {
continue;
}
const subcommand = normalizeSubcommand(command.subcommand);
const commandId = `${plugin.id}:${command.id || trigger}`;
const ids = [commandId, command.id];
if (command.id && !command.id.toString().includes(":")) {
ids.push(`${plugin.id}:${command.id}`);
}
if (plugin.id === "echonomy-framework" && command.id === "root") {
ids.push("echonomy:root");
}
if (plugin.id === "echonomy-games" && command.id === "mysterybox") {
ids.push("echonomy-games:mystery");
}
const label = subcommand ? `${prefix}${trigger} ${subcommand}` : `${prefix}${trigger}`;
addEntry(ids, label, commandId);
}
}
return index;
}
function listCommandPlugins() {
try {
const enabled = getPlugins()
.filter((plugin) => plugin.enabled)
.map((plugin) => ({
id: plugin.id,
name: plugin.name,
path: plugin.path
}))
.filter((plugin) => plugin.id && plugin.path);
if (enabled.length) {
return enabled;
}
} catch {
// Fall back to filesystem manifests below.
}
if (!fs.existsSync(pluginsDir)) {
return [];
}
return fs
.readdirSync(pluginsDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => {
const dir = path.join(pluginsDir, entry.name);
const manifest = readJsonSafe(path.join(dir, "plugin.json"));
return manifest?.id ? { id: manifest.id, name: manifest.name, path: dir } : null;
})
.filter(Boolean);
}
function readJsonSafe(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch {
return null;
}
}
function getPluginSettingsMap(pluginId) {
if (!tableExists("plugin_settings")) {
return {};
}
const rows = db
.prepare("SELECT key, value FROM plugin_settings WHERE plugin_id = ?")
.all(pluginId);
return Object.fromEntries(rows.map((row) => [row.key, row.value]));
}
function normalizeCommandTrigger(value, fallback = "") {
const raw = (value || fallback || "").toString().trim().toLowerCase();
const firstToken = raw.replace(/^!+/, "").split(/\s+/)[0] || "";
return firstToken.replace(/[^a-z0-9_-]/g, "");
}
function normalizeSubcommand(value) {
const raw = (value || "").toString().trim().toLowerCase();
const firstToken = raw.replace(/^!+/, "").split(/\s+/)[0] || "";
return firstToken.replace(/[^a-z0-9_-]/g, "");
}
function normalizeCommandDisplay(value, prefix) {
const raw = (value || "").toString().trim();
if (!raw) {
return `${prefix}unknown`;
}
return raw.startsWith(prefix) ? raw : `${prefix}${raw.replace(/^!+/, "")}`;
}
function formatCommandId(commandId, prefix = "!") {
if (!commandId) { if (!commandId) {
return "Unknown"; return "Unknown";
} }
if (commandId.startsWith("custom:")) { if (commandId.startsWith("custom:")) {
return `!${commandId.slice(7)}`; return `${prefix}${commandId.slice(7)}`;
} }
return commandId; return commandId;
} }

View File

@ -490,13 +490,39 @@
}); });
} }
const compareToggle = document.querySelector("[data-compare-toggle]"); const compareToggles = document.querySelectorAll("[data-compare-toggle]");
if (compareToggle) { if (compareToggles.length) {
const defaultLabel = compareToggle.textContent.trim(); const defaultStats = document.querySelector("[data-stats-default]");
const altLabel = compareToggle.getAttribute("data-compare-label") || "Back"; const compareStats = document.querySelector("[data-stats-compare]");
compareToggle.addEventListener("click", () => { const setCompareMode = (active) => {
const active = document.body.classList.toggle("stats-compare-mode"); document.body.classList.toggle("stats-compare-mode", active);
compareToggle.textContent = active ? altLabel : defaultLabel; if (defaultStats) {
defaultStats.hidden = active;
defaultStats.setAttribute("aria-hidden", active ? "true" : "false");
}
if (compareStats) {
compareStats.hidden = !active;
compareStats.setAttribute("aria-hidden", active ? "false" : "true");
}
compareToggles.forEach((button) => {
if (!button.dataset.compareDefaultLabel) {
button.dataset.compareDefaultLabel = button.textContent.trim();
}
const defaultLabel = button.dataset.compareDefaultLabel || "Compare";
const altLabel = button.getAttribute("data-compare-label") || "Back";
button.textContent = active ? altLabel : defaultLabel;
button.setAttribute("aria-expanded", active ? "true" : "false");
});
};
setCompareMode(false);
document.addEventListener("click", (event) => {
const toggle = event.target.closest("[data-compare-toggle]");
if (!toggle) {
return;
}
event.preventDefault();
setCompareMode(!document.body.classList.contains("stats-compare-mode"));
}); });
} }

View File

@ -36,14 +36,19 @@
</thead> </thead>
<tbody> <tbody>
<% board.rows.forEach((entry) => { %> <% board.rows.forEach((entry) => { %>
<% const entryLabel = entry.label || entry.username || "Unknown"; %>
<tr> <tr>
<td> <td>
<% if (rowType === "user" && entry.username) { %> <% if (rowType === "user" && entry.username) { %>
<a class="link" href="/stats/<%= encodeURIComponent(entry.username) %>"><%= entry.username %></a> <a class="link" href="/stats/<%= encodeURIComponent(entry.username) %>"><%= entry.username %></a>
<% } else if (rowType === "command") { %> <% } else if (rowType === "command") { %>
<code><%= entry.label || entry.username || "Unknown" %></code> <% if (entry.href) { %>
<a class="link" href="<%= entry.href %>"><code><%= entryLabel %></code></a>
<% } else { %> <% } else { %>
<%= entry.label || entry.username || "Unknown" %> <code><%= entryLabel %></code>
<% } %>
<% } else { %>
<%= entryLabel %>
<% } %> <% } %>
</td> </td>
<td><%= entry.value %></td> <td><%= entry.value %></td>