diff --git a/CHANGELOG.md b/CHANGELOG.md index 764826d..cd75e52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer. +## [0.6.0] - 2026-10-18 + +### Adicionado +- **Saúde do disco no estilo do CrystalDiskInfo:** a tela do dispositivo mostra o estado (Bom, Atenção, Ruim) com a % de vida útil restante em SSD, temperatura, horas ligado, vezes ligado, total lido e total gravado. Também traz a ficha do dispositivo (modelo, firmware, nº de série, interface, modo de transferência, capacidade, rotação, formato e recursos como TRIM) e a tabela completa de atributos SMART, com ID, valor atual, pior, limite e bruto, e um indicador por atributo. Funciona com discos ATA/SATA e NVMe (nos NVMe, os campos do log de saúde viram a tabela). +- **Partições:** ao abrir uma partição (`/dev/sda1`, `/dev/nvme0n1p2`, `/dev/disk2s1`), o SMART é lido do disco que a contém, e a tela avisa isso. +- **Comando `guara health`** no terminal, com saída legível ou `--json`. + +### Alterado +- Um atributo SMART com valor normalizado no limite de falha (ou abaixo dele) agora marca o disco como "Ruim", mesmo que a autoavaliação geral ainda passe. +- **Nova logo:** a cabeça do lobo-guará recortada das trilhas de um disco, inteiras de um lado (recuperado) e quebradas do outro (perdido), nas cores do app. Na interface ela acompanha o tema escolhido (claro ou escuro). + ## [0.5.0] - 2026-10-18 ### Adicionado diff --git a/Cargo.lock b/Cargo.lock index f921203..3ba6481 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,7 +317,7 @@ dependencies = [ [[package]] name = "guara-cli" -version = "0.5.0" +version = "0.6.0" dependencies = [ "clap", "ctrlc", @@ -329,7 +329,7 @@ dependencies = [ [[package]] name = "guara-core" -version = "0.5.0" +version = "0.6.0" dependencies = [ "flate2", "image", diff --git a/Cargo.toml b/Cargo.toml index 0951116..c484105 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/guara-core", "crates/guara-cli"] [workspace.package] -version = "0.5.0" +version = "0.6.0" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/lucasrguerra/guara" diff --git a/README.md b/README.md index 9aeb394..710b77b 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Ler discos brutos exige privilégios: **administrador** (Windows), `sudo` (Linux ```bash sudo guara devices # discos e pendrives (avisa TRIM e volumes montados) sudo guara inspect /dev/sdb # partições, espaço não particionado, cifragem +sudo guara health /dev/sdb # saúde do disco (SMART), requer smartmontools sudo guara scan /dev/sdb --session s.json --list # varredura profunda sudo guara scan /dev/sdb --quick # só metadados (segundos) guara scan disco.img --category image,document --min-confidence medium @@ -117,6 +118,12 @@ Veja [CONTRIBUTING.md](CONTRIBUTING.md). Versões seguem [SemVer](https://semver [MIT](LICENSE-MIT) ou [Apache-2.0](LICENSE-APACHE), à sua escolha. +## Novidades da 0.6.0 + +- Saúde do disco completa, no estilo do CrystalDiskInfo: estado com % de vida útil, temperatura, horas e vezes ligado, total lido e gravado, ficha do dispositivo (modelo, firmware, interface, modo de transferência, rotação, recursos) e a tabela inteira de atributos SMART (ATA e NVMe). +- Novo comando `guara health` no terminal. +- Nova logo: a cabeça do guará recortada das trilhas de um disco. + ## Novidades da 0.5.0 - Visualizador de bancos SQLite com recuperação de linhas apagadas. diff --git a/apps/gui/src-tauri/Cargo.toml b/apps/gui/src-tauri/Cargo.toml index 94f820f..77e1179 100644 --- a/apps/gui/src-tauri/Cargo.toml +++ b/apps/gui/src-tauri/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "guara-gui" description = "Guará: interface gráfica de recuperação de arquivos" -version = "0.5.0" +version = "0.6.0" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/lucasrguerra/guara" diff --git a/apps/gui/src-tauri/icons/128x128.png b/apps/gui/src-tauri/icons/128x128.png index 4ca70f7..6e74fb0 100644 Binary files a/apps/gui/src-tauri/icons/128x128.png and b/apps/gui/src-tauri/icons/128x128.png differ diff --git a/apps/gui/src-tauri/icons/128x128@2x.png b/apps/gui/src-tauri/icons/128x128@2x.png index 5d0b22e..e8afcdf 100644 Binary files a/apps/gui/src-tauri/icons/128x128@2x.png and b/apps/gui/src-tauri/icons/128x128@2x.png differ diff --git a/apps/gui/src-tauri/icons/32x32.png b/apps/gui/src-tauri/icons/32x32.png index d1faf21..ba6098b 100644 Binary files a/apps/gui/src-tauri/icons/32x32.png and b/apps/gui/src-tauri/icons/32x32.png differ diff --git a/apps/gui/src-tauri/icons/icon.ico b/apps/gui/src-tauri/icons/icon.ico index cbb8e7c..31ea6e4 100644 Binary files a/apps/gui/src-tauri/icons/icon.ico and b/apps/gui/src-tauri/icons/icon.ico differ diff --git a/apps/gui/src-tauri/icons/icon.png b/apps/gui/src-tauri/icons/icon.png index a2bc21e..13429ea 100644 Binary files a/apps/gui/src-tauri/icons/icon.png and b/apps/gui/src-tauri/icons/icon.png differ diff --git a/apps/gui/src-tauri/icons/source.png b/apps/gui/src-tauri/icons/source.png index a2bc21e..58b1f34 100644 Binary files a/apps/gui/src-tauri/icons/source.png and b/apps/gui/src-tauri/icons/source.png differ diff --git a/apps/gui/src-tauri/tauri.conf.json b/apps/gui/src-tauri/tauri.conf.json index e239672..086d624 100644 --- a/apps/gui/src-tauri/tauri.conf.json +++ b/apps/gui/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Guara", - "version": "0.5.0", + "version": "0.6.0", "identifier": "dev.guara.app", "build": { "frontendDist": "../ui" diff --git a/apps/gui/ui/app.css b/apps/gui/ui/app.css index aa3426e..e53772c 100644 --- a/apps/gui/ui/app.css +++ b/apps/gui/ui/app.css @@ -494,3 +494,25 @@ dialog.lightbox[open] { display: grid; } .xp-tiles { display: grid; grid-template-columns: repeat(3, 1fr); width: 100%; height: 100%; } .xp-tiles img { width: 100%; height: 100%; display: block; } .xp-pin { position: absolute; width: 14px; height: 14px; margin: -7px 0 0 -7px; border-radius: 50%; background: #0f8f8f; border: 3px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,.4); } + +/* saúde do disco (SMART) */ +.h-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; } +.h-tile { background: var(--surface); border-radius: 14px; box-shadow: var(--shadow); padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; } +.h-tile small { color: var(--text-3); font-size: 12px; } +.h-tile b { font-size: 18px; font-variant-numeric: tabular-nums; } +.h-tile.warning b { color: var(--warn); } .h-tile.failing b { color: var(--bad); } +.h-status.ok { background: var(--ok-soft); } .h-status.ok b { color: var(--ok); } +.h-status.warning { background: var(--warn-soft); } .h-status.failing { background: var(--bad-soft); } +.h-status b span { font-weight: 500; opacity: .8; } +.h-info { margin-top: 14px; background: var(--surface); border-radius: 14px; box-shadow: var(--shadow); padding: 14px 16px; } +.h-scroll { overflow-x: auto; background: var(--surface); border-radius: 14px; box-shadow: var(--shadow); } +.h-table { width: 100%; border-collapse: collapse; font-size: 13px; font-variant-numeric: tabular-nums; } +.h-table th { text-align: left; color: var(--text-3); font-weight: 500; padding: 8px 10px; border-bottom: 1px solid var(--line, rgba(127,127,127,.2)); white-space: nowrap; } +.h-table td { padding: 6px 10px; border-bottom: 1px solid var(--line, rgba(127,127,127,.12)); white-space: nowrap; } +.h-table tr:last-child td { border-bottom: 0; } +.h-table .dim { color: var(--text-3); } +.h-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--ok); } +.h-dot.warning { background: var(--warn); } .h-dot.failing { background: var(--bad); } + +/* logo inline: segue o tema escolhido no app, não o do sistema */ +.logo .a { stroke: var(--accent); } .logo .b { stroke: var(--accent-2); } .logo .t { fill: var(--text); } diff --git a/apps/gui/ui/app.js b/apps/gui/ui/app.js index 6cc7fd1..4c096b2 100644 --- a/apps/gui/ui/app.js +++ b/apps/gui/ui/app.js @@ -287,15 +287,46 @@ async function openDevice(d) { rpc('health', { path: d.path }).then((h) => { if (S.device === d) { S.health = h; paintHealth(); } }).catch(() => {}); } +const HEALTH_WORD = { ok: 'Bom', warning: 'Atenção', failing: 'Ruim' }; + function paintHealth() { const el = $('#healthArea'); const h = S.health; - if (!el || !h || !h.available) { if (el) el.innerHTML = ''; return; } + if (!el || !h) { if (el) el.innerHTML = ''; return; } + if (!h.available) { + el.innerHTML = `

