From 613836b02255ba2963c2989ed5db1fbe139376cb Mon Sep 17 00:00:00 2001 From: Paulo Modanez Date: Thu, 12 Nov 2026 09:14:19 -0300 Subject: [PATCH] =?UTF-8?q?Bitaxe:=20bot=C3=A3o=20para=20abrir=20o=20AxeOS?= =?UTF-8?q?=20pelo=20IP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O link 'abrir o AxeOS' era pequeno e usava o host cadastrado (nome .local não abre em todo celular). Agora é um botão '🌐 AxeOS 192.168.0.x ↗' com o IP que o próprio AxeOS informa (ipv4), caindo no host cadastrado. Claude-Session: https://claude.ai/code/session_01XjfaWQbpm2Tv3YnJgEKrFs --- CHANGELOG.md | 2 ++ src/bitaxe.js | 2 ++ src/i18n.js | 4 ++-- src/painel.js | 7 +++++-- tests/bitaxe.test.js | 6 ++++++ 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fdb5ec..61e7754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### English +- **"🌐 AxeOS" button on each Bitaxe card:** opens the miner's own AxeOS page by IP (the address AxeOS reports, falling back to the registered host), since `.local` names don't open on every phone. The IP is shown on the button. - **Umbrel app (community store):** the `umbrel/` folder is an Umbrel community app store with HomeMinerProxy (dashboard behind Umbrel's login, Stratum ports open on the Umbrel's IP, data in the app folder, multi-arch image from GHCR). See `umbrel/README.md`. The Docker image now also exposes ports 3340 and 3341. - **Record sound ("tam-tam-tam-tammm"):** the record celebration now plays a short fanfare in the browser, generated live with Web Audio (no audio file): the full fanfare for a farm record, a short "tam-tammm" for a personal one. 🔊/🔇 in the top bar turns it on or off (remembered per browser). Browsers only allow sound after a tap on the page; for kiosk mode, start Chromium with `--autoplay-policy=no-user-gesture-required`. - **Bitaxe "regulator hot" alert at 95 °C (was 80 °C):** the first real Gamma 602 runs its voltage regulator at ~82 °C with the chip at 60 °C, which is normal; the old limit fired a false alarm. AxeOS itself stops hashing only above 105 °C (and resumes at 95 °C), so 95 °C warns before that. @@ -27,6 +28,7 @@ - Fixed in Settings: "Use these" after the Bitaxe scan always named them bitaxe1, bitaxe2… instead of their worker name, and a Sonoff address with a port (`host:8081`) was refused in the browser. ### Português +- **Botão "🌐 AxeOS" em cada cartão do Bitaxe:** abre a página do próprio AxeOS pelo IP (o endereço que o AxeOS informa; sem ele, o host cadastrado), porque nome `.local` não abre em todo celular. O IP aparece no botão. - **App para Umbrel (loja comunitária):** a pasta `umbrel/` é uma loja comunitária do Umbrel com o HomeMinerProxy (painel atrás do login do Umbrel, portas Stratum abertas no IP do Umbrel, dados na pasta do app, imagem multi-arquitetura do GHCR). Veja `umbrel/README.md`. A imagem Docker também passa a expor as portas 3340 e 3341. - **Som de recorde ("tam-tam-tam-tammm"):** a comemoração de recorde agora toca uma fanfarra curta no navegador, gerada na hora com Web Audio (sem arquivo de áudio): a fanfarra inteira no recorde da fazenda, um "tam-tammm" curto no recorde pessoal. O 🔊/🔇 da barra superior liga ou desliga (fica guardado em cada navegador). Os navegadores só liberam som depois de um toque na página; no modo quiosque, abra o Chromium com `--autoplay-policy=no-user-gesture-required`. - **Alerta "regulador quente" do Bitaxe em 95 °C (era 80 °C):** o primeiro Gamma 602 de verdade trabalha com o regulador a ~82 °C e o chip a 60 °C, o que é normal; o limite antigo dava alarme falso. O próprio AxeOS só para de minerar acima de 105 °C (e volta a 95 °C), então 95 °C avisa antes disso. diff --git a/src/bitaxe.js b/src/bitaxe.js index 8c48a4a..03c6820 100644 --- a/src/bitaxe.js +++ b/src/bitaxe.js @@ -93,6 +93,8 @@ function normalize(j) { board: j.boardVersion || null, version: j.version || null, hostname: j.hostname || null, + // IP que o próprio AxeOS informa (2.x): o link abre por ele, porque nome .local não resolve em todo celular + ip: [j.ipv4, j.ip].find((v) => typeof v === 'string' && /^\d{1,3}(\.\d{1,3}){3}$/.test(v)) || null, hashRate, hashRate1m: ghs(j.hashRate_1m), hashRate10m: ghs(j.hashRate_10m), diff --git a/src/i18n.js b/src/i18n.js index dfb527a..ec4748f 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -21,8 +21,8 @@ function pickLang(req, serverLang) { const EN = [ ["const CX = { overview: 'Visão geral', overviewD: 'Recorde, hashrate, workers e carteira da fazenda', bitaxe: 'Bitaxe', bitaxeD: 'Temperatura, consumo, pool e ações de cada Bitaxe', network: 'Rede Bitcoin', networkD: 'Dificuldade, próximo ajuste, mempool e taxas', near: 'Quase bloco', nearD: 'O quanto cada share chegou perto de um bloco', luck: 'Sorte', luckD: 'Chance de achar um bloco e o campeonato dos workers', history: 'Histórico', historyD: 'Gráficos, hall da fama, saúde do servidor e energia', gFarm: 'Fazenda', gMining: 'Mineração', gOps: 'Operação', pools: 'Pools e portas', discover: 'Descobrir na rede', donate: 'Apoiar o projeto', settings: 'Configurações', kiosk: 'Modo quiosque', workers: 'workers ativos', proxyDown: 'proxy fora do ar', ping: 'ping' };", "const CX = { overview: 'Overview', overviewD: 'Farm record, hashrate, workers and wallet', bitaxe: 'Bitaxe', bitaxeD: 'Temperature, power, pool and actions for each Bitaxe', network: 'Bitcoin network', networkD: 'Difficulty, next adjustment, mempool and fees', near: 'Almost a block', nearD: 'How close each share came to a block', luck: 'Luck', luckD: 'Odds of finding a block and the worker championship', history: 'History', historyD: 'Charts, hall of fame, server health and energy', gFarm: 'Farm', gMining: 'Mining', gOps: 'Operations', pools: 'Pools and ports', discover: 'Find miners', donate: 'Support the project', settings: 'Settings', kiosk: 'Kiosk mode', workers: 'active workers', proxyDown: 'proxy down', ping: 'ping' };"], - ["const BXT = { noAnswer: 'sem resposta', hash: 'Hashrate (10 min)', of: 'de', expected: 'esperado', chip: 'Chip', vr: 'Regulador', power: 'Consumo', fan: 'Fan', freq: 'Frequência', pool: 'Pool', backup: 'reserva', best: 'Melhor da sessão', up: 'Ligado há', wifi: 'Wi-Fi', paused: 'pausado', overheat: 'superaquecido: o AxeOS baixou a frequência', powerFault: 'falha de energia (confira a fonte)', hwFault: 'falha de hardware', open: 'abrir o AxeOS ↗', last: 'última leitura', hot: 'chip quente', vrhot: 'regulador quente', lowhash: 'hashrate baixo', onBackup: 'na pool reserva', reboots: 'reinícios', lastAct: 'Última ação', acts: { restart: 'reiniciar', identify: 'identificar', pause: 'pausar', resume: 'retomar', fan: 'fan', point: 'apontar para o proxy' }, actFail: 'falhou', actions: 'ações ⚙️', chHash: 'Hashrate · 24 h', chTemp: 'Chip · 24 h', chWait: 'o gráfico aparece depois de 10 min de leituras' };", - "const BXT = { noAnswer: 'no answer', hash: 'Hashrate (10 min)', of: 'of', expected: 'expected', chip: 'Chip', vr: 'Regulator', power: 'Power', fan: 'Fan', freq: 'Frequency', pool: 'Pool', backup: 'backup', best: 'Session best', up: 'Up for', wifi: 'Wi-Fi', paused: 'paused', overheat: 'overheated: AxeOS lowered the frequency', powerFault: 'power fault (check the power supply)', hwFault: 'hardware fault', open: 'open AxeOS ↗', last: 'last read', hot: 'chip hot', vrhot: 'regulator hot', lowhash: 'low hashrate', onBackup: 'on the backup pool', reboots: 'restarts', lastAct: 'Last action', acts: { restart: 'restart', identify: 'identify', pause: 'pause', resume: 'resume', fan: 'fan', point: 'point at the proxy' }, actFail: 'failed', actions: 'actions ⚙️', chHash: 'Hashrate · 24 h', chTemp: 'Chip · 24 h', chWait: 'the chart shows after 10 min of readings' };"], + ["const BXT = { noAnswer: 'sem resposta', hash: 'Hashrate (10 min)', of: 'de', expected: 'esperado', chip: 'Chip', vr: 'Regulador', power: 'Consumo', fan: 'Fan', freq: 'Frequência', pool: 'Pool', backup: 'reserva', best: 'Melhor da sessão', up: 'Ligado há', wifi: 'Wi-Fi', paused: 'pausado', overheat: 'superaquecido: o AxeOS baixou a frequência', powerFault: 'falha de energia (confira a fonte)', hwFault: 'falha de hardware', open: '🌐 AxeOS', last: 'última leitura', hot: 'chip quente', vrhot: 'regulador quente', lowhash: 'hashrate baixo', onBackup: 'na pool reserva', reboots: 'reinícios', lastAct: 'Última ação', acts: { restart: 'reiniciar', identify: 'identificar', pause: 'pausar', resume: 'retomar', fan: 'fan', point: 'apontar para o proxy' }, actFail: 'falhou', actions: 'ações ⚙️', chHash: 'Hashrate · 24 h', chTemp: 'Chip · 24 h', chWait: 'o gráfico aparece depois de 10 min de leituras' };", + "const BXT = { noAnswer: 'no answer', hash: 'Hashrate (10 min)', of: 'of', expected: 'expected', chip: 'Chip', vr: 'Regulator', power: 'Power', fan: 'Fan', freq: 'Frequency', pool: 'Pool', backup: 'backup', best: 'Session best', up: 'Up for', wifi: 'Wi-Fi', paused: 'paused', overheat: 'overheated: AxeOS lowered the frequency', powerFault: 'power fault (check the power supply)', hwFault: 'hardware fault', open: '🌐 AxeOS', last: 'last read', hot: 'chip hot', vrhot: 'regulator hot', lowhash: 'low hashrate', onBackup: 'on the backup pool', reboots: 'restarts', lastAct: 'Last action', acts: { restart: 'restart', identify: 'identify', pause: 'pause', resume: 'resume', fan: 'fan', point: 'point at the proxy' }, actFail: 'failed', actions: 'actions ⚙️', chHash: 'Hashrate · 24 h', chTemp: 'Chip · 24 h', chWait: 'the chart shows after 10 min of readings' };"], // ---------- cabeçalho e telas fixas ['', ''], ['Fazenda de Mineração', 'Mining Farm'], diff --git a/src/painel.js b/src/painel.js index 946bde3..dc01fcd 100644 --- a/src/painel.js +++ b/src/painel.js @@ -1202,6 +1202,8 @@ const PAGE = ` .bx .bxh { display:flex; flex-wrap:wrap; gap:6px 10px; align-items:baseline; } .bx .bxh b { font-size:15px; } .bx .bxh a { margin-left:auto; font-size:12px; color:var(--dim); } + .bx .bxh a.axeos { font-size:13px; color:var(--ok); border:1px solid var(--line); border-radius:8px; padding:3px 9px; text-decoration:none; } + .bx .bxh a.axeos span { color:var(--dim); } .bxg { display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:8px 14px; margin-top:8px; } .bxg div { font-size:14px; } .bxg span { display:block; font-size:11px; color:var(--dim); } @@ -1783,7 +1785,7 @@ function renderProxyExtras(p) { ul.innerHTML = (p.events || []).map((e) => '
  • ' + (icon[e.type] || '•') + ' ' + hora(e.at) + ' — ' + esc(e.msg) + '
  • ').join('') || '
  • nenhum evento ainda
  • '; } -const BXT = { noAnswer: 'sem resposta', hash: 'Hashrate (10 min)', of: 'de', expected: 'esperado', chip: 'Chip', vr: 'Regulador', power: 'Consumo', fan: 'Fan', freq: 'Frequência', pool: 'Pool', backup: 'reserva', best: 'Melhor da sessão', up: 'Ligado há', wifi: 'Wi-Fi', paused: 'pausado', overheat: 'superaquecido: o AxeOS baixou a frequência', powerFault: 'falha de energia (confira a fonte)', hwFault: 'falha de hardware', open: 'abrir o AxeOS ↗', last: 'última leitura', hot: 'chip quente', vrhot: 'regulador quente', lowhash: 'hashrate baixo', onBackup: 'na pool reserva', reboots: 'reinícios', lastAct: 'Última ação', acts: { restart: 'reiniciar', identify: 'identificar', pause: 'pausar', resume: 'retomar', fan: 'fan', point: 'apontar para o proxy' }, actFail: 'falhou', actions: 'ações ⚙️', chHash: 'Hashrate · 24 h', chTemp: 'Chip · 24 h', chWait: 'o gráfico aparece depois de 10 min de leituras' }; +const BXT = { noAnswer: 'sem resposta', hash: 'Hashrate (10 min)', of: 'de', expected: 'esperado', chip: 'Chip', vr: 'Regulador', power: 'Consumo', fan: 'Fan', freq: 'Frequência', pool: 'Pool', backup: 'reserva', best: 'Melhor da sessão', up: 'Ligado há', wifi: 'Wi-Fi', paused: 'pausado', overheat: 'superaquecido: o AxeOS baixou a frequência', powerFault: 'falha de energia (confira a fonte)', hwFault: 'falha de hardware', open: '🌐 AxeOS', last: 'última leitura', hot: 'chip quente', vrhot: 'regulador quente', lowhash: 'hashrate baixo', onBackup: 'na pool reserva', reboots: 'reinícios', lastAct: 'Última ação', acts: { restart: 'reiniciar', identify: 'identificar', pause: 'pausar', resume: 'retomar', fan: 'fan', point: 'apontar para o proxy' }, actFail: 'falhou', actions: 'ações ⚙️', chHash: 'Hashrate · 24 h', chTemp: 'Chip · 24 h', chWait: 'o gráfico aparece depois de 10 min de leituras' }; // Gráficos de 24 h do Bitaxe: dois pequenos lado a lado (hashrate e chip), cada um com o seu eixo, nunca dois eixos no mesmo const BXCH = {}; // id do gráfico → pontos, para o tooltip function bxChart(id, title, pts, fmt) { @@ -1838,7 +1840,8 @@ function renderBitaxes(j) { const st = (label, val) => '
    ' + label + '' + val + '
    '; $('bxlist').innerHTML = list.map((b) => { const d = b.data; - const link = /^[\w.:\-]+$/.test(b.host) ? '' + BXT.open + '' : ''; + const addr = (d && d.ip) || b.host; // IP primeiro: nome .local não abre em todo celular + const link = /^[\w.:\-]+$/.test(addr) ? '' + BXT.open + ' ' + esc(addr) + ' ↗' : ''; const head = '
    ' + esc(b.name) + '' + esc([d && d.model, d && d.version, b.host].filter(Boolean).join(' · ')) + '' + (!b.fresh ? '' + BXT.noAnswer + (b.okAt ? ' · ' + BXT.last + ' ' + agoTxt(b.okAt) : '') + '' : '') + link + '' + BXT.actions + '
    '; if (!d) return '
    ' + head + '
    '; diff --git a/tests/bitaxe.test.js b/tests/bitaxe.test.js index 836d23c..d446622 100644 --- a/tests/bitaxe.test.js +++ b/tests/bitaxe.test.js @@ -276,3 +276,9 @@ test('worker do Bitaxe em SOLO Group: sem o #grupo (e sem o .hidden)', () => { assert.strictEqual(d('bc1qx.bitaxe2#minhaturma.hidden'), 'bitaxe2'); assert.strictEqual(d('bc1qx.bitaxe3'), 'bitaxe3'); }); + +test('IP do AxeOS para o link (ipv4 válido; senão nada)', () => { + assert.strictEqual(bx.normalize({ ipv4: '192.168.0.138' }).ip, '192.168.0.138'); + assert.strictEqual(bx.normalize({ ipv4: '