> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/pt-br/architecture/frontend.md).

# Frontend

O aplicativo de página única em Vue 3: registro de ferramentas, estado, roteamento e os barramentos de eventos e comandos do app.

Tudo em `frontend/` é uma SPA em Vue 3 usando `<script setup>`, Pinia, vue-router no modo de histórico HTML5, vue-i18n e Tailwind CSS v4 sobre primitivas shadcn-vue copiadas. Sem TypeScript.

`App.vue` é um shell fino — provedor de tooltip, host de toast, prompt de instalação do PWA, tema e `<router-view>`. É também onde os pipelines orientados a eventos são inicializados exatamente uma vez.

## Roteamento

`frontend/router/index.js` declara quatro rotas reais e uma rota coringa:

| Caminho            | Componente              | Observações                                                       |
| ------------------ | ----------------------- | ----------------------------------------------------------------- |
| `/`                | `Home.vue`              | Importado de forma imediata — a página inicial padrão             |
| `/tools/:slug`     | `StandaloneTool.vue`    | Página completa de uma ferramenta, compartilhável e indexável     |
| `/privacy`         | `PrivacyPolicy.vue`     |                                                                   |
| `/r/:id`           | `report/ReportPage.vue` | Relatório diagnóstico compartilhado somente para leitura, noindex |
| `/:pathMatch(.*)*` | —                       | Redireciona para `/`                                              |

Tudo, exceto `Home` é importado sob demanda para ficar fora do bundle da página inicial. `scrollBehavior` deliberadamente **não** rola quando apenas a query muda no mesmo caminho — é isso que abrir e fechar a gaveta de ferramentas faz.

Ferramentas avançadas têm um segundo ponto de entrada: na página inicial, `?tool=<slug>` abre o mesmo componente dentro de uma gaveta inferior. Ambos os pontos de entrada renderizam o mesmo `.vue` arquivo; apenas o wrapper é diferente.

Uma ferramenta cujo resultado vale a pena linkar carrega sua entrada na query como `?q=`, em ambos os pontos de entrada — `/tools/ipcalculator?q=…` e `/?tool=ipcalculator&q=…`. O componente lê `route.query.q` na montagem e a executa, depois grava a query de volta com `router.replace` após cada execução, de modo que a barra de endereços sempre descreva o resultado visível sem crescer o histórico com uma entrada por execução. `IpCalculator.vue` é o exemplo a ser seguido.

A gaveta é o *único* ponto de entrada para uma ferramenta cuja entrada no registro define `noStandalone: true` — `/tools/<slug>` redireciona para `/?tool=<slug>` em vez de renderizar. Uma ferramenta recebe essa flag quando lê estado pertencente à página inicial: o Persona Check cruza os resultados dos testes da página inicial, e executá-los de uma página independente faria sair dela.

## O registro de ferramentas

`frontend/data/tools.js` é a única fonte da verdade para ferramentas avançadas. Ele exporta um array ordenado e um mapa de busca:

```js
export const ADVANCED_TOOLS = [
  { slug: 'whois', emoji: '📓', titleKey: 'whois.Title',
    noteKey: 'advancedtools.Whois',
    component: () => import('@/components/advanced-tools/Whois.vue') },
  // …
];

export const TOOL_BY_SLUG = new Map(ADVANCED_TOOLS.map((t) => [t.slug, t]));
```

Campos de entrada:

| Campo                  | Significado                                                                                                            |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `slug`                 | Identificador estável usado por ambos `?tool=<slug>` e `/tools/:slug`                                                  |
| `emoji`                | Símbolo no card e no cabeçalho da gaveta                                                                               |
| `titleKey` / `noteKey` | chaves de i18n para o título e a descrição de uma linha                                                                |
| `componente`           | Sob demanda `import()` do `.vue` arquivo                                                                               |
| `requiresOriginalSite` | Gatilho opcional; omitido significa que a ferramenta é pública                                                         |
| `noStandalone`         | Opcional. `true` significa que a ferramenta existe apenas como a gaveta da página inicial — sem `/tools/<slug>` página |

Três consumidores derivam dele, por isso adicionar uma entrada costuma ser o trabalho inteiro:

* **`Advanced.vue`** mapeia o array para a grade de cards (`cards`), filtra-o contra `configs.originalSite` (`enabledCards`), e resolve a ferramenta ativa da gaveta com `TOOL_BY_SLUG.get(route.query.tool)`. O componente sob demanda resolvido é armazenado em cache em um `Map` para que re-renderizações não remontem uma ferramenta em execução. Um clique esquerdo simples em um card chama `router.push({ path: '/', query: { tool } })`; cliques com modificadores e no botão do meio caem para o `<a href>` para que a página independente abra em uma nova aba. Para uma `noStandalone` ferramenta que `href` é `/?tool=<slug>` em vez disso, e o ícone de abrir em nova aba da gaveta fica oculto.
* **`StandaloneTool.vue`** resolve `TOOL_BY_SLUG.get(route.params.slug)`, envolve-o em `defineAsyncComponent`, define um título, descrição e URL canônica localizados por ferramenta via `use-document-meta.js`, e redireciona para `/` em caso de slug desconhecido. Uma `noStandalone` ferramenta é tratada como não registrada aqui, mas mantém seu destino: o redirecionamento leva `?tool=<slug>` para que a página inicial a abra na gaveta. Observe que o próprio roteador tem apenas uma rota dinâmica — quem faz a resolução é o registro, não uma tabela de rotas gerada.
* **`Nav.vue`** lista as mesmas ferramentas na navegação, aplicando o mesmo `requiresOriginalSite` filtro.

{% hint style="info" %}
`requiresOriginalSite: true` oculta uma ferramenta em instâncias auto-hospedadas, porque ela precisa da API privada do IPCheck.ing e de um usuário autenticado. A flag é avaliada contra `store.configs.originalSite`, que o backend deriva do referer da requisição — veja [Recursos vinculados ao IPCheck.ing](/developer/pt-br/configuration/features-tied-to-ipcheck-ing.md).
{% endhint %}

Outros registros ficam ao lado dele em `data/`: `sections.js` (os IDs das seções da página inicial que alimentam a navegação, o rastreamento de rolagem e o estado de carregamento), `ip-databases.js` (as fontes de geolocalização entre as quais os usuários podem alternar), `achievements.js` e `achievement-rules.js`, `connectivity-import-lists.js` (os membros padrão da lista da seção Connectivity, os pacotes curados que seu diálogo de importação oferece, e os limites do modelo de múltiplas listas — alvos por lista, quantidade de listas, comprimento do nome da lista; cada membro vem com um ícone versionado em `public/favicons/`, obtido por `pnpm fetch-favicons`, e pode trazer uma `siteUrl` substituição para o link de abrir site do card), `persona-tables.js` (as duas tabelas de referência que o In-depth Persona Check precisa antes de qualquer requisição — idiomas plausíveis por país e as fontes que marcam um sistema de escrita), `changelog.json`, e `default-preferences.js`.

O arquivo de dados de Connectivity permanece declarativo: a lógica por trás das listas gerenciadas pelo usuário na seção — saneamento no boot do modelo armazenado, as proteções de CRUD das listas (a lista inicial "Mine" não pode ser excluída nem renomeada e mantém ao menos um membro), e o planejamento de importação curada (deduplicação de hostname dentro da lista de destino, tudo ou nada em relação ao limite por lista) — está em funções puras em `utils/connectivity-lists.js`, especificadas por `tests/connectivity-lists.test.js`.

Um subdiretório de `data/` é deliberadamente **não** um registro versionado: `data/banners/` está no .gitignore, dados de tempo de deploy para o slot de banner por seção (`components/widgets/InfoBanner.vue`) que toda seção da página inicial conecta em sua parte inferior — veja [Banners de seção](/developer/pt-br/configuration/section-banners.md).

## Ferramentas sem backend

Browser Info, a Security Checklist e a IP Calculator nunca chamam `/api`. A entrada no registro é idêntica — nada em `tools.js` diz se uma ferramenta tem rota — então o que as diferencia é onde a lógica delas vive: módulos puros em `utils/` (ou `common/`, quando o backend os compartilha) com uma especificação para cada um, e um componente que apenas renderiza.

A IP Calculator (`ipcalculator`) é o exemplo mais completo, em três camadas:

| Camada                | Arquivo                                                         | Função                                                                                                                                                                                                                                                                                                                     |
| --------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Aritmética            | `common/ip-math.js`                                             | aritmética de endereços BigInt para ambas as famílias — parsers rígidos, formatação RFC 5952, máscaras e contagens, contenção, divisão / agregação / intervalo-para-CIDR. Sem imports; a busca de bootstrap RDAP do backend (`common/rdap.js`) usa o mesmo `parseCidr` / `prefixContains`. Adaptado em `utils/ip-math.js`. |
| Lógica da calculadora | `utils/ip-calc.js`                                              | `classifyInput(raw)` decide o que foi colado; os analisadores derivam tudo o que os cards exibem; `calculate(raw)` é o único ponto de entrada que o componente chama.                                                                                                                                                      |
| Interface             | `components/advanced-tools/IpCalculator.vue` + `ip-calculator/` | O shell (entrada, pills de exemplo, os IPs do próprio visitante vindos de `store.allIPs`, `?q=` sync) e um card de resultado por formato: `Ipv4Result`, `Ipv6Result`, `RangeResult`, construído com `CalcSection`, `ValueRow`, `PrefixBitmap` e `SubnetSplitter`.                                                          |

Duas regras se mantêm nas duas camadas inferiores: **nada lança exceção** — lixo gera `null` de um helper ou `{ kind: 'invalid', reason }` do classificador, e o componente mapeia `reason` para uma `ipcalculator.invalid.*` string — e **a saída do analisador já está pronta para exibição**: endereços são strings e contagens passam por `formatCount()`, então nenhum template jamais encontra um BigInt (que `JSON.stringify` também engasgaria). O classificador mantém o `valor` porque o mapa de bits precisa do número.

`classifyInput` aplica suas regras em uma ordem fixa, e a ordem é a gramática — um intervalo é verificado antes de uma lista, uma lista antes de um único token:

| `tipo`                    | Corresponde a                                                                                                                                                                               |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `intervalo`               | `a-b` (ou `a - b`); o final de um IPv4 pode ser um último octeto isolado (`10.0.0.1-254`; um par invertido é trocado e sinalizado                                                           |
| `cidr-list`               | Dois ou mais tokens separados por espaços, `,` ou `;`, cada um um prefixo ou um endereço; tokens inválidos são reportados, não fatais                                                       |
| `ipv4-cidr` / `ipv6-cidr` | `a.b.c.d/n`, `x::/n`, ou um IPv4 com máscara pontuada; os bits de host são mantidos em `endereço`, zerados em `rede`                                                                        |
| `ipv6`                    | Qualquer coisa com dois-pontos que `parseIPv6` aceita; um `%zone` sufixo e `[…]` são removidos primeiro, e a zona é mantida para exibição                                                   |
| `ipv4`                    | Quadra pontilhada estrita (`notação: 'dotted'`), ou um formato inet\_aton — `127.1`, `0177.0.0.1`, `0x7f.1`, um número octal isolado — sinalizado como `obfuscated: true` com sua `notação` |
| `hex`                     | `0x…` até 32 dígitos (≤ 8 → IPv4), ou uma string hexadecimal isolada de 32 dígitos                                                                                                          |
| `inteiro`                 | Um número decimal; família pela magnitude                                                                                                                                                   |
| `inválido`                | Todo o resto, com um `reason` código (`vazio`, `intervalo`, `cidr-prefix`, `ipv6-syntax`, `hex-too-large`, …)                                                                               |

A análise rígida (`01.2.3.4` não é um IPv4) fica em `ip-math.js`; a gramática leniente inet\_aton fica em `ip-calc.js` de propósito, rotulada como obfuscated — o objetivo é dizer ao visitante como um navegador leria a string, não escolher silenciosamente uma interpretação. Endereços MAC deliberadamente não são uma entrada: a ferramenta MAC Lookup é quem cuida deles, e aqui vive apenas a recuperação EUI-64 → MAC dentro de um identificador de interface IPv6.

`ip-calc.js` também carrega os registros de propósito especial da IANA como duas tabelas, `IPV4_SPECIAL_BLOCKS` / `IPV6_SPECIAL_BLOCKS` — linhas de `{ cidr, id, label, rfc[], scope, global }`, resolvidas pelo prefixo mais longo (`lookupBlocks`). `rótulo` é o nome em inglês do próprio registro e é renderizado como está; apenas a `escopo` palavra passa por `t()` (`ipcalculator.scope.*`). Um endereço IPv6 fora de todas as linhas é classificado como reservado pela IETF; um IPv4 é unicast global simples.

Um detalhe de interação que o shell controla: `PrefixBitmap` emite `update:prefix` em cada passo enquanto o controle deslizante é arrastado e `commit:prefix` ao soltar. O shell recalcula `analysis.cidr` em tempo real, mas avança `analysis.split` — o que `SubnetSplitter` lê — apenas no commit, porque uma lista de sub-redes encolhendo no meio do arrasto encurta a página, o navegador limita a rolagem e a alça é arrancada debaixo do ponteiro. `ValueRow` tem uma altura mínima fixa pelo mesmo motivo.

Especificações: `tests/ip-math.test.js` (o que também fixa a ponte em cada `comum` comum `tests/ip-calc.test.js`. Veja [Testes](/developer/pt-br/development/testing.md).

## Store do Pinia

`frontend/store.js` define uma store, `main`. Ela mantém estado compartilhado entre componentes em vez de detalhes por componente:

* **Sessão / autenticação** — `usuário`, `isSignedIn`, `isFireBaseSet`, além das ações de login, logout e listener de autenticação. `remoteUserInfo` guarda o perfil autenticado de `/api/getuserinfo`, incluindo o instantâneo de cota por recurso por trás do `quotaExceeded` getter (com `markQuotaExhausted()` fixando-a quando o backend responde 429). O getter é apenas consultivo — o backend aplica as cotas de forma autoritativa — e sua `ipinfo` flag é **apenas para exibição**: essa cota é contabilizada por IP único, então "esgotado" ainda permite consultas repetidas e nunca deve bloquear uma solicitação.
* **Flags de recursos do backend** — `configs`, preenchido de forma assíncrona por `fetchConfigs()` de `/api/configs`. Os componentes o leem de forma reativa, então a primeira renderização nunca espera essa ida e volta. Ao chegar, as flags também derivam a disponibilidade de cada fonte de geolocalização e, se a preferência de fonte salva não estiver mais configurada, migram-na para a mais próxima disponível.
* **Preferências do usuário** — `userPreferences`, lido de e gravado de volta em uma única chave versionada do `localStorage` key; chaves antigas são ignoradas, não migradas. A única entrada estruturada, o modelo de múltiplas listas da seção Conectividade, `connectivityLists` modelo de múltiplas listas, também passa por `sanitizeLists()` (`utils/connectivity-lists.js`) em cada carregamento: entradas lixo são descartadas, e um modelo ausente ou corrompido é reconstruído a partir das chaves de destino planas legadas — que são lidas para essa migração e mantidas para reversão, mas nunca são gravadas novamente.
* **Estado da página** — `mountingStatus` e `loadingStatus` (uma flag por seção de `data/sections.js`), `currentSection`, `isMobile`, `isDarkMode`, `openSheet`, o toast `alerta` slot.
* **IPs coletados** — `allIPs`, um array de `{ ip, country, location, asn, org }` mesclado a partir de vários componentes por meio de `updateAllIPs()`; fontes posteriores preenchem os campos que as anteriores deixaram vazios. Os seletores do Globalping e o gravador de histórico de IP o leem.
* **Fontes de geolocalização** — `ipDBs` (de `data/ip-databases.js`) com o `activeSources` getter; `habilitado` é derivado apenas da configuração, nunca alterado por falhas em tempo de execução.
* **Conquistas** — `userAchievements` mais um pipeline de atualização de slot único (`triggerUpdateAchievements` / `achievementToUpdate`) que `User.vue` observa e reporta ao backend.

Objetos de estado que não devem ser compartilhados entre instâncias da store são produzidos por fábricas (`createInitialIpDBs()`, `createMountingStatus()`, …) em vez de literais no nível do módulo.

## O barramento app-events

`frontend/utils/app-events.js` é deliberadamente pequeno: um `Map` mapeamento de nome de evento para um `Set` de manipuladores, `onAppEvent(event, handler)` retornando uma função de cancelamento de inscrição, `emitAppEvent(event, payload)`, e a chamada única `waitForAppEvent(event, { timeoutMs })`, que se resolve com o próximo payload de um evento (inscreva-se primeiro, depois acione o produtor). O disparo é fire-and-forget e um handler que lança erro é capturado, então um assinante com problema não pode quebrar o emissor. Ele não importa nada do Vue, então utils e módulos comuns também podem usá-lo.

Eventos registram fatos; quando algo precisa *fazer* uma execução de teste e receber seu resultado de volta, esse é [o barramento app-commands](#the-app-commands-bus) abaixo.

Os componentes emitem eventos de domínio **incondicionalmente** — "o teste de velocidade terminou", "a consulta whois foi executada" — e permanecem sem saber quem escuta. Três pipelines usam o barramento, cada um inicializado uma vez em `App.vue`.

```mermaid
flowchart TD
    CB["Chamadores (atalhos, orquestrador de atualização, PersonaCheck, assistente de docs)"]
    CMD["utils/app-commands.js"]
    C["Componentes (SpeedTest, Whois, IpInfos, …)"]
    BUS["utils/app-events.js"]
    AE["use-achievement-engine.js"]
    RC["use-report-collector.js"]
    PC["use-persona-collector.js"]
    SE["sentry-init.js"]
    ST["slot da store do Pinia → User.vue → backend"]
    SN["Instantâneos do relatório → diálogo de compartilhamento + /r/:id"]
    PO["Instantâneos de Persona → PersonaCheck.vue"]

    CB -->|"dispatchAppCommand('webrtc:run', {...})"| CMD
    CMD -->|"um proprietário por comando"| C
    C -->|"emitAppEvent('speedtest:finished', {...})"| BUS
    BUS --> AE --> ST
    BUS --> RC --> SN
    BUS --> PC --> PO
    BUS --> SE
```

### Motor de conquistas

`data/achievement-rules.js` mapeia eventos para slugs de conquistas. Cada regra é `{ event, slug, when? }`, em que `when` é um predicado puro sobre o payload:

```js
{ event: 'speedtest:finished', slug: 'RapidPace', when: (p) => p.downloadSpeed >= 500 },
```

`composables/use-achievement-engine.js` inscreve um listener por regra e assume todas as proteções transversais: descarta eventos quando o visitante não está autenticado, avalia `when`, ignora conquistas já desbloqueadas e então enfileira o slug. Como eventos de inicialização podem disparar antes que o instantâneo de conquistas da conta tenha sido carregado do backend, regras que correspondem antes da chegada desse instantâneo ficam em espera e são reavaliadas quando ele é aplicado — o estado inicial todo-falso nunca é confundido com "nada conquistado ainda". A store mantém uma conquista por vez, então desbloqueios simultâneos (um único teste de velocidade pode cruzar três limites) são despachados com 2 segundos de intervalo, e cada um é reavaliado no momento do despacho caso tenha sido desbloqueado enquanto aguardava.

Adicionar uma conquista, portanto, significa: uma entrada em `data/achievements.js`, uma regra em `data/achievement-rules.js`, e um novo evento de domínio somente se ainda não existir um adequado. Os componentes nunca são tocados.

### Coletor de relatórios

O relatório de diagnóstico compartilhável usa o mesmo barramento. Cada teste "minha rede" emite `<domain>:finished` com seu resultado estruturado completo. `composables/use-report-collector.js` passa cada payload pelo seu construtor em `utils/report-builders.js`, mantém o instantâneo mais recente por seção (o mais recente vence) e os expõe somente para leitura ao diálogo de compartilhamento e à `/r/:id` página. As estruturas das seções estão na lista de permissões de `common/report-schema.js`, o mesmo módulo em relação ao qual o backend valida os envios.

{% hint style="warning" %}
Os construtores falham de forma suave: um valor desconhecido descarta o campo silenciosamente. Se você mudar a semântica do resultado de um teste, atualize a lista de permissões do construtor e o enum do schema na mesma alteração, ou o campo simplesmente desaparecerá dos relatórios em vez de gerar erro.
{% endhint %}

Um campo precisa estar listado em **cinco** lugares para sobreviver a toda a jornada, e só os últimos quatro falham de forma explícita:

1. O `emitAppEvent()` chamado no componente — vários emissores selecionam manualmente campos do próprio estado em vez de espalhá-lo.
2. O construtor em `utils/report-builders.js`.
3. A especificação da seção em `common/report-schema.js`.
4. O `/r/:id` renderizador em `components/report/sections/`.
5. A tabela Markdown em `utils/report-export.js`, cujas colunas são listadas manualmente. (O download em JSON não precisa de nada — ele serializa o instantâneo coletado como está.)

Se você pular o passo 1 ou 5, tudo ainda passa: os testes alimentam os construtores diretamente, então nunca exercitam o emissor, e uma coluna Markdown ausente não é um erro. Adicione o campo em todos os lugares em uma única alteração.

Os handlers de comando se resolvem com o mesmo `<domain>:finished` payload, então a lista de permissões de campos também define o que a ferramenta do assistente de docs `run_my_tests` recebe de volta.

## O barramento app-commands

`frontend/utils/app-commands.js` é o gêmeo imperativo do barramento de eventos. Eventos dizem "isso aconteceu" — qualquer número de assinantes, sem valor de retorno. Um comando diz "faça isso" — exatamente um proprietário, e `dispatchAppCommand(name, payload, { timeoutMs })` retorna uma promise que se resolve quando o trabalho termina, com o resultado do proprietário. Como o barramento de eventos, ele não importa nada do Vue.

A superfície da API:

| Função                                             | Função                                                                                                    |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `registerAppCommand(name, handler)`                | Reivindica um comando; retorna uma função de cancelamento de registro que remove apenas o próprio handler |
| `dispatchAppCommand(name, payload, { timeoutMs })` | Executa o comando; se resolve com o resultado do handler                                                  |
| `hasAppCommand(name)`                              | Se o comando atualmente tem um proprietário                                                               |
| `waitForAppCommand(name, { timeoutMs })`           | Se resolve assim que o comando tem um proprietário — imediatamente, se já tiver                           |
| `appCommandError(code, message)`                   | Um verdadeiro `Error` que carrega um `código`, para rejeições estruturadas                                |

As rejeições carregam `error.code`, então os chamadores podem distinguir programaticamente uma execução bloqueada ou malformada de uma falha real:

| Código        | Produzido por                | Significado                                               |
| ------------- | ---------------------------- | --------------------------------------------------------- |
| `unavailable` | O barramento                 | Nenhum proprietário registrado para o comando             |
| `timeout`     | O barramento                 | O handler (ou a espera pelo registro) excedeu `timeoutMs` |
| `auth`        | Proprietários, por convenção | É necessário fazer login                                  |
| `quota`       | Proprietários, por convenção | Limite de uso atingido                                    |
| `input`       | Proprietários, por convenção | Payload ausente ou inválido                               |

Os proprietários podem adicionar códigos específicos do domínio, mas esses cinco mantêm seus significados em todo o barramento.

O contrato do payload é deliberadamente flexível: um único objeto JSON simples cuja estrutura é definida e documentada pelo proprietário do comando no local de registro — não existe um registro central de payloads.

Os componentes se registram por meio de `composables/use-app-command.js`, o binding do Vue: o registro acontece no setup, então o comando pode ser despachado assim que o proprietário existir, e a desmontagem do escopo do componente o desregistra. Cinco comandos estão registrados hoje:

| Comando            | Proprietário           | Payload                                                    |
| ------------------ | ---------------------- | ---------------------------------------------------------- |
| `ipinfo:refresh`   | `IpInfos.vue`          | `{ index }` opcional — um card, ou a grade inteira sem ele |
| `connectivity:run` | `ConnectivityTest.vue` | `{ trigger }`                                              |
| `webrtc:run`       | `WebRtcTest.vue`       | `{ isRefresh }`                                            |
| `dnsleak:run`      | `DnsLeaksTest.vue`     | `{ isRefresh }`                                            |
| `speedtest:toggle` | `SpeedTest.vue`        | nenhum — alternar entre executar / pausar / retomar        |

Os chamadores são os gatilhos desacoplados: os atalhos de teclado (`composables/use-shortcuts.js`), o orquestrador de atualização (`composables/use-refresh-orchestrator.js`), o reparo "Executar os testes ausentes" do Persona Check, e a `run_my_tests` ferramenta do assistente de docs.

{% hint style="warning" %}
**Gatilhos entre componentes passam pelo barramento — nunca por refs de template.** A Home costumava coletar refs para seus componentes de seção e chamar seus métodos; esse padrão acabou. As refs de template continuam apenas para elementos de interface (focar um input, medir um elemento), nunca para fazer outro componente trabalhar.
{% endhint %}

Os proprietários dos comandos vivem na rota inicial, então um chamador fora da home navega primeiro e depois emparelha `waitForAppCommand` com o despacho — o proprietário se registra durante o setup, e a espera faz a ponte entre `router.push('/')` a resolução e a montagem da seção. Persona Check e o assistente de docs seguem essa sequência; uma futura ferramenta montada em drawer que só possui um comando enquanto sua `?tool=` rota está aberta é chamada da mesma forma.

Um handler se resolve com o resultado do seu teste ao se apoiar no barramento de eventos em vez de construir um caminho de retorno pelo código do teste: ele se inscreve com `waitForAppEvent('<domain>:finished')`, inicia a execução e retorna essa promise — assim o chamador recebe exatamente o payload que o coletor de relatórios e o motor de conquistas veem. `IpInfos.vue`de `ipinfo:refresh` o handler é o padrão a ser copiado.

## Sequência de inicialização

`frontend/main.js` mantém o caminho crítico curto. Ele cria o app, Pinia, i18n e o router e então condiciona a primeira renderização a apenas três coisas, executadas em paralelo: o listener de autenticação (**único** para um visitante cujo `auth-hint` flag indica que ele estava autenticado), `store.loadPreferences()`, e `loadActiveLocaleMessages()`. Todo o resto é fire-and-forget ou adiado para depois da montagem — `store.fetchConfigs()`, analytics, a coleção de ícones de bandeiras (centenas de KB) e Sentry.

Ele também registra um `vite:preloadError` listener na avaliação do módulo: após um deploy, uma página carregada da build antiga falhará ao fazer lazy import de um chunk com hash que não existe mais, então o app recarrega uma vez (com um latch de timestamp por aba que impede um loop de recarga quando a causa real é uma rede offline).

## Locales sob demanda

`frontend/locales/i18n.js` cria a instância i18n com **vazio** mensagens e um mapa de carregadores construído por globbing de `locales/*.json` e mantendo tudo o que `common/locale-registry.js` declara — um arquivo de pacote e uma linha do registro precisam concordar para que um idioma exista. Apenas um locale fica ativo por carregamento de página — trocar de idioma persiste a escolha e reinicia o app — então `loadActiveLocaleMessages()` carrega toda a cadeia de fallback do locale ativo (variante → base → `en`), e `main.js` aguarda isso antes da montagem. Empacotar todos os pacotes de forma eager custava cerca de 44 KB gzipped de peso morto quando havia quatro deles, e o registro cresceu desde então.

O mesmo princípio vale para subpacotes: os conjuntos de dados de security-checklist e privacy em `locales/` são obtidos por glob e carregados pela própria view sob demanda, não pelo caminho de inicialização.

Depois que as mensagens chegam, `updateMeta()` define `document.documentElement.lang` a partir do `htmlLang` (`zh` declara `zh-CN`), o título da página e as meta tags de palavras-chave / descrição. As substituições por página vêm de `composables/use-document-meta.js`.

Um plugin Vite executado na build espelha a mesma ordem de escolha — preferência armazenada, `?hl=`, `navigator.language`, `en` — em um inline `<head>` script e `modulepreload`a cadeia enquanto o HTML ainda está sendo transmitido. Detalhes em [i18n](/developer/pt-br/development/i18n.md).

## Inicialização dinâmica com gatilho de ambiente

Duas integrações opcionais são controladas no momento da build, de modo que uma implantação auto-hospedada sem elas seja distribuída **nenhum** código relacionado.

* **Sentry** — controlado por `VITE_SENTRY_DSN_FRONTEND`. Sem o DSN, `sentry-init.js` nunca é importado e o SDK não entra no bundle. Com ele, o chunk carrega após a montagem — ou imediatamente se ocorrer um erro de pré-inicialização. Um pequeno buffer captura erros e rejeições não tratados antes da inicialização e faz o flush deles depois.
* **Firebase Auth** — controlado por `VITE_FIREBASE_API_KEY` + `VITE_FIREBASE_AUTH_DOMAIN` + `VITE_FIREBASE_PROJECT_ID`. `firebase-init.js` carrega `firebase/app` e `firebase/auth` na primeira `loadFirebaseAuth()` chamada — uma inicialização com usuário autenticado, um clique em entrar ou a verificação em segundo plano — e memoriza o resultado. Um visitante que nunca faz login nunca baixa o SDK.

<details>

<summary>Como a dica de autenticação escolhe o caminho de inicialização</summary>

`utils/auth-hint.js` armazena uma flag que descreve se a última sessão estava autenticada. Na inicialização `main.js` lê isso:

* `'1'` — carregue o Firebase e aguarde o listener de autenticação antes da primeira renderização, para que a primeira requisição autenticada já carregue o token.
* `'0'` — monte imediatamente; o SDK nunca é carregado até um clique em entrar.
* `null` (primeira visita desde que a flag foi lançada, ou armazenamento limpo) — monte imediatamente, depois faça uma verificação de autenticação em segundo plano após 3 segundos, para que a próxima inicialização siga o caminho exato.

</details>

Um terceiro recurso é controlado por variável de ambiente sem a mecânica de importação dinâmica: **Earth Online**, o painel de visitantes ao vivo na navegação — em dois níveis. `utils/pulse-beacon.js` deriva `PULSE_BEACON_URL` e `hasPulseBackend` de `VITE_PULSE_BEACON_URL`, e a metade social depende dessa flag: `widgets/Pulse.vue` oculta seu compositor de status, o feed Mais recente e o mapa de visitantes, `PrivacyPolicy.vue` omite sua seção, e `App.vue`o beacon de visita por carregamento de página retorna imediatamente. O próprio painel é mais amplo — seu feed de indisponibilidade usa o backend's `/api/cfradar?view=outages` — então a entrada da navegação e o vínculo de <kbd>p</kbd> em `use-shortcuts.js` aparecem quando qualquer `hasPulseBackend` ou a flag em tempo de execução `cloudFlare` de `/api/configs` está definida. O componente é um import estático normal, então isso é uma barreira de renderização, não uma barreira de bundle; o beacon em si é dispara-e-esquece, agendado para o tempo ocioso e engole todas as rejeições, então um serviço ausente ou quebrado nunca pode chegar ao restante do app. Veja [Recursos vinculados ao IPCheck.ing](/developer/pt-br/configuration/features-tied-to-ipcheck-ing.md).

O código do app nunca deve importar `@sentry/vue` diretamente — um import estático puxaria o SDK de volta para o bundle principal. Sinais explícitos passam pelo barramento de eventos em vez disso; `sentry-init.js` assina `ip-source:exhausted` (um card de IP cuja cadeia inteira de origem falhou) da mesma forma que o mecanismo de conquistas assina seus próprios eventos. A configuração fica em [Monitoramento de Erros](/developer/pt-br/configuration/error-monitoring.md).

## Posicionamento dos helpers

| Precisa de                           | Fica em                                                  |
| ------------------------------------ | -------------------------------------------------------- |
| Reatividade ou ciclo de vida do Vue  | `composables/` como `useXxx`                             |
| Nada específico do Vue               | `utils/` (nunca `use-` prefixado)                        |
| suporte ao shadcn (`cn()`)           | `lib/`                                                   |
| Algo de que o backend também precisa | `common/`, reexportado por meio de uma ponte em `utils/` |

As convenções para escrever esses arquivos estão em [Convenções de Código](/developer/pt-br/development/coding-conventions.md); o escopo dos testes está em [Testes](/developer/pt-br/development/testing.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/pt-br/architecture/frontend.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