Saúde do disco

${esc(h.summary)}

`; + return; + } const bad = h.status === 'failing' || h.status === 'warning'; - const rows = (h.details || []).filter(([k]) => bad || ['Modelo', 'Horas ligado', 'Temperatura'].includes(k)).map(([k, v]) => `
${esc(k)}
${esc(v)}
`).join(''); - el.innerHTML = `
${warnSvg}
${esc(h.summary)} - ${rows ? `
${rows}
` : ''}${(h.advice || []).map((a) => `
${esc(a)}
`).join('')} - ${bad ? '
' : ''}
`; + const fmtN = (n) => Number(n).toLocaleString(LOC()); + const tile = (label, value, cls = '') => value == null ? '' : `
${label}${value}
`; + const temp = h.temperature_c; + const tiles = [ + `
Estado${HEALTH_WORD[h.status] || '?'}${h.health_pct != null ? ` ${h.health_pct}%` : ''}
`, + tile('Temperatura', temp == null ? null : `${temp} °C`, temp >= 60 ? 'failing' : temp >= 50 ? 'warning' : ''), + tile('Horas ligado', h.power_on_hours == null ? null : `${fmtN(h.power_on_hours)} h`), + tile('Vezes ligado', h.power_cycles == null ? null : fmtN(h.power_cycles)), + tile('Total lido', h.host_reads == null ? null : human(h.host_reads)), + tile('Total gravado', h.host_writes == null ? null : human(h.host_writes)), + ].join(''); + const info = (h.info || []).map(([k, v]) => `
${esc(k)}
${esc(v)}
`).join(''); + const dash = (x) => x == null ? '–' : x; + const attrs = h.attributes || []; + const table = attrs.length ? `
Atributos SMART (${attrs.length}) +
+ ${attrs.map((a) => ``).join('')} +
IDAtributoAtualPiorLimiteBruto
${esc(a.id)}${esc(a.name)}${dash(a.value)}${dash(a.worst)}${dash(a.thresh)}${esc(a.raw)}
` : ''; + const scope = h.partition ? `

O SMART é do disco inteiro: os dados abaixo são de ${esc(h.disk)}, que contém ${esc(h.partition)}.

` : ''; + // o que motivou o alerta (os campos da ficha já aparecem abaixo) + const known = new Set(['Modelo', 'Nº de série', 'Horas ligado', 'Temperatura', 'Autoavaliação SMART', 'Vida útil usada']); + const why = (h.details || []).filter(([k]) => !known.has(k)).map(([k, v]) => `
${esc(k)}
${esc(v)}
`).join(''); + el.innerHTML = `${bad ? `
${warnSvg}
${esc(h.summary)} + ${why ? `
${why}
` : ''} + ${(h.advice || []).map((a) => `
${esc(a)}
`).join('')} +
` : ''} +

Saúde do disco

${scope} +
${tiles}
+ ${info ? `
${info}
` : ''}${table}
`; $('#healthImg')?.addEventListener('click', startImage); } diff --git a/apps/gui/ui/guara-logo.svg b/apps/gui/ui/guara-logo.svg index 09a34ae..5af635e 100644 --- a/apps/gui/ui/guara-logo.svg +++ b/apps/gui/ui/guara-logo.svg @@ -1 +1 @@ - + diff --git a/apps/gui/ui/guara.svg b/apps/gui/ui/guara.svg index 2da20c6..5af635e 100644 --- a/apps/gui/ui/guara.svg +++ b/apps/gui/ui/guara.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/apps/gui/ui/i18n-en.js b/apps/gui/ui/i18n-en.js index 40d4256..e502202 100644 --- a/apps/gui/ui/i18n-en.js +++ b/apps/gui/ui/i18n-en.js @@ -1,5 +1,48 @@ // Dicionário português -> inglês da interface. {n} marca um valor variável. export const EN = { + // saúde do disco (SMART) + 'Saúde do disco': 'Disk health', + 'O SMART não pôde ser lido. Instale o pacote smartmontools (comando smartctl) e rode como administrador para ver a saúde do disco.': 'SMART could not be read. Install the smartmontools package (smartctl command) and run as administrator to see the disk health.', + 'Setores realocados': 'Reallocated sectors', + 'Setores pendentes de realocação': 'Pending sectors', + 'Setores incorrigíveis offline': 'Offline uncorrectable sectors', + 'Erros incorrigíveis reportados': 'Reported uncorrectable errors', + 'Erros de CRC na interface (cabo ou porta)': 'Interface CRC errors (cable or port)', + 'Erros registrados no log do disco': 'Errors in the disk log', + 'Aviso crítico NVMe': 'NVMe critical warning', + 'Erros de mídia': 'Media errors', + 'Reserva de blocos abaixo do limite': 'Spare blocks below threshold', + 'Erros de leitura/gravação vistos pelo sistema': 'Read/write errors seen by the system', + 'O sistema já registrou erros de E/S neste disco.': 'The system has already logged I/O errors on this disk.', + 'Estado': 'Status', + 'Bom': 'Good', + 'Atenção': 'Caution', + 'Ruim': 'Bad', + 'Temperatura': 'Temperature', + 'Horas ligado': 'Power on hours', + 'Vezes ligado': 'Power on count', + 'Total lido': 'Host reads', + 'Total gravado': 'Host writes', + 'Atributos SMART ({n})': 'SMART attributes ({n})', + 'Atributo': 'Attribute', + 'Atual': 'Current', + 'Pior': 'Worst', + 'Limite': 'Threshold', + 'Bruto': 'Raw', + 'Modelo': 'Model', + 'Nº de série': 'Serial number', + 'Família': 'Family', + 'Modo de transferência': 'Transfer mode', + 'Capacidade': 'Capacity', + 'Rotação': 'Rotation rate', + 'Formato': 'Form factor', + 'Recursos': 'Features', + 'O SMART é do disco inteiro: os dados abaixo são de': 'SMART belongs to the whole disk: the data below is for', + ', que contém': ', which contains', + 'Imagem de disco: não há SMART para avaliar.': 'Disk image: there is no SMART to check.', + 'Nenhum sinal de defeito encontrado pelo SMART.': 'SMART found no signs of failure.', + 'Este disco mostra sinais sérios de defeito.': 'This disk shows serious signs of failure.', + 'Este disco mostra sinais de desgaste ou erros.': 'This disk shows signs of wear or errors.', // barra lateral e estrutura 'Recuperação de arquivos': 'File recovery', 'Dispositivos': 'Devices', diff --git a/apps/gui/ui/index.html b/apps/gui/ui/index.html index cd49cf9..666405f 100644 --- a/apps/gui/ui/index.html +++ b/apps/gui/ui/index.html @@ -14,10 +14,10 @@ - Guará + Guará