Lumi/plugins/echonomy-framework/views/echonomy.ejs
2026-05-30 20:37:42 +02:00

769 lines
24 KiB
Plaintext

<%- include("../../../src/web/views/partials/layout-top", { title }) %>
<style>
.echonomy-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.echonomy-card {
padding: 14px;
border-radius: 14px;
background: var(--surface-2);
display: flex;
flex-direction: column;
gap: 6px;
}
.echonomy-label {
color: var(--ink-soft);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.echonomy-value {
font-size: 1.4rem;
font-weight: 700;
}
.echonomy-currency {
display: inline-flex;
align-items: center;
gap: 10px;
}
.echonomy-currency img {
width: 32px;
height: 32px;
border-radius: 8px;
object-fit: cover;
}
.echonomy-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.echonomy-list li {
background: var(--surface-2);
padding: 10px 12px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.echonomy-table td small {
color: var(--ink-soft);
}
.response-grid {
display: grid;
gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.response-item {
background: var(--surface-2);
border-radius: 14px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
}
.response-preview {
display: flex;
flex-direction: column;
gap: 6px;
color: var(--ink-soft);
font-size: 0.9rem;
}
.response-rows {
display: grid;
gap: 8px;
}
.response-row {
display: grid;
gap: 8px;
grid-template-columns: 1fr 90px auto;
align-items: center;
}
.response-row input {
width: 100%;
}
.response-row button {
justify-self: end;
}
.response-note {
font-size: 0.85rem;
color: var(--ink-soft);
}
.uuid-chip {
border: 1px solid var(--border);
background: transparent;
color: inherit;
border-radius: 8px;
padding: 2px 8px;
font-size: 0.8rem;
cursor: pointer;
}
.uuid-chip:hover {
border-color: var(--ink-soft);
}
.tx-note-details {
margin: 0;
}
.tx-note-details summary {
cursor: pointer;
color: var(--ink);
text-decoration: underline;
text-underline-offset: 2px;
}
.tx-note-details ul {
margin: 6px 0 0;
padding-left: 18px;
color: var(--ink-soft);
}
.tx-note-period {
margin-top: 6px;
color: var(--ink-soft);
font-size: 0.85rem;
}
</style>
<section class="card">
<div class="section-header">
<div>
<h1>Echonomy Framework</h1>
<p class="command-subtitle">Unified, cross-platform currency tooling and stats.</p>
<div class="echonomy-currency">
<% if (config.currency.icon) { %>
<img src="<%= config.currency.icon %>" alt="Currency icon" />
<% } %>
<strong><%= config.currency.name %></strong>
<span class="hint">(<%= config.currency.plural %>)</span>
</div>
</div>
</div>
</section>
<section class="card">
<h2>Overview</h2>
<div class="echonomy-grid">
<div class="echonomy-card">
<span class="echonomy-label">Your balance</span>
<span class="echonomy-value"><%= userBalance %></span>
</div>
<div class="echonomy-card">
<span class="echonomy-label">Command root</span>
<span class="echonomy-value">!<%= config.command.root %></span>
</div>
<div class="echonomy-card">
<span class="echonomy-label">Cooldown</span>
<span class="echonomy-value"><%= config.cooldownSeconds %>s</span>
</div>
<% if (isAdmin) { %>
<div class="echonomy-card">
<span class="echonomy-label">Total in circulation</span>
<span class="echonomy-value"><%= globalStats.totalBalance %></span>
</div>
<div class="echonomy-card">
<span class="echonomy-label">Total spent</span>
<span class="echonomy-value"><%= globalStats.totalSpent %></span>
</div>
<% } %>
</div>
</section>
<% if (isAdmin) { %>
<section class="card">
<h2>Currency settings</h2>
<form method="post" action="/plugins/echonomy-framework/settings/currency" class="form-grid">
<div class="field">
<label>Currency name (singular)</label>
<input name="currency_name" value="<%= config.currency.name %>" />
</div>
<div class="field">
<label>Currency name (plural)</label>
<input name="currency_name_plural" value="<%= config.currency.plural %>" />
</div>
<div class="field">
<label>Command root</label>
<input name="command_root" value="<%= config.command.root %>" />
<span class="hint">Example: coins, souls, shards</span>
</div>
<div class="field">
<label>Command aliases</label>
<input name="command_aliases" value="<%= config.command.aliases.join(', ') %>" />
<span class="hint">Comma separated aliases that also trigger the root command.</span>
</div>
<button type="submit" class="button">Save currency</button>
</form>
</section>
<section class="card">
<h2>Currency icon</h2>
<form method="post" action="/plugins/echonomy-framework/settings/icon" enctype="multipart/form-data" class="form-grid">
<div class="field">
<label>Upload PNG icon</label>
<input type="file" name="currency_icon" accept="image/png" />
<span class="hint">PNG only. Used across the WebUI.</span>
</div>
<button type="submit" class="button">Upload icon</button>
</form>
</section>
<section class="card">
<h2>Banking labels</h2>
<form method="post" action="/plugins/echonomy-framework/settings/banking" class="form-grid">
<div class="field">
<label>Banking page label</label>
<input name="banking_label" value="<%= config.banking.label %>" />
<span class="hint">Shown on the profile page button and the banking page title.</span>
</div>
<div class="field">
<label>Enable banking page for users</label>
<label class="switch">
<input
type="checkbox"
class="switch-input"
name="banking_enabled"
<%= config.banking.enabled ? 'checked' : '' %>
/>
<span class="switch-track" aria-hidden="true"></span>
<span class="switch-text"><%= config.banking.enabled ? 'Enabled' : 'Disabled' %></span>
</label>
</div>
<div class="field">
<label>Community fund label (singular)</label>
<input name="community_fund_name" value="<%= config.communityFunds.name %>" />
</div>
<div class="field">
<label>Community fund label (plural)</label>
<input name="community_fund_name_plural" value="<%= config.communityFunds.plural %>" />
</div>
<button type="submit" class="button">Save labels</button>
</form>
</section>
<section class="card">
<h2>Platforms</h2>
<form method="post" action="/plugins/echonomy-framework/settings/platforms" class="form-grid">
<div class="field">
<label>Enable on Discord</label>
<label class="switch">
<input
type="checkbox"
class="switch-input"
name="platform_discord"
<%= config.platforms.discord ? 'checked' : '' %>
/>
<span class="switch-track" aria-hidden="true"></span>
<span class="switch-text"><%= config.platforms.discord ? 'On' : 'Off' %></span>
</label>
</div>
<div class="field">
<label>Enable on Twitch</label>
<label class="switch">
<input
type="checkbox"
class="switch-input"
name="platform_twitch"
<%= config.platforms.twitch ? 'checked' : '' %>
/>
<span class="switch-track" aria-hidden="true"></span>
<span class="switch-text"><%= config.platforms.twitch ? 'On' : 'Off' %></span>
</label>
</div>
<div class="field">
<label>Enable on YouTube</label>
<label class="switch">
<input
type="checkbox"
class="switch-input"
name="platform_youtube"
<%= config.platforms.youtube ? 'checked' : '' %>
/>
<span class="switch-track" aria-hidden="true"></span>
<span class="switch-text"><%= config.platforms.youtube ? 'On' : 'Off' %></span>
</label>
</div>
<button type="submit" class="button">Save platform access</button>
</form>
</section>
<section class="card">
<h2>Currency earning rules</h2>
<form method="post" action="/plugins/echonomy-framework/settings/earn" class="form-grid">
<div class="field">
<label>Discord message rewards</label>
<label class="switch">
<input
type="checkbox"
class="switch-input"
name="earn_discord_message_enabled"
<%= config.earn.discordMessage.enabled ? 'checked' : '' %>
/>
<span class="switch-track" aria-hidden="true"></span>
<span class="switch-text"><%= config.earn.discordMessage.enabled ? 'On' : 'Off' %></span>
</label>
</div>
<div class="field">
<label>Discord message amount</label>
<input name="earn_discord_message_amount" value="<%= config.earn.discordMessage.amount %>" />
</div>
<div class="field">
<label>Discord message cooldown (seconds)</label>
<input name="earn_discord_message_cooldown" value="<%= config.earn.discordMessage.cooldown %>" />
</div>
<div class="field">
<label>Twitch chat rewards</label>
<label class="switch">
<input
type="checkbox"
class="switch-input"
name="earn_twitch_message_enabled"
<%= config.earn.twitchMessage.enabled ? 'checked' : '' %>
/>
<span class="switch-track" aria-hidden="true"></span>
<span class="switch-text"><%= config.earn.twitchMessage.enabled ? 'On' : 'Off' %></span>
</label>
</div>
<div class="field">
<label>Twitch message amount</label>
<input name="earn_twitch_message_amount" value="<%= config.earn.twitchMessage.amount %>" />
</div>
<div class="field">
<label>Twitch message cooldown (seconds)</label>
<input name="earn_twitch_message_cooldown" value="<%= config.earn.twitchMessage.cooldown %>" />
</div>
<div class="field">
<label>Discord voice presence rewards</label>
<label class="switch">
<input
type="checkbox"
class="switch-input"
name="earn_discord_voice_enabled"
<%= config.earn.discordVoice.enabled ? 'checked' : '' %>
/>
<span class="switch-track" aria-hidden="true"></span>
<span class="switch-text"><%= config.earn.discordVoice.enabled ? 'On' : 'Off' %></span>
</label>
</div>
<div class="field">
<label>Voice reward per tick</label>
<input name="earn_discord_voice_amount_per_min" value="<%= config.earn.discordVoice.amountPerMin %>" />
</div>
<div class="field">
<label>Voice tick minutes</label>
<input name="earn_discord_voice_tick_minutes" value="<%= config.earn.discordVoice.tickMinutes %>" />
</div>
<button type="submit" class="button">Save earning rules</button>
</form>
</section>
<section class="card">
<h2>Monetization tiers</h2>
<form method="post" action="/plugins/echonomy-framework/settings/tiers" class="form-grid">
<div class="field">
<label>Discord server booster multiplier</label>
<input name="tier_discord_booster_multiplier" value="<%= config.tiers.discordBooster %>" />
</div>
<div class="field">
<label>Twitch subscriber multiplier</label>
<input name="tier_twitch_sub_multiplier" value="<%= config.tiers.twitchSub %>" />
</div>
<div class="field">
<label>Twitch moderator multiplier</label>
<input name="tier_twitch_mod_multiplier" value="<%= config.tiers.twitchMod %>" />
</div>
<div class="field">
<label>Twitch VIP multiplier</label>
<input name="tier_twitch_vip_multiplier" value="<%= config.tiers.twitchVip %>" />
</div>
<div class="field">
<label>Twitch broadcaster multiplier</label>
<input name="tier_twitch_broadcaster_multiplier" value="<%= config.tiers.twitchBroadcaster %>" />
</div>
<button type="submit" class="button">Save multipliers</button>
</form>
</section>
<section class="card">
<h2><%= config.communityFunds.plural %></h2>
<% if (!funds.length) { %>
<p>No <%= config.communityFunds.plural.toLowerCase() %> configured yet.</p>
<% } else { %>
<ul class="echonomy-list">
<% funds.forEach((fund) => { %>
<li>
<span><strong><%= fund.name %></strong> - <%= fund.current_amount %>/<%= fund.target_amount %></span>
<span class="hint"><%= fund.description || '' %></span>
</li>
<% }) %>
</ul>
<% } %>
<% if (isAdmin) { %>
<h3>Create <%= config.communityFunds.name %></h3>
<form method="post" action="/plugins/echonomy-framework/funds/create" class="form-grid">
<div class="field">
<label>Name</label>
<input name="name" />
</div>
<div class="field">
<label>Description</label>
<input name="description" />
</div>
<div class="field">
<label>Target amount</label>
<input name="target_amount" value="0" />
</div>
<button type="submit" class="button">Create fund</button>
</form>
<h3>Update <%= config.communityFunds.plural %></h3>
<% funds.forEach((fund) => { %>
<form method="post" action="/plugins/echonomy-framework/funds/<%= fund.id %>/update" class="form-grid">
<div class="field">
<label>Name</label>
<input name="name" value="<%= fund.name %>" />
</div>
<div class="field">
<label>Description</label>
<input name="description" value="<%= fund.description || '' %>" />
</div>
<div class="field">
<label>Target</label>
<input name="target_amount" value="<%= fund.target_amount %>" />
</div>
<div class="field">
<label>Status</label>
<input name="status" value="<%= fund.status %>" />
</div>
<button type="submit" class="button subtle">Update fund</button>
</form>
<% }) %>
<% } %>
</section>
<% } %>
<% if (isAdmin) { %>
<section class="card">
<h2>Event rewards</h2>
<% if (!events.length) { %>
<p>No custom events configured yet.</p>
<% } else { %>
<ul class="echonomy-list">
<% events.forEach((event) => { %>
<li>
<span><strong><%= event.name %></strong> (<%= event.amount %>)</span>
<form method="post" action="/plugins/echonomy-framework/events/<%= event.id %>/delete">
<button type="submit" class="button subtle">Delete</button>
</form>
</li>
<% }) %>
</ul>
<% } %>
<form method="post" action="/plugins/echonomy-framework/events/create" class="form-grid">
<div class="field">
<label>Event name</label>
<input name="name" />
</div>
<div class="field">
<label>Reward amount</label>
<input name="amount" value="0" />
</div>
<button type="submit" class="button">Add event</button>
</form>
</section>
<section class="card">
<div class="section-header">
<div>
<h2>Response templates</h2>
<p class="hint">Customize bot replies. Tokens: {amount_text}, {balance_text}, {target}, {fund}, {lines}, {cooldown}, {usage}, {help}.</p>
</div>
</div>
<div class="response-grid">
<% responses.forEach((response) => { %>
<div class="response-item">
<strong><%= response.label %></strong>
<div class="response-preview">
<% response.replies.slice(0, 2).forEach((reply) => { %>
<span>• <%= reply.text %></span>
<% }) %>
<% if (response.replies.length > 2) { %>
<span>…and <%= response.replies.length - 2 %> more</span>
<% } %>
</div>
<button type="button" class="button subtle" data-response-open="<%= response.key %>">Edit responses</button>
</div>
<div class="modal-backdrop" data-response-modal="<%= response.key %>" aria-hidden="true">
<div class="modal">
<div class="modal-header">
<h3>Edit: <%= response.label %></h3>
<button type="button" class="button subtle" data-modal-close>Close</button>
</div>
<form method="post" action="/plugins/echonomy-framework/settings/responses" data-response-form>
<input type="hidden" name="response_key" value="<%= response.key %>" />
<div class="field">
<label>Selection mode</label>
<select name="response_mode" data-response-mode>
<option value="random" <%= response.mode === 'random' ? 'selected' : '' %>>Random</option>
<option value="weighted" <%= response.mode === 'weighted' ? 'selected' : '' %>>Weighted</option>
</select>
</div>
<div class="response-rows" data-response-rows>
<% response.replies.forEach((reply) => { %>
<div class="response-row" data-response-row>
<input name="response_text" value="<%= reply.text %>" />
<input name="response_weight" value="<%= reply.weight || 1 %>" />
<button type="button" class="button subtle" data-response-remove>Remove</button>
</div>
<% }) %>
</div>
<button type="button" class="button subtle" data-response-add>Add response</button>
<div class="response-note">Weights are used only when "Weighted" is selected.</div>
<div class="modal-actions">
<button type="submit" class="button">Save</button>
</div>
</form>
</div>
</div>
<% }) %>
</div>
</section>
<% } %>
<% if (isMod) { %>
<section class="card">
<h2>Adjust user balance</h2>
<form method="post" action="/plugins/echonomy-framework/accounts/adjust" class="form-grid">
<div class="field">
<label>Username</label>
<input name="username" />
</div>
<div class="field">
<label>Amount (use negative to remove)</label>
<input name="amount" />
</div>
<div class="field">
<label>Note (optional)</label>
<input name="note" />
</div>
<button type="submit" class="button">Apply adjustment</button>
</form>
</section>
<% } %>
<section class="card">
<div class="section-header">
<div>
<h2>Top balances</h2>
<p class="hint">Snapshot of the richest accounts.</p>
</div>
</div>
<% if (!topBalances.length) { %>
<p>No balances yet.</p>
<% } else { %>
<table class="table">
<thead>
<tr>
<th>User</th>
<th>Balance</th>
</tr>
</thead>
<tbody>
<% topBalances.forEach((entry) => { %>
<tr>
<td><%= entry.username %></td>
<td><%= entry.balance %></td>
</tr>
<% }) %>
</tbody>
</table>
<% } %>
</section>
<section class="card">
<div class="section-header">
<div>
<h2>Transaction history</h2>
<p class="hint">Every change is logged with a UUID.</p>
</div>
<div class="table-tools">
<input
class="table-search"
type="search"
placeholder="Search transactions"
aria-label="Search transactions"
data-table-filter="echonomy-transactions"
/>
<div class="table-controls">
<label class="table-page-size">
Show
<select data-table-size="echonomy-transactions">
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="250">250</option>
</select>
</label>
</div>
</div>
</div>
<div class="table-wrap">
<table
class="table echonomy-table"
data-table="echonomy-transactions"
data-pageable="true"
data-page-size="25"
data-page-sizes="25,50,100,250"
>
<thead>
<tr>
<th data-sort="id">UUID</th>
<th data-sort="type">Type</th>
<th data-sort="amount">Amount</th>
<th data-sort="from">From</th>
<th data-sort="to">To</th>
<th>Note</th>
<th data-sort="date">Date</th>
</tr>
</thead>
<tbody>
<% transactions.forEach((tx) => { %>
<% const fromName = tx.from_name || 'System'; %>
<% const toName = tx.to_name || 'System'; %>
<tr
data-search="<%= `${tx.id} ${tx.type} ${tx.amount} ${fromName} ${toName} ${tx.note_search || tx.note || ''}` %>"
data-id="<%= tx.id %>"
data-type="<%= tx.type %>"
data-amount="<%= tx.amount %>"
data-from="<%= fromName %>"
data-to="<%= toName %>"
data-date="<%= tx.created_at %>"
>
<td>
<button type="button" class="uuid-chip" data-copy="<%= tx.id %>" title="Copy UUID">
<%= tx.id %>
</button>
</td>
<td><%= tx.type %></td>
<td><%= tx.amount %></td>
<td><%= fromName %></td>
<td><%= toName %></td>
<td>
<% if (tx.activity_reward) { %>
<details class="tx-note-details">
<summary><%= tx.note_display %></summary>
<% if (tx.activity_reward.hourStart && tx.activity_reward.hourEnd) { %>
<div class="tx-note-period">
<%= new Date(tx.activity_reward.hourStart).toLocaleString() %> -
<%= new Date(tx.activity_reward.hourEnd).toLocaleString() %>
</div>
<% } %>
<ul>
<% tx.activity_reward.rewards.forEach((reward) => { %>
<li>
<%= reward.label %>: <%= reward.amount %>
<% if (reward.hits > 0) { %> (<%= reward.hits %> events)<% } %>
<% if (reward.minutes > 0) { %> (<%= reward.minutes %> min)<% } %>
</li>
<% }) %>
</ul>
</details>
<% } else { %>
<%= tx.note_display || tx.note || '-' %>
<% } %>
</td>
<td><%= new Date(tx.created_at).toLocaleString() %></td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<div class="table-pagination" data-table-pagination="echonomy-transactions">
<button type="button" class="button subtle" data-page-prev>Previous</button>
<span class="table-page-label" data-page-label>Page 1 of 1</span>
<button type="button" class="button subtle" data-page-next>Next</button>
</div>
</section>
<script>
(() => {
const openButtons = document.querySelectorAll("[data-response-open]");
const modals = document.querySelectorAll("[data-response-modal]");
const openModal = (key) => {
const modal = document.querySelector(`[data-response-modal="${key}"]`);
if (!modal) {
return;
}
modal.classList.add("is-open");
modal.setAttribute("aria-hidden", "false");
};
const closeModal = (modal) => {
if (!modal) {
return;
}
modal.classList.remove("is-open");
modal.setAttribute("aria-hidden", "true");
};
openButtons.forEach((button) => {
button.addEventListener("click", () => {
openModal(button.getAttribute("data-response-open"));
});
});
modals.forEach((modal) => {
modal.querySelectorAll("[data-modal-close]").forEach((button) => {
button.addEventListener("click", () => closeModal(modal));
});
modal.addEventListener("click", (event) => {
if (event.target === modal) {
closeModal(modal);
}
});
modal.addEventListener("click", (event) => {
const addButton = event.target.closest("[data-response-add]");
if (addButton) {
const rows = modal.querySelector("[data-response-rows]");
if (!rows) {
return;
}
const row = document.createElement("div");
row.className = "response-row";
row.setAttribute("data-response-row", "true");
row.innerHTML = `
<input name="response_text" value="" />
<input name="response_weight" value="1" />
<button type="button" class="button subtle" data-response-remove>Remove</button>
`;
rows.appendChild(row);
}
const removeButton = event.target.closest("[data-response-remove]");
if (removeButton) {
const row = removeButton.closest("[data-response-row]");
if (row) {
row.remove();
}
}
});
});
window.addEventListener("keydown", (event) => {
if (event.key !== "Escape") {
return;
}
modals.forEach((modal) => {
if (modal.classList.contains("is-open")) {
closeModal(modal);
}
});
});
})();
</script>
<%- include("../../../src/web/views/partials/layout-bottom") %>