From 471218a79d15bddac19e631580572741c5888147 Mon Sep 17 00:00:00 2001 From: Franz Rolfsvaag Date: Wed, 17 Jun 2026 21:56:20 +0200 Subject: [PATCH] fix UI text and command leaderboard links --- TODO.md | 2 +- package-lock.json | 4 +- package.json | 2 +- plugins/birthday/README.md | 4 +- plugins/birthday/plugin.json | 2 +- plugins/birthday/views/birthday-admin.ejs | 4 +- plugins/echonomy-framework/cmds.json | 4 +- plugins/echonomy-framework/index.js | 4 +- plugins/echonomy-framework/plugin.json | 4 +- plugins/echonomy-framework/stats.json | 6 +- plugins/echonomy-framework/views/echonomy.ejs | 2 +- plugins/echonomy-games/cmds.json | 2 +- plugins/echonomy-games/index.js | 10 +- plugins/echonomy-games/plugin.json | 6 +- plugins/echonomy-games/views/games.ejs | 4 +- src/services/top.js | 168 +++++++++++++++++- src/web/public/app.js | 40 ++++- src/web/views/leaderboards.ejs | 9 +- 18 files changed, 230 insertions(+), 47 deletions(-) diff --git a/TODO.md b/TODO.md index 82a7ca3..bda54d8 100644 --- a/TODO.md +++ b/TODO.md @@ -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. - 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. - ## 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 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. diff --git a/package-lock.json b/package-lock.json index f4ba4ea..7c69903 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lumi-bot", - "version": "0.1.5", + "version": "0.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lumi-bot", - "version": "0.1.5", + "version": "0.1.6", "dependencies": { "adm-zip": "^0.5.12", "better-sqlite3": "^11.5.0", diff --git a/package.json b/package.json index 667b0bd..8e315b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lumi-bot", - "version": "0.1.5", + "version": "0.1.6", "private": true, "type": "commonjs", "scripts": { diff --git a/plugins/birthday/README.md b/plugins/birthday/README.md index 6c73a21..e554cce 100644 --- a/plugins/birthday/README.md +++ b/plugins/birthday/README.md @@ -1,6 +1,6 @@ # 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 @@ -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`. - Dash-separated dates and `DD/MM` dates are rejected. - 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. diff --git a/plugins/birthday/plugin.json b/plugins/birthday/plugin.json index 9ed3280..d8d1cd5 100644 --- a/plugins/birthday/plugin.json +++ b/plugins/birthday/plugin.json @@ -1,7 +1,7 @@ { "id": "birthday", "name": "Birthday", - "version": "0.1.0", + "version": "0.1.1", "description": "Birthday profiles, announcements, lookup commands, and optional birthday currency gifts.", "main": "index.js" } diff --git a/plugins/birthday/views/birthday-admin.ejs b/plugins/birthday/views/birthday-admin.ejs index f148900..6c3a378 100644 --- a/plugins/birthday/views/birthday-admin.ejs +++ b/plugins/birthday/views/birthday-admin.ejs @@ -4,7 +4,7 @@

Birthdays

-

Birthday announcements, profile display, and optional echonomy gifts.

+

Birthday announcements, profile display, and optional Economy gifts.

@@ -12,7 +12,7 @@ Discord client<%= diagnostics.discordAvailable ? (diagnostics.discordReady ? "Ready" : "Available") : "Unavailable" %> Announcement channel<%= diagnostics.channel.message %> - Echonomy<%= diagnostics.echonomyAvailable ? "Available" : "Unavailable" %> + Economy<%= diagnostics.echonomyAvailable ? "Available" : "Unavailable" %> Current plugin date<%= diagnostics.currentDate.year %>-<%= String(diagnostics.currentDate.month).padStart(2, "0") %>-<%= String(diagnostics.currentDate.day).padStart(2, "0") %> diff --git a/plugins/echonomy-framework/cmds.json b/plugins/echonomy-framework/cmds.json index 8928880..a9a1660 100644 --- a/plugins/echonomy-framework/cmds.json +++ b/plugins/echonomy-framework/cmds.json @@ -1,6 +1,6 @@ { "pluginId": "echonomy-framework", - "pluginName": "Echonomy Framework", + "pluginName": "Economy Framework", "platformKeys": { "discord": "platform_discord", "twitch": "platform_twitch", @@ -11,7 +11,7 @@ "id": "root", "trigger": "coins", "name": "Coins", - "description": "Root command for the Echonomy currency system.", + "description": "Root command for the Economy currency system.", "level": "public", "platforms": ["discord", "twitch", "youtube"], "triggerKey": "command_root", diff --git a/plugins/echonomy-framework/index.js b/plugins/echonomy-framework/index.js index bfc3960..bee8caf 100644 --- a/plugins/echonomy-framework/index.js +++ b/plugins/echonomy-framework/index.js @@ -240,7 +240,7 @@ module.exports = { const responses = Object.values(config.responses || {}); res.render(path.join(__dirname, "views", "echonomy.ejs"), { - title: "Echonomy Framework", + title: "Economy Framework", config, user, isAdmin, @@ -733,7 +733,7 @@ module.exports = { }); web.mount(`/plugins/${PLUGIN_ID}`, router, { - label: "Echonomy", + label: "Economy", role: "public", section: "plugins" }); diff --git a/plugins/echonomy-framework/plugin.json b/plugins/echonomy-framework/plugin.json index 9bc74d9..89f414a 100644 --- a/plugins/echonomy-framework/plugin.json +++ b/plugins/echonomy-framework/plugin.json @@ -1,7 +1,7 @@ { "id": "echonomy-framework", - "name": "Echonomy Framework", - "version": "0.2.6", + "name": "Economy Framework", + "version": "0.2.7", "description": "Cross-platform currency framework with shared balances and extensible hooks.", "main": "index.js" } diff --git a/plugins/echonomy-framework/stats.json b/plugins/echonomy-framework/stats.json index 49b707d..d082c89 100644 --- a/plugins/echonomy-framework/stats.json +++ b/plugins/echonomy-framework/stats.json @@ -1,13 +1,13 @@ { "pluginId": "echonomy-framework", - "pluginName": "Echonomy Framework", + "pluginName": "Economy Framework", "provider": "stats.js", "profile": { - "title": "Echonomy", + "title": "Economy", "emptyMessage": "No currency activity yet." }, "leaderboards": { - "title": "Echonomy", + "title": "Economy", "emptyMessage": "No currency activity yet." } } diff --git a/plugins/echonomy-framework/views/echonomy.ejs b/plugins/echonomy-framework/views/echonomy.ejs index b085bdc..205e812 100644 --- a/plugins/echonomy-framework/views/echonomy.ejs +++ b/plugins/echonomy-framework/views/echonomy.ejs @@ -131,7 +131,7 @@
-

Echonomy Framework

+

Economy Framework

Unified, cross-platform currency tooling and stats.

<% if (config.currency.icon) { %> diff --git a/plugins/echonomy-games/cmds.json b/plugins/echonomy-games/cmds.json index fd9ab5c..a909a57 100644 --- a/plugins/echonomy-games/cmds.json +++ b/plugins/echonomy-games/cmds.json @@ -1,6 +1,6 @@ { "pluginId": "echonomy-games", - "pluginName": "Echonomy Games", + "pluginName": "Economy Games", "commands": [ { "id": "hotpotato", diff --git a/plugins/echonomy-games/index.js b/plugins/echonomy-games/index.js index 3828abf..9eb674d 100644 --- a/plugins/echonomy-games/index.js +++ b/plugins/echonomy-games/index.js @@ -138,7 +138,7 @@ module.exports = { mystery: getGameStatsView(db, "mystery") }; res.render(path.join(__dirname, "views", "games.ejs"), { - title: "Echonomy Games", + title: "Economy Games", config, responses, responsesByGame, @@ -331,7 +331,7 @@ module.exports = { }); web.mount(`/plugins/${PLUGIN_ID}`, router, { - label: "Echonomy Games", + label: "Economy Games", role: "admin", section: "plugins" }); @@ -811,7 +811,7 @@ async function handleHotPotato({ ctx, db }) { const config = getConfig(db); const framework = getFramework(); if (!framework) { - await ctx.reply("Echonomy framework is not available."); + await ctx.reply("Economy framework is not available."); return true; } if (!config.hotpotato.enabled || !config.hotpotato.platforms[ctx.platform]) { @@ -1065,7 +1065,7 @@ async function handleCoinflip({ ctx, db }) { const config = getConfig(db); const framework = getFramework(); if (!framework) { - await ctx.reply("Echonomy framework is not available."); + await ctx.reply("Economy framework is not available."); return true; } if (!config.coinflip.enabled || !config.coinflip.platforms[ctx.platform]) { @@ -1148,7 +1148,7 @@ async function handleMystery({ ctx, db }) { const config = getConfig(db); const framework = getFramework(); if (!framework) { - await ctx.reply("Echonomy framework is not available."); + await ctx.reply("Economy framework is not available."); return true; } if (!config.mystery.enabled || !config.mystery.platforms[ctx.platform]) { diff --git a/plugins/echonomy-games/plugin.json b/plugins/echonomy-games/plugin.json index 5392ece..5f58c17 100644 --- a/plugins/echonomy-games/plugin.json +++ b/plugins/echonomy-games/plugin.json @@ -1,7 +1,7 @@ { "id": "echonomy-games", - "name": "Echonomy Games", - "version": "0.1.5", - "description": "Cross-platform mini-games that use the Echonomy currency framework.", + "name": "Economy Games", + "version": "0.1.6", + "description": "Cross-platform mini-games that use the Economy currency framework.", "main": "index.js" } diff --git a/plugins/echonomy-games/views/games.ejs b/plugins/echonomy-games/views/games.ejs index 557bf8c..2fcd6a0 100644 --- a/plugins/echonomy-games/views/games.ejs +++ b/plugins/echonomy-games/views/games.ejs @@ -177,8 +177,8 @@
-

Echonomy Games

-

Mini-games that spend and reward coins via the Echonomy framework.

+

Economy Games

+

Mini-games that spend and reward coins via the Economy framework.

<%= frameworkReady ? 'Framework connected' : 'Framework missing' %> diff --git a/src/services/top.js b/src/services/top.js index 9bd34fb..1e8277b 100644 --- a/src/services/top.js +++ b/src/services/top.js @@ -1,7 +1,10 @@ +const fs = require("fs"); +const path = require("path"); const { db } = require("./db"); const { getSetting } = require("./settings"); const { getEnabledPlatformIds } = require("./platforms"); const { getPluginLeaderboards } = require("./plugin-stats"); +const { getPlugins, pluginsDir } = require("./plugins"); const coreProviders = new Map(); const coreOrder = []; @@ -215,7 +218,7 @@ async function handleTopCommand({ ctx, settings }) { const list = board.rows .slice(0, 5) .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})`; }) .join(" | "); @@ -282,6 +285,7 @@ function normalizeRows(rows) { return { username: row.username || row.user || row.label || row.name || null, label: row.label || row.username || row.name || null, + href: row.href || row.url || null, value: formatNumber(value), rawValue: Number(value) || 0 }; @@ -322,12 +326,13 @@ function buildPluginProviders({ limit }) { } function slugify(value) { - return (value || "") + const slug = (value || "") .toString() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .trim(); + return slug || "command"; } function normalizeProviderId(value) { @@ -400,13 +405,19 @@ function buildCommandUsageRows(limit) { if (!tableExists("command_usage")) { return { rows: [], emptyMessage: "No command usage recorded yet." }; } + const commandIndex = buildCommandIndex(); + const prefix = getSetting("command_prefix", "!"); const rows = db .prepare("SELECT command_id, count FROM command_usage ORDER BY count DESC LIMIT ?") .all(limit) - .map((row) => ({ - label: formatCommandId(row.command_id), - value: row.count - })); + .map((row) => { + const command = commandIndex.get(row.command_id); + return { + label: command?.label || formatCommandId(row.command_id, prefix), + href: command?.href || null, + value: row.count + }; + }); return { rows, rowType: "command" }; } @@ -425,12 +436,153 @@ function buildCurrencyRows(limit) { 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) { return "Unknown"; } if (commandId.startsWith("custom:")) { - return `!${commandId.slice(7)}`; + return `${prefix}${commandId.slice(7)}`; } return commandId; } diff --git a/src/web/public/app.js b/src/web/public/app.js index 1e83dd2..4a791ba 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -490,13 +490,39 @@ }); } - const compareToggle = document.querySelector("[data-compare-toggle]"); - if (compareToggle) { - const defaultLabel = compareToggle.textContent.trim(); - const altLabel = compareToggle.getAttribute("data-compare-label") || "Back"; - compareToggle.addEventListener("click", () => { - const active = document.body.classList.toggle("stats-compare-mode"); - compareToggle.textContent = active ? altLabel : defaultLabel; + const compareToggles = document.querySelectorAll("[data-compare-toggle]"); + if (compareToggles.length) { + const defaultStats = document.querySelector("[data-stats-default]"); + const compareStats = document.querySelector("[data-stats-compare]"); + const setCompareMode = (active) => { + document.body.classList.toggle("stats-compare-mode", active); + 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")); }); } diff --git a/src/web/views/leaderboards.ejs b/src/web/views/leaderboards.ejs index c0d7316..f3ce962 100644 --- a/src/web/views/leaderboards.ejs +++ b/src/web/views/leaderboards.ejs @@ -36,14 +36,19 @@ <% board.rows.forEach((entry) => { %> + <% const entryLabel = entry.label || entry.username || "Unknown"; %> <% if (rowType === "user" && entry.username) { %> <%= entry.username %> <% } else if (rowType === "command") { %> - <%= entry.label || entry.username || "Unknown" %> + <% if (entry.href) { %> + <%= entryLabel %> <% } else { %> - <%= entry.label || entry.username || "Unknown" %> + <%= entryLabel %> + <% } %> + <% } else { %> + <%= entryLabel %> <% } %> <%= entry.value %>