Files
ESPDocs/app/[locale]/diagnostico/page.jsx
T
Lucas RayanandClaude Sonnet 5.5 5a9fe676fc
CI / Lint e auditorias (push) Successful in 1m10s
CI / Trivy (dependências e segredos) (push) Successful in 12s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m39s
Release / Plano (reaproveitar a imagem?) (push) Successful in 6s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 18s
Release / Cria a release no Gitea (push) Successful in 5s
feat(i18n): Simplified Chinese (zh-CN), search answers per locale, fr/de/es data fixes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 23:28:39 -03:00

731 lines
38 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useMemo, useRef } from "react";
import { usePathname } from "next/navigation";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import * as diagnosticoPt from "@/lib/diagnosticoData";
import * as diagnosticoEn from "@/lib/data/en/diagnostico";
import * as diagnosticoEs from "@/lib/data/es/diagnostico";
import * as diagnosticoFr from "@/lib/data/fr/diagnostico";
import * as diagnosticoDe from "@/lib/data/de/diagnostico";
import * as diagnosticoZh from "@/lib/data/zh-CN/diagnostico";
import {
Activity,
AlertTriangle,
Check,
CheckCircle2,
ChevronDown,
Copy,
Cpu,
ExternalLink,
HelpCircle,
Layers,
LayoutGrid,
RotateCcw,
Search,
ShieldAlert,
Terminal,
Wifi,
Wrench,
X,
XCircle,
Zap,
} from "lucide-react";
/** Os rótulos dos sintomas vêm com emoji nos dados; a interface usa só texto e ícones. */
const semEmoji = (texto) => texto.replace(/^[\p{Extended_Pictographic}\uFE0F\s]+/u, "");
const ICONES = { Zap, Cpu, Layers, Activity, Terminal, Wifi };
const EXEMPLOS_LOG_PT = [
["Brownout", "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13"],
["LoadProhibited", "Guru Meditation Error: Core 0 panic'ed (LoadProhibited).\nEXCVADDR: 0x00000000"],
["Task watchdog", "Task watchdog got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
["Handshake TLS", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
];
const EXEMPLOS_LOG_EN = [
["Brownout", "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13"],
["LoadProhibited", "Guru Meditation Error: Core 0 panic'ed (LoadProhibited).\nEXCVADDR: 0x00000000"],
["Task watchdog", "Task watchdog got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
["TLS Handshake", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
];
const EXEMPLOS_LOG_ES = [
["Brownout", "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13"],
["LoadProhibited", "Guru Meditation Error: Core 0 panic'ed (LoadProhibited).\nEXCVADDR: 0x00000000"],
["Task watchdog", "Task watchdog got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
["Handshake TLS", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
];
const EXEMPLOS_LOG_FR = [
["Brownout", "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13"],
["LoadProhibited", "Guru Meditation Error: Core 0 panic'ed (LoadProhibited).\nEXCVADDR: 0x00000000"],
["Task watchdog", "Task watchdog got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
["Handshake TLS", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
];
const EXEMPLOS_LOG_DE = [
["Brownout", "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13"],
["LoadProhibited", "Guru Meditation Error: Core 0 panic'ed (LoadProhibited).\nEXCVADDR: 0x00000000"],
["Task watchdog", "Task watchdog got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
["TLS-Handshake", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
];
const EXEMPLOS_LOG_ZH = [
["Brownout", "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13"],
["LoadProhibited", "Guru Meditation Error: Core 0 panic'ed (LoadProhibited).\nEXCVADDR: 0x00000000"],
["任务看门狗", "Task watchdog got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
["TLS 握手", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
];
const GRAVIDADE = {
"Crítica": "text-danger",
"Critical": "text-danger",
"Critique": "text-danger",
"Kritisch": "text-danger",
Alta: "text-gold",
High: "text-gold",
"Élevée": "text-gold",
Hoch: "text-gold",
"Média": "text-accent",
Medium: "text-accent",
Media: "text-accent",
Moyenne: "text-accent",
Mittel: "text-accent",
"严重": "text-danger",
"高": "text-gold",
"中": "text-accent",
};
const CHECKLISTS_PT = [
{
icon: Zap,
titulo: "Hardware e alimentação",
itens: [
"Capacitor de 10 µF a 100 µF de baixo ESR no VCC, mais um cerâmico de 100 nF junto ao chip.",
"LDO dimensionado para picos de 600 mA a 1 A, com folga térmica.",
"Nenhum periférico forçando nível lógico nos pinos de strapping durante o boot.",
"Capacitor de 10 µF ou 1 µF no pino EN (RST) para auto-reset estável na gravação.",
],
},
{
icon: Layers,
titulo: "Firmware e FreeRTOS",
itens: [
"Toda tarefa FreeRTOS cede a CPU com vTaskDelay, filas ou semáforos.",
"Toda ISR marcada com IRAM_ATTR e sem chamadas bloqueantes.",
"Sem uso contínuo de String do Arduino em laços, para evitar fragmentação do heap.",
"Horário sincronizado por SNTP antes de abrir conexões TLS/HTTPS.",
],
},
];
const CHECKLISTS_EN = [
{
icon: Zap,
titulo: "Hardware and Power Supply",
itens: [
"10 µF to 100 µF low-ESR capacitor on VCC, plus a 100 nF ceramic capacitor close to the chip.",
"LDO regulator sized for 600 mA to 1 A current spikes, with adequate thermal margin.",
"No external peripherals forcing logic levels on strapping pins during reset and boot.",
"10 µF or 1 µF capacitor on the EN (RST) pin for stable auto-reset flashing.",
],
},
{
icon: Layers,
titulo: "Firmware and FreeRTOS",
itens: [
"Every FreeRTOS task yields CPU time with vTaskDelay, queues, or semaphores.",
"Every ISR marked with IRAM_ATTR with zero blocking calls.",
"Avoid continuous heap reallocations using Arduino Strings in tight loops.",
"Clock synchronized via SNTP before attempting TLS/HTTPS handshakes.",
],
},
];
const CHECKLISTS_ES = [
{
icon: Zap,
titulo: "Hardware y alimentación",
itens: [
"Condensador de 10 µF a 100 µF de bajo ESR en VCC, más uno cerámico de 100 nF junto al chip.",
"Regulador LDO dimensionado para picos de 600 mA a 1 A, con margen térmico.",
"Ningún periférico forzando niveles lógicos en los pines de strapping durante el arranque.",
"Condensador de 10 µF o 1 µF en el pin EN (RST) para auto-reset estable durante la programación.",
],
},
{
icon: Layers,
titulo: "Firmware y FreeRTOS",
itens: [
"Cada tarea de FreeRTOS cede la CPU con vTaskDelay, colas o semáforos.",
"Cada ISR marcada con IRAM_ATTR y sin llamadas bloqueantes.",
"Sin uso continuo de String de Arduino en bucles para evitar la fragmentación del heap.",
"Hora sincronizada mediante SNTP antes de abrir conexiones TLS/HTTPS.",
],
},
];
const CHECKLISTS_FR = [
{
icon: Zap,
titulo: "Matériel et alimentation",
itens: [
"Condensateur de 10 µF à 100 µF à faible ESR sur VCC, plus un céramique de 100 nF près de la puce.",
"Régulateur LDO dimensionné pour des pointes de 600 mA à 1 A, avec marge thermique.",
"Aucun périphérique ne forçant de niveau logique sur les broches de strapping pendant le boot.",
"Condensateur de 10 µF ou 1 µF sur la broche EN (RST) pour un auto-reset stable au flashage.",
],
},
{
icon: Layers,
titulo: "Firmware et FreeRTOS",
itens: [
"Chaque tâche FreeRTOS cède la CPU avec vTaskDelay, des files ou des sémaphores.",
"Chaque ISR marquée avec IRAM_ATTR et exempte d'appels bloquants.",
"Pas d'usage continu de String d'Arduino dans les boucles, afin d'éviter la fragmentation du tas.",
"Horloge synchronisée par SNTP avant d'ouvrir des connexions TLS/HTTPS.",
],
},
];
const CHECKLISTS_DE = [
{
icon: Zap,
titulo: "Hardware und Spannungsversorgung",
itens: [
"10 µF bis 100 µF Low-ESR-Kondensator an VCC, plus ein 100-nF-Keramikkondensator nah am Chip.",
"LDO-Regler ausgelegt für 600 mA bis 1 A Stromspitzen mit thermischer Reserve.",
"Keine Peripheriegeräte, die beim Booten Logikpegel an Strapping-Pins erzwingen.",
"10 µF- oder 1 µF-Kondensator am EN-Pin (RST) für zuverlässigen Auto-Reset beim Flashen.",
],
},
{
icon: Layers,
titulo: "Firmware und FreeRTOS",
itens: [
"Jeder FreeRTOS-Task gibt CPU-Zeit mit vTaskDelay, Queues oder Semaphoren ab.",
"Jede ISR mit IRAM_ATTR versehen und frei von blockierenden Aufrufen.",
"Keine fortlaufende Verwendung von Arduino-Strings in Schleifen zur Vermeidung von Heap-Fragmentierung.",
"Uhrzeit vor dem Aufbau von TLS/HTTPS-Verbindungen per SNTP synchronisiert.",
],
},
];
const CHECKLISTS_ZH = [
{
icon: Zap,
titulo: "硬件与供电",
itens: [
"VCC 上接 10 µF 到 100 µF 的低 ESR 电容,并在靠近芯片处再加一个 100 nF 陶瓷电容。",
"LDO 稳压器按 600 mA 到 1 A 的电流峰值选型,并留有足够的散热余量。",
"复位和启动期间,没有外部外设在启动配置引脚上强制逻辑电平。",
"EN(RST)引脚上接 10 µF 或 1 µF 电容,使自动复位烧录更稳定。",
],
},
{
icon: Layers,
titulo: "固件与 FreeRTOS",
itens: [
"每个 FreeRTOS 任务都通过 vTaskDelay、队列或信号量让出 CPU 时间。",
"每个 ISR 都标记了 IRAM_ATTR,且不包含任何阻塞调用。",
"避免在紧凑循环中使用 Arduino String 造成持续的堆重新分配。",
"在尝试 TLS/HTTPS 握手之前,先通过 SNTP 同步时钟。",
],
},
];
export default function DiagnosticoPage() {
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const isEs = pathname?.startsWith("/es");
const isFr = pathname?.startsWith("/fr");
const isDe = pathname?.startsWith("/de");
const isZh = pathname?.startsWith("/zh-CN");
const { categoriasDiagnostico, sintomasRapidos, errosCatalogo, regrasDecodificadorLog } = isEs ? diagnosticoEs : isEn ? diagnosticoEn : isFr ? diagnosticoFr : isZh ? diagnosticoZh : isDe ? diagnosticoDe : diagnosticoPt;
const exemplosLog = isEs ? EXEMPLOS_LOG_ES : isEn ? EXEMPLOS_LOG_EN : isFr ? EXEMPLOS_LOG_FR : isZh ? EXEMPLOS_LOG_ZH : isDe ? EXEMPLOS_LOG_DE : EXEMPLOS_LOG_PT;
const checklists = isEs ? CHECKLISTS_ES : isEn ? CHECKLISTS_EN : isFr ? CHECKLISTS_FR : isZh ? CHECKLISTS_ZH : isDe ? CHECKLISTS_DE : CHECKLISTS_PT;
const [termoBusca, setTermoBusca] = useState("");
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
const [sintomaAtivo, setSintomaAtivo] = useState(null);
const [cardAberto, setCardAberto] = useState(null);
const [linguagemCodigo, setLinguagemCodigo] = useState({}); // { [id]: 'arduino' | 'espidf' }
const [copiadoId, setCopiadoId] = useState(null);
const searchInputRef = useRef(null);
// Estado do Analisador de Logs
const [logInput, setLogInput] = useState("");
const [resultadoLog, setResultadoLog] = useState(null);
// Função de Análise do Log
const analisarLog = (texto) => {
setLogInput(texto);
if (!texto || texto.trim() === "") {
setResultadoLog(null);
return;
}
for (const regra of regrasDecodificadorLog) {
if (regra.padrao.test(texto)) {
setResultadoLog(regra);
return;
}
}
setResultadoLog(
isEs
? {
titulo: "Patrón de error no categorizado automáticamente",
tipo: "Desconocido",
explicacao:
"No identificamos una firma clásica de Guru Meditation o Brownout en este fragmento. Verifique que la velocidad de baudios (ej: 115200) sea correcta y si el encabezado 'rst:' o 'panic' está presente.",
acaoRecomendada:
"Busque palabras clave como 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' o 'panic' en la consola serie.",
}
: isEn
? {
titulo: "Error pattern not automatically classified",
tipo: "Unknown",
explicacao:
"We could not identify a classic Guru Meditation or Brownout signature in this snippet. Verify that the baud rate (e.g. 115200) is correct and whether the 'rst:' or 'panic' header is present.",
acaoRecomendada:
"Look for keywords such as 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog', or 'panic' in the serial output.",
}
: isFr
? {
titulo: "Modèle d'erreur non catégorisé automatiquement",
tipo: "Inconnu",
explicacao:
"Nous n'avons pas identifié de signature classique de Guru Meditation ou Brownout dans cet extrait. Vérifiez que la vitesse en bauds (ex. : 115200) est correcte et si l'en-tête 'rst:' ou 'panic' est présent.",
acaoRecomendada:
"Recherchez des mots-clés tels que 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' ou 'panic' dans la console série.",
}
: isZh ? {
titulo: "未能自动归类的错误模式",
tipo: "未知",
explicacao: "在这段内容中没有识别出典型的 Guru Meditation 或 Brownout 特征。请检查波特率(例如 115200)是否正确,以及是否包含以 'rst:' 或 'panic' 开头的信息。",
acaoRecomendada: "在串口控制台中查找 'Guru Meditation'、'rst:'、'LoadProhibited'、'watchdog' 或 'panic' 等关键字。",
} : isDe
? {
titulo: "Fehlermuster nicht automatisch kategorisiert",
tipo: "Unbekannt",
explicacao:
"Wir konnten in diesem Ausschnitt keine klassische Guru-Meditation- oder Brownout-Signatur erkennen. Prüfen Sie, ob die Baudrate (z. B. 115200) korrekt ist und ob der Header 'rst:' oder 'panic' vorhanden ist.",
acaoRecomendada:
"Suchen Sie in der seriellen Konsole nach Begriffen wie 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' oder 'panic'.",
}
: {
titulo: "Padrão de erro não categorizado automaticamente",
tipo: "Desconhecido",
explicacao:
"Não identificamos uma assinatura clássica de Guru Meditation ou Brownout neste trecho. Verifique se a taxa de baud rate (ex: 115200) está correta e se o cabeçalho 'rst:' ou 'panic' está presente.",
acaoRecomendada:
"Procure por palavras-chave como 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' ou 'panic' no console serial.",
}
);
};
const copiarParaClipboard = (texto, id) => {
navigator.clipboard.writeText(texto);
setCopiadoId(id);
setTimeout(() => setCopiadoId(null), 2000);
};
// Filtragem dos erros
const errosFiltrados = useMemo(() => {
const termo = termoBusca.toLowerCase().trim();
return errosCatalogo.filter((erro) => {
const matchCategoria =
categoriaAtiva === "todos" || erro.categoria === categoriaAtiva;
const matchSintoma =
!sintomaAtivo || erro.sintomas.includes(sintomaAtivo);
const matchBusca =
!termo ||
erro.titulo.toLowerCase().includes(termo) ||
erro.resumo.toLowerCase().includes(termo) ||
erro.causaRaiz.toLowerCase().includes(termo) ||
erro.logSerial.toLowerCase().includes(termo);
return matchCategoria && matchSintoma && matchBusca;
});
}, [categoriaAtiva, sintomaAtivo, termoBusca, errosCatalogo]);
const temFiltroAtivo =
termoBusca.trim() !== "" ||
categoriaAtiva !== "todos" ||
sintomaAtivo !== null;
const limparTodosFiltros = () => {
setTermoBusca("");
setCategoriaAtiva("todos");
setSintomaAtivo(null);
searchInputRef.current?.focus();
};
const toggleCard = (id) => {
setCardAberto((prev) => (prev === id ? null : id));
};
const setLang = (id, lang) => {
setLinguagemCodigo((prev) => ({ ...prev, [id]: lang }));
};
const totalFiltros = (termoBusca.trim() ? 1 : 0) + (categoriaAtiva !== "todos" ? 1 : 0) + (sintomaAtivo ? 1 : 0);
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
<PageHero
isEn={isEn}
isEs={isEs}
isFr={isFr}
isDe={isDe} isZh={isZh}
trail={[{ label: isEs ? "Diagnóstico" : isEn ? "Diagnostics" : isFr ? "Diagnostic" : isZh ? "诊断" : isDe ? "Diagnose" : "Diagnóstico" }]}
icon={AlertTriangle}
label={isEs ? "Depuración de firmware" : isEn ? "Firmware Debugging" : isFr ? "Débogage du firmware" : isZh ? "固件调试" : isDe ? "Firmware-Debugging" : "Depuração de firmware"}
title={isEs ? "Diagnóstico de fallas del ESP32" : isEn ? "ESP32 Diagnostics and Crash Decoder" : isFr ? "Diagnostic des pannes de l'ESP32" : isZh ? "ESP32 故障诊断" : isDe ? "ESP32-Fehlerdiagnose und Crash-Decoder" : "Diagnóstico de falhas do ESP32"}
lede={
isEs
? "Comprenda la causa raíz de las fallas por Brownout, Guru Meditation, Watchdog y Stack Overflow, y aplique la corrección de ingeniería en lugar de soluciones improvisadas."
: isEn
? "Understand the root cause of Brownout, Guru Meditation, Watchdog, and Stack Overflow crashes, and apply engineering solutions instead of workarounds."
: isFr
? "Comprenez la cause racine des pannes de Brownout, Guru Meditation, Watchdog et Stack Overflow, et appliquez la correction d'ingénierie plutôt qu'un bricolage."
: isZh ? "弄清 Brownout、Guru Meditation、Watchdog 和栈溢出的成因,并采用工程上的正确修复,而不是临时补救。" : isDe
? "Verstehen Sie die Ursache von Brownout, Guru Meditation, Watchdog und Stack Overflow und wenden Sie saubere Ingenieurlösungen statt Notbehelfen an."
: "Entenda a causa de Brownout, Guru Meditation, Watchdog e Stack Overflow, e aplique a correção de engenharia, não o remendo."
}
>
<a
href="https://cienciaembarcada.com.br/publicacoes/esp32-5-erros-comuns-e-como-resolve-los-profissionalmente/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong"
>
{isEs ? "Basado en el artículo de Ciência Embarcada" : isEn ? "Based on article by Ciência Embarcada" : isFr ? "D'après l'article de Ciência Embarcada" : isZh ? "基于 Ciência Embarcada 的文章" : isDe ? "Basierend auf dem Artikel von Ciência Embarcada" : "Baseado no artigo do Ciência Embarcada"}
<ExternalLink className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> {isEs ? "(abre en una nueva pestaña)" : isEn ? "(opens in a new tab)" : isFr ? "(ouvre un nouvel onglet)" : isZh ? "(在新标签页打开)" : isDe ? "(öffnet in neuem Tab)" : "(abre em nova aba)"}</span>
</a>
</PageHero>
<section className="shell pt-10 sm:pt-12" aria-labelledby="decodificador-titulo">
<div className="panel p-5 sm:p-8">
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-end">
<div className="max-w-xl">
<h2 id="decodificador-titulo" className="section-title">
{isEs ? "Pegue el registro y descubra la causa" : isEn ? "Paste the log to see the cause" : isFr ? "Collez le journal et découvrez la cause" : isZh ? "粘贴日志,查看原因" : isDe ? "Log einfügen und Ursache ermitteln" : "Cole o log e veja a causa"}
</h2>
<p className="mt-2 text-sm text-muted">
{isEs ? "Copie el fragmento del monitor serie. El diagnóstico aparece al instante." : isEn ? "Copy the serial monitor output snippet. Diagnostics appear instantly." : isFr ? "Copiez l'extrait du moniteur série. Le diagnostic apparaît immédiatement." : isZh ? "复制串口监视器中的内容,诊断结果立即显示。" : isDe ? "Kopieren Sie den Ausschnitt aus dem seriellen Monitor. Die Diagnose erscheint sofort." : "Copie o trecho do monitor serial. O diagnóstico aparece na hora."}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted">{isEs ? "Probar con" : isEn ? "Test with" : isFr ? "Tester avec" : isZh ? "试试这些" : isDe ? "Testen mit" : "Testar com"}</span>
{exemplosLog.map(([rotulo, texto]) => (
<button key={rotulo} type="button" onClick={() => analisarLog(texto)} className="chip-btn">
{rotulo}
</button>
))}
</div>
</div>
<div className="relative mt-5">
<label htmlFor="log-serial" className="sr-only">
{isEs ? "Registro del monitor serie" : isEn ? "Serial monitor log" : isFr ? "Journal du moniteur série" : isZh ? "串口监视器日志" : isDe ? "Serieller Monitor-Log" : "Log do monitor serial"}
</label>
<textarea
id="log-serial"
rows={4}
value={logInput}
onChange={(e) => analisarLog(e.target.value)}
placeholder={isEs ? "Pegue el registro del monitor serie aquí" : isEn ? "Paste serial monitor log here" : isFr ? "Collez le journal du moniteur série ici" : isZh ? "在此粘贴串口监视器日志" : isDe ? "Seriellen Monitor-Log hier einfügen" : "Cole o log do monitor serial aqui"}
spellCheck={false}
className="code-panel field !min-h-32 w-full resize-y !rounded-xl !border-white/10 !bg-[var(--code-bg)] p-4 font-mono text-sm !text-[#e9eef4] placeholder:!text-[#a6afbd]"
/>
{logInput && (
<button type="button" onClick={() => analisarLog("")} className="absolute right-3 top-3 inline-flex min-h-11 items-center gap-1 rounded-full border border-white/15 px-3 text-xs font-medium text-[#e9eef4] hover:bg-white/10">
<X className="h-3.5 w-3.5" aria-hidden="true" />
{isEs ? "Limpiar" : isEn ? "Clear" : isFr ? "Effacer" : isZh ? "清除" : isDe ? "Löschen" : "Limpar"}
</button>
)}
</div>
<div role="status" aria-live="polite">
{resultadoLog && (
<div className="callout callout-warn mt-4 flex items-start gap-3">
<ShieldAlert className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
<div className="min-w-0 space-y-2">
<div className="flex flex-wrap items-center gap-2">
<h3 className="font-semibold text-ink">{resultadoLog.titulo}</h3>
<span className="chip">{resultadoLog.tipo}</span>
</div>
<p><strong className="font-semibold text-ink">{isEs ? "Causa probable:" : isEn ? "Likely cause:" : isFr ? "Cause probable :" : isZh ? "可能的原因:" : isDe ? "Wahrscheinliche Ursache :" : "Causa provável:"}</strong> {resultadoLog.explicacao}</p>
<p><strong className="font-semibold text-ink">{isEs ? "Qué hacer:" : isEn ? "What to do:" : isFr ? "Que faire :" : isZh ? "该怎么做:" : isDe ? "Was zu tun ist :" : "O que fazer:"}</strong> {resultadoLog.acaoRecomendada}</p>
</div>
</div>
)}
</div>
</div>
</section>
<section className="shell section pt-12 sm:pt-14" aria-labelledby="catalogo-titulo">
<h2 id="catalogo-titulo" className="section-title">
{isEs ? "Catálogo de fallas" : isEn ? "Fault catalog" : isFr ? "Catalogue de pannes" : isZh ? "故障目录" : isDe ? "Fehlerkatalog" : "Catálogo de falhas"}
</h2>
<div className="panel mt-6 p-5 sm:p-6">
<div className="relative">
<label htmlFor="busca-erro" className="sr-only">
{isEs ? "Buscar por error, síntoma o mensaje serie" : isEn ? "Search by error, symptom, or serial message" : isFr ? "Rechercher par erreur, symptôme ou message série" : isZh ? "按错误、症状或串口信息搜索" : isDe ? "Nach Fehler, Symptom oder serieller Meldung suchen" : "Buscar por erro, sintoma ou mensagem serial"}
</label>
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
id="busca-erro"
ref={searchInputRef}
type="search"
value={termoBusca}
onChange={(e) => setTermoBusca(e.target.value)}
placeholder={isEs ? "Buscar error, síntoma o mensaje" : isEn ? "Search error, symptom, or message" : isFr ? "Rechercher une erreur, un symptôme ou un message" : isZh ? "搜索错误、症状或信息" : isDe ? "Fehler, Symptom oder Meldung suchen" : "Buscar erro, sintoma ou mensagem"}
autoComplete="off"
className="field pl-12 pr-4 text-base"
/>
</div>
<div className="mt-5">
<h3 className="text-sm font-semibold text-ink">{isEs ? "Síntoma" : isEn ? "Symptom" : isFr ? "Symptôme" : isZh ? "症状" : isDe ? "Symptom" : "Sintoma"}</h3>
<div className="mt-3 flex flex-wrap gap-2">
{sintomasRapidos.map((sintoma) => (
<button key={sintoma.id} type="button" onClick={() => setSintomaAtivo(sintomaAtivo === sintoma.id ? null : sintoma.id)} aria-pressed={sintomaAtivo === sintoma.id} className="chip-btn">
{semEmoji(sintoma.rotulo)}
</button>
))}
</div>
</div>
<div className="mt-5 border-t border-line pt-5">
<h3 className="text-sm font-semibold text-ink">{isEs ? "Área" : isEn ? "Area" : isFr ? "Domaine" : isZh ? "领域" : isDe ? "Bereich" : "Área"}</h3>
<div className="mt-3 flex flex-wrap gap-2">
<button type="button" onClick={() => setCategoriaAtiva("todos")} aria-pressed={categoriaAtiva === "todos"} className="chip-btn">
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
{isEs ? "Todas" : isEn ? "All" : isFr ? "Tous" : isZh ? "全部" : isDe ? "Alle" : "Todas"}
<span className="tabular-nums opacity-80">{errosCatalogo.length}</span>
</button>
{categoriasDiagnostico.map((cat) => {
const Icone = ICONES[cat.icone] ?? AlertTriangle;
const total = errosCatalogo.filter((e) => e.categoria === cat.id).length;
return (
<button key={cat.id} type="button" onClick={() => setCategoriaAtiva(cat.id)} aria-pressed={categoriaAtiva === cat.id} className="chip-btn">
<Icone className="h-4 w-4" aria-hidden="true" />
{cat.nome.split(/ &|,/)[0]}
<span className="tabular-nums opacity-80">{total}</span>
</button>
);
})}
</div>
</div>
</div>
<div className="mt-6 flex flex-wrap items-center justify-between gap-3">
<p role="status" aria-live="polite" className="text-sm font-semibold text-ink">
{errosFiltrados.length} {errosFiltrados.length === 1 ? (isEs ? "falla" : isEn ? "fault" : isFr ? "panne" : isZh ? "个故障" : isDe ? "Fehler" : "falha") : (isEs ? "fallas" : isEn ? "faults" : isFr ? "pannes" : isZh ? "个故障" : isDe ? "Fehler" : "falhas")}
{totalFiltros > 0 && <span className="font-normal text-muted">{isEs ? " con los filtros actuales" : isEn ? " with current filters" : isFr ? " avec les filtres actuels" : isZh ? "(当前筛选下)" : isDe ? " mit aktuellen Filtern" : " com os filtros atuais"}</span>}
</p>
{temFiltroAtivo && (
<button type="button" onClick={limparTodosFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{isEs ? "Restablecer filtros" : isEn ? "Reset filters" : isFr ? "Réinitialiser les filtres" : isZh ? "重置筛选" : isDe ? "Filter zurücksetzen" : "Redefinir filtros"}
</button>
)}
</div>
<div className="mt-3 space-y-3">
{errosFiltrados.length === 0 ? (
<div className="card px-6 py-14 text-center">
<HelpCircle className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h3 className="mt-4 font-display text-xl font-semibold text-ink">
{isEs ? "Ninguna falla coincide con estos filtros" : isEn ? "No faults matching these filters" : isFr ? "Aucune panne ne correspond à ces filtres" : isZh ? "没有符合这些筛选条件的故障" : isDe ? "Keine Fehler entsprechen diesen Filtern" : "Nenhuma falha com esses filtros"}
</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
{isEs ? "Busque por un término más general o elimine el síntoma y el área seleccionados." : isEn ? "Search for a more general term or remove selected symptom and area." : isFr ? "Recherchez un terme plus général ou retirez le symptôme et le domaine sélectionnés." : isZh ? "请搜索更通用的词,或去掉已选的症状和领域。" : isDe ? "Suchen Sie nach einem allgemeineren Begriff oder entfernen Sie gewähltes Symptom und Bereich." : "Busque por um termo mais geral ou remova o sintoma e a área selecionados."}
</p>
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : isFr ? "Effacer les filtres" : isZh ? "清除筛选" : isDe ? "Filter zurücksetzen" : "Limpar filtros"}
</button>
</div>
) : (
errosFiltrados.map((erro) => {
const estaAberto = cardAberto === erro.id;
const lang = linguagemCodigo[erro.id] || "arduino";
const codigoAtual =
lang === "arduino" ? erro.codigoArduino : lang === "espidf" ? erro.codigoEspIdf : erro.codigoMicroPython || (isEs ? "# Ejemplo no aplicable" : isEn ? "# Example not applicable" : isFr ? "# Exemple non applicable" : isZh ? "# 示例不适用" : isDe ? "# Beispiel nicht anwendbar" : "# Exemplo não aplicável");
const catInfo = categoriasDiagnostico.find((c) => c.id === erro.categoria);
const IconeCat = ICONES[catInfo?.icone] ?? AlertTriangle;
return (
<article key={erro.id} id={erro.id} className={`card scroll-mt-28 overflow-hidden ${estaAberto ? "border-line-strong" : ""}`}>
<h3>
<button
type="button"
onClick={() => toggleCard(erro.id)}
aria-expanded={estaAberto}
aria-controls={`detalhe-${erro.id}`}
className="flex w-full cursor-pointer items-start justify-between gap-4 p-5 text-left sm:p-6"
>
<span className="min-w-0">
<span className="flex flex-wrap items-center gap-2">
<span className="chip">
<IconeCat className="h-3.5 w-3.5 text-accent" aria-hidden="true" />
{catInfo?.nome}
</span>
<span className={`chip ${GRAVIDADE[erro.gravidade] ?? "text-accent"}`}>
{isEs ? (
<>Severidad {erro.gravidade.toLowerCase()}</>
) : isEn ? (
<>Severity: {erro.gravidade.toLowerCase()}</>
) : isFr ? (
<>Gravité : {erro.gravidade.toLowerCase()}</>
) : isZh ? ( <>严重程度:{erro.gravidade}</> ) : isDe ? (
<>Schweregrad: {erro.gravidade.toLowerCase()}</>
) : (
<>Gravidade {erro.gravidade.toLowerCase()}</>
)}
</span>
</span>
<span className="mt-3 block font-display text-xl font-semibold tracking-[-0.03em] text-ink">{erro.titulo}</span>
<span className="mt-1.5 block text-sm font-normal leading-relaxed text-muted">{erro.resumo}</span>
</span>
<span className="mt-1 grid h-9 w-9 shrink-0 place-items-center rounded-full bg-surface-2 text-muted">
<ChevronDown className={`h-4 w-4 transition-transform ${estaAberto ? "rotate-180" : ""}`} aria-hidden="true" />
</span>
</button>
</h3>
{estaAberto && (
<div id={`detalhe-${erro.id}`} className="space-y-6 border-t border-line p-5 sm:p-6">
<div>
<div className="flex items-center justify-between gap-3">
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
<Terminal className="h-4 w-4 text-accent" aria-hidden="true" />
{isEs ? "Cómo aparece en el monitor serie" : isEn ? "How it appears in serial monitor" : isFr ? "Apparence dans le moniteur série" : isZh ? "在串口监视器中的样子" : isDe ? "Erscheinungsbild im seriellen Monitor" : "Como aparece no monitor serial"}
</h4>
<button type="button" onClick={() => copiarParaClipboard(erro.logSerial, `log-${erro.id}`)} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
{copiadoId === `log-${erro.id}` ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
{copiadoId === `log-${erro.id}` ? (isEs ? "Copiado" : isEn ? "Copied" : isFr ? "Copié" : isZh ? "已复制" : isDe ? "Kopiert" : "Copiado") : (isEs ? "Copiar registro" : isEn ? "Copy log" : isFr ? "Copier le log" : isZh ? "复制日志" : isDe ? "Log kopieren" : "Copiar log")}
</button>
</div>
<div className="code-panel mt-1">
<pre tabIndex={0}><code className="text-[#e9eef4]">{erro.logSerial}</code></pre>
</div>
</div>
<div>
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
<Cpu className="h-4 w-4 text-accent" aria-hidden="true" />
{isEs ? "Causa raíz" : isEn ? "Root cause" : isFr ? "Cause racine" : isZh ? "根本原因" : isDe ? "Ursache" : "Causa raiz"}
</h4>
<p className="callout mt-2">{erro.causaRaiz}</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="callout callout-danger">
<p className="flex items-center gap-2 font-semibold text-ink">
<XCircle className="h-4 w-4 shrink-0 text-danger" aria-hidden="true" />
{isEs ? "La solución improvisada común" : isEn ? "The common workaround" : isFr ? "Le bricolage courant" : isZh ? "常见的临时补救" : isDe ? "Der verbreitete Notbehelf" : "O remendo comum"}
</p>
<p className="mt-2 font-medium text-ink">{erro.gambiarra.titulo}</p>
<p className="mt-1">{erro.gambiarra.descricao}</p>
<p className="mt-2"><strong className="font-semibold text-danger">{isEs ? "Consecuencia:" : isEn ? "Consequence:" : isFr ? "Conséquence :" : isZh ? "后果:" : isDe ? "Folge :" : "Consequência:"}</strong> {erro.gambiarra.consequencia}</p>
</div>
<div className="callout callout-ok">
<p className="flex items-center gap-2 font-semibold text-ink">
<CheckCircle2 className="h-4 w-4 shrink-0 text-ok" aria-hidden="true" />
{isEs ? "La solución de ingeniería" : isEn ? "The engineering solution" : isFr ? "La solution d'ingénierie" : isZh ? "工程上的解决方案" : isDe ? "Die Ingenieurlösung" : "A solução de engenharia"}
</p>
<p className="mt-2 font-medium text-ink">{erro.solucaoEngenharia.titulo}</p>
<ul className="mt-2 space-y-1.5">
{erro.solucaoEngenharia.passos.map((passo) => (
<li key={passo} className="flex items-start gap-2">
<Check className="mt-1 h-3.5 w-3.5 shrink-0 text-ok" aria-hidden="true" />
{passo}
</li>
))}
</ul>
</div>
</div>
<div>
<div className="flex flex-wrap items-center justify-between gap-3">
<div role="group" aria-label={isEs ? "Lenguaje del ejemplo" : isEn ? "Example language" : isFr ? "Langage de l'exemple" : isZh ? "示例语言" : isDe ? "Sprache des Beispiels" : "Linguagem do exemplo"} className="flex flex-wrap gap-2">
{[
["arduino", "Arduino C++"],
["espidf", "ESP-IDF (C)"],
["micropython", "MicroPython"],
].map(([id, rotulo]) => (
<button key={id} type="button" onClick={() => setLang(erro.id, id)} aria-pressed={lang === id} className="chip-btn">
{rotulo}
</button>
))}
</div>
<button type="button" onClick={() => copiarParaClipboard(codigoAtual, `code-${erro.id}`)} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
{copiadoId === `code-${erro.id}` ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
{copiadoId === `code-${erro.id}` ? (isEs ? "Copiado" : isEn ? "Copied" : isFr ? "Copié" : isZh ? "已复制" : isDe ? "Kopiert" : "Copiado") : (isEs ? "Copiar código" : isEn ? "Copy code" : isFr ? "Copier le code" : isZh ? "复制代码" : isDe ? "Code kopieren" : "Copiar código")}
</button>
</div>
<div className="code-panel mt-3">
<pre tabIndex={0}><code className="text-[#e9eef4]">{codigoAtual}</code></pre>
</div>
</div>
<div className="callout callout-warn flex items-start gap-3">
<Wrench className="mt-0.5 h-4 w-4 shrink-0 text-gold" aria-hidden="true" />
<p><strong className="font-semibold text-ink">{isEs ? "Consejo de ingeniería:" : isEn ? "Engineering tip:" : isFr ? "Conseil d'ingénierie :" : isZh ? "工程提示:" : isDe ? "Praxistipp :" : "Dica de engenharia:"}</strong> {erro.dicaPro}</p>
</div>
</div>
)}
</article>
);
})
)}
</div>
</section>
<section className="shell pb-20 md:pb-28" aria-labelledby="checklist-titulo">
<h2 id="checklist-titulo" className="section-title">
{isEs ? "Lista de verificación previa a campo" : isEn ? "Pre-deployment checklist" : isFr ? "Checklist avant déploiement" : isZh ? "上线前的检查清单" : isDe ? "Checkliste vor dem Feldeinsatz" : "Checklist antes de ir para campo"}
</h2>
<p className="mt-3 max-w-2xl text-base text-muted">
{isEs ? "Elementos esenciales para validar en hardware y firmware antes de entregar el producto." : isEn ? "Essential items to validate on hardware and firmware before shipping a product." : isFr ? "Éléments essentiels à valider sur le matériel et le firmware avant de livrer le produit." : isZh ? "在交付产品之前,需要在硬件和固件上验证的关键项目。" : isDe ? "Wesentliche Punkte zur Prüfung von Hardware und Firmware vor der Auslieferung." : "Itens essenciais para validar no hardware e no firmware antes de entregar o produto."}
</p>
<div className="mt-8 grid gap-4 md:grid-cols-2">
{checklists.map(({ icon: Icon, titulo, itens }) => (
<div key={titulo} className="card p-5 sm:p-7">
<h3 className="flex items-center gap-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
<span className="grid h-9 w-9 place-items-center rounded-full bg-accent-soft text-accent">
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
{titulo}
</h3>
<ul className="mt-5 space-y-3">
{itens.map((item) => (
<li key={item} className="flex items-start gap-3 text-sm leading-relaxed text-ink-soft">
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-ok" aria-hidden="true" />
{item}
</li>
))}
</ul>
</div>
))}
</div>
</section>
</main>
<Footer />
</div>
);
}