fix UI text and command leaderboard links
This commit is contained in:
parent
0cf7408225
commit
471218a79d
2
TODO.md
2
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.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lumi-bot",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<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 class="table-wrap">
|
||||
@ -12,7 +12,7 @@
|
||||
<tbody>
|
||||
<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>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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
});
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@
|
||||
<section class="card">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h1>Echonomy Framework</h1>
|
||||
<h1>Economy Framework</h1>
|
||||
<p class="command-subtitle">Unified, cross-platform currency tooling and stats.</p>
|
||||
<div class="echonomy-currency">
|
||||
<% if (config.currency.icon) { %>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"pluginId": "echonomy-games",
|
||||
"pluginName": "Echonomy Games",
|
||||
"pluginName": "Economy Games",
|
||||
"commands": [
|
||||
{
|
||||
"id": "hotpotato",
|
||||
|
||||
@ -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]) {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -177,8 +177,8 @@
|
||||
<section class="card">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h1>Echonomy Games</h1>
|
||||
<p class="command-subtitle">Mini-games that spend and reward coins via the Echonomy framework.</p>
|
||||
<h1>Economy Games</h1>
|
||||
<p class="command-subtitle">Mini-games that spend and reward coins via the Economy framework.</p>
|
||||
</div>
|
||||
<span class="badge <%= frameworkReady ? 'discord' : 'youtube' %>">
|
||||
<%= frameworkReady ? 'Framework connected' : 'Framework missing' %>
|
||||
|
||||
@ -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),
|
||||
.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;
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -36,14 +36,19 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<% board.rows.forEach((entry) => { %>
|
||||
<% const entryLabel = entry.label || entry.username || "Unknown"; %>
|
||||
<tr>
|
||||
<td>
|
||||
<% if (rowType === "user" && entry.username) { %>
|
||||
<a class="link" href="/stats/<%= encodeURIComponent(entry.username) %>"><%= entry.username %></a>
|
||||
<% } 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 { %>
|
||||
<%= entry.label || entry.username || "Unknown" %>
|
||||
<code><%= entryLabel %></code>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<%= entryLabel %>
|
||||
<% } %>
|
||||
</td>
|
||||
<td><%= entry.value %></td>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user