diff --git a/CHANGELOG.md b/CHANGELOG.md
index 15ad94e..c2ebc4d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## Unreleased
+
+### English
+- **Worker details (last 24 h):** click a worker in the table to open a dialog with its last 24 hours through the proxy: accepted and rejected shares, best share and average hashrate, a chart of shares per hour (rejections in red), a chart of the best difficulty per hour (log scale, so the ESP32 fit too), the 10 best hashes with their time and the latest shares. The proxy keeps this per hour in its stats file (survives restarts) and serves it at `/stats/worker?name=`; the dashboard at `/api/worker?name=`. Not in kiosk mode.
+- **Two new achievements:** 🌋 **Trillion hash** (a share with difficulty over 1 trillion) and the 🏆 **Golden prize: block found!**, unlocked when a share passes the network difficulty. The golden prize sits on its own full-width row, shows its trophy even while locked and lights up in gold when won.
+
+### Português
+- **Detalhes do worker (últimas 24 h):** clique num worker da tabela para abrir um modal com as últimas 24 horas dele pelo proxy: shares aceitos e recusados, melhor share e hashrate médio, gráfico de shares por hora (recusados em vermelho), gráfico da melhor dificuldade por hora (escala logarítmica, para os ESP32 caberem), os 10 melhores hashes com a hora e os últimos shares. O proxy guarda isso por hora no arquivo de estatísticas (sobrevive ao reinício) e entrega em `/stats/worker?name=`; o painel, em `/api/worker?name=`. Não abre no modo quiosque.
+- **Duas conquistas novas:** 🌋 **Um trilhão** (um share com dificuldade acima de 1 trilhão) e o 🏆 **Prêmio de ouro: bloco achado!**, que desbloqueia quando um share passa da dificuldade da rede. O prêmio de ouro fica numa linha inteira só para ele, mostra o troféu mesmo trancado e acende em dourado quando sai.
+
## v0.6.0 — 2026-11-12
### English
diff --git a/src/i18n.js b/src/i18n.js
index ec4748f..1302fc8 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -139,6 +139,8 @@ const EN = [
["href=\"#\">Abrir na carteira", "href=\"#\">Open in wallet"],
["id=\"dnCopy\">Copiar endereço", "id=\"dnCopy\">Copy address"],
["
Dá para esconder este item em Configurações → Fazenda.
", "
You can hide this item in Settings → Farm.
"],
+ ["const WKT = { click: 'ver as últimas 24 h', loading: 'carregando…', none: 'Sem dados do proxy para este worker (ele minera direto numa pool ou ainda não mandou shares pelo proxy).', acc: 'Shares aceitos', rej: 'Recusados', best: 'Melhor (24 h)', hr: 'Hashrate (24 h)', perHour: 'Shares por hora', bestHour: 'Melhor dificuldade por hora', top: 'Melhores hashes (24 h)', last: 'Últimos shares', ever: 'recorde', online: 'conectado', offline: 'desconectado', noShares: 'nenhum share nas últimas 24 h' };", "const WKT = { click: 'see the last 24 h', loading: 'loading…', none: 'No proxy data for this worker (it mines straight to a pool or has not sent shares through the proxy yet).', acc: 'Accepted shares', rej: 'Rejected', best: 'Best (24 h)', hr: 'Hashrate (24 h)', perHour: 'Shares per hour', bestHour: 'Best difficulty per hour', top: 'Best hashes (24 h)', last: 'Latest shares', ever: 'record', online: 'connected', offline: 'disconnected', noShares: 'no shares in the last 24 h' };"],
+ ["
", "
"],
["const DN = { copied: 'Endereço copiado. Obrigado! 🧡', select: 'Selecionado: use Ctrl+C / Copiar.' };", "const DN = { copied: 'Address copied. Thank you! 🧡', select: 'Selected: use Ctrl+C / Copy.' };"],
["id=\"cxSound\" title=\"Som dos recordes\">", "id=\"cxSound\" title=\"Record sound\">"],
["const SND = { on: 'Som dos recordes: ligado', off: 'Som dos recordes: desligado' };", "const SND = { on: 'Record sound: on', off: 'Record sound: off' };"],
diff --git a/src/luck.js b/src/luck.js
index 3bd9b96..a5b82e0 100644
--- a/src/luck.js
+++ b/src/luck.js
@@ -73,17 +73,21 @@ const ACHIEVEMENTS = [
{ id: 'diff_1k', icon: '🎯', pt: ['Clube dos 1.000', 'Um share com dificuldade acima de 1.000'], en: ['1,000 club', 'A share with difficulty over 1,000'] },
{ id: 'diff_1m', icon: '🚀', pt: ['Um em um milhão', 'Um share com dificuldade acima de 1 milhão'], en: ['One in a million', 'A share with difficulty over 1 million'] },
{ id: 'diff_1b', icon: '☄️', pt: ['Quase bloco (1 bi)', 'Um share com dificuldade acima de 1 bilhão'], en: ['Billionaire hash', 'A share with difficulty over 1 billion'] },
+ { id: 'diff_1t', icon: '🌋', pt: ['Um trilhão', 'Um share com dificuldade acima de 1 trilhão'], en: ['Trillion hash', 'A share with difficulty over 1 trillion'] },
{ id: 'shares_1k', icon: '📦', pt: ['Mil tentativas', '1.000 shares aceitos na fazenda'], en: ['A thousand tries', '1,000 accepted shares on the farm'] },
{ id: 'shares_100k', icon: '🏭', pt: ['100 mil tentativas', '100.000 shares aceitos na fazenda'], en: ['100,000 tries', '100,000 accepted shares on the farm'] },
{ id: 'luck_2x', icon: '🍀', pt: ['Dobro da sorte', 'Worker com sorte de pelo menos 2× o esperado'], en: ['Double luck', 'A worker with at least 2× expected luck'] },
{ id: 'luck_10x', icon: '⚡', pt: ['Raio na cabeça (10×)', 'Worker com sorte de pelo menos 10× o esperado'], en: ['Lightning strike (10×)', 'A worker with at least 10× expected luck'] },
{ id: 'week_nonstop', icon: '🛡️', pt: ['Uma semana sem parar', '7 dias seguidos minerando sem cair'], en: ['One week nonstop', '7 days in a row mining without stopping'] },
{ id: 'underdog', icon: '🐢', pt: ['Troféu vovô', 'Líder da semana com menos de 10% do maior hashrate'], en: ['Underdog trophy', 'Weekly leader with under 10% of the top hashrate'] },
+ // o prêmio de ouro: um share acima da dificuldade da rede é um bloco
+ { id: 'block', icon: '🏆', golden: true, pt: ['Prêmio de ouro: bloco achado!', 'Um share acima da dificuldade da rede: a fazenda achou um bloco'], en: ['Golden prize: block found!', 'A share above the network difficulty: the farm found a block'] },
];
// Avalia quais conquistas já foram atingidas e preserva as já desbloqueadas em `saved`.
function evaluateAchievements(ctx = {}, saved = {}, now = Date.now()) {
const best = Number(ctx.best) || 0;
+ const netDiff = Number(ctx.networkDifficulty) || 0;
const bestWorker = ctx.bestWorker && ctx.bestWorker !== '—' ? ctx.bestWorker : null;
const totalShares = Number(ctx.totalShares) || 0;
const workers = Array.isArray(ctx.workers) ? ctx.workers : [];
@@ -104,12 +108,14 @@ function evaluateAchievements(ctx = {}, saved = {}, now = Date.now()) {
diff_1k: () => (best >= 1e3 ? { worker: bestWorker } : null),
diff_1m: () => (best >= 1e6 ? { worker: bestWorker } : null),
diff_1b: () => (best >= 1e9 ? { worker: bestWorker } : null),
+ diff_1t: () => (best >= 1e12 ? { worker: bestWorker } : null),
shares_1k: () => (totalShares >= 1e3 ? { worker: null } : null),
shares_100k: () => (totalShares >= 1e5 ? { worker: null } : null),
luck_2x: () => (topLuck && topLuck.factor >= 2 ? { worker: topLuck.name } : null),
luck_10x: () => (topLuck && topLuck.factor >= 10 ? { worker: topLuck.name } : null),
week_nonstop: () => (nonstop ? { worker: nonstop.name } : null),
underdog: () => (isUnderdog ? { worker: leaderName } : null),
+ block: () => (netDiff > 0 && best >= netDiff ? { worker: bestWorker } : null),
};
const map = {};
@@ -130,6 +136,7 @@ function evaluateAchievements(ctx = {}, saved = {}, now = Date.now()) {
const list = ACHIEVEMENTS.map((a) => ({
id: a.id,
icon: a.icon,
+ golden: !!a.golden,
pt: a.pt,
en: a.en,
unlocked: !!map[a.id],
diff --git a/src/painel.js b/src/painel.js
index 2d1428a..6a91c2d 100644
--- a/src/painel.js
+++ b/src/painel.js
@@ -1014,6 +1014,7 @@ function snapshot(internal = false) {
const topRec = [...records].sort((a, b) => b.value - a.value)[0];
const ach = evaluateAchievements({
best,
+ networkDifficulty: pool.difficulty,
bestWorker: topRec ? topRec.worker : null,
totalShares: Math.max(proxyTotalShares, poolTotalShares),
workers: view,
@@ -1244,6 +1245,9 @@ const PAGE = `
.ach-i { font-size:18px; line-height:1.2; }
.ach-t { font-size:13px; font-weight:600; color:var(--txt); }
.ach.on .ach-t { color:var(--gold); }
+ .ach.golden { grid-column:1 / -1; border-style:dashed; }
+ .ach.golden.on { border:2px solid #f2a900; background:linear-gradient(90deg, rgba(242,169,0,.18), rgba(242,169,0,.04)); box-shadow:0 0 18px rgba(242,169,0,.35); }
+ .ach.golden.on .ach-t { color:#f2a900; font-size:15px; }
.ach-d { font-size:11px; color:var(--dim); line-height:1.3; margin-top:2px; }
.hist-card .chart { height:240px; }
body.kiosk .luck-duo { flex:1; min-height:0; }
@@ -1296,6 +1300,17 @@ const PAGE = `
.pm-btns { flex-wrap:wrap; } a.pm-btn { text-decoration:none; display:inline-flex; align-items:center; }
.dn-foot { font-size:12px; }
.cx-item.cx-donate .ic { filter:none; opacity:.9; }
+ /* modal do worker: últimas 24 h */
+ #rows tr[data-w] { cursor:pointer; } #rows tr[data-w]:hover td { background:var(--gold-soft); }
+ .wk-box { max-width:680px; max-height:calc(100vh - 32px); overflow:auto; }
+ .wk-head { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; } .wk-head b { font-size:18px; } .wk-head .sub { color:var(--dim); font-size:13px; }
+ .wk-sum { display:grid; grid-template-columns:repeat(4, 1fr); gap:8px; }
+ .wk-sum div { border:1px solid var(--line); border-radius:8px; padding:8px 10px; } .wk-sum span { display:block; color:var(--dim); font-size:11px; text-transform:uppercase; letter-spacing:.05em; } .wk-sum strong { font-size:16px; }
+ .wk-ch { width:100%; height:auto; display:block; } .wk-l { color:var(--dim); font-size:12px; text-transform:uppercase; letter-spacing:.06em; margin-top:4px; }
+ .wk-list { display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:4px 12px; font-size:13px; font-variant-numeric:tabular-nums; }
+ .wk-list span { color:var(--dim); } .wk-list .bad { color:var(--warn); }
+ #wkMsg { color:var(--dim); font-size:13px; }
+ @media (max-width:560px) { .wk-sum { grid-template-columns:repeat(2, 1fr); } }
.pm-btn.pm-ok { background:var(--gold); border-color:var(--gold); color:var(--on-gold, #111); font-weight:600; }
.tooltip { position:fixed; pointer-events:none; background:var(--tip-bg); box-shadow:var(--shadow); border:1px solid var(--line); border-radius:8px; padding:6px 9px; font-size:12px; display:none; z-index:9; white-space:nowrap; }
.tooltip b { color:var(--txt); } .tooltip span { color:var(--dim); }
@@ -1521,6 +1536,10 @@ const PAGE = `