# Technical Documentation: hydralauncher/hydra

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/hydralauncher/hydra)

### Overview (`README.md`)

<div align="center">

[<img src="https://raw.githubusercontent.com/hydralauncher/hydra/refs/heads/main/resources/icon.png" width="144"/>](https://help.hydralauncher.gg)

  <h1 align="center">Hydra Launcher</h1>

  <p align="center">
    <strong>Hydra Launcher is an open-source gaming platform created to be the single tool that you need in order to manage your gaming library. Hydra is written in Node.js (Electron, React, Typescript) and Rust, with libtorrent providing the torrent engine.</strong>
  </p>

[](https://github.com/hydralauncher/hydra/actions)
[](https://github.com/hydralauncher/hydra/releases)
[](https://community.chocolatey.org/packages/hydralauncher)



</div>

## Features

- Add games that you own to your library
- Have a nice profile that shows what you are playing to your friends
- Save your game progress in the cloud with Hydra Cloud
- Unlock achievements
- Navigate through a rich catalogue with a powerful suggestion algorithm
- Discover new games that you haven't played before

## Build from source and contributing

Please, refer to our Documentation pages: [docs.hydralauncher.gg](https://docs.hydralauncher.gg/getting-started)

### Local development requirements

- Node.js + Yarn
- Rust toolchain (for `hydra-native`)
- Git and a C++ toolchain (Visual Studio C++ Build Tools on Windows, GCC/Clang on Linux, Xcode command-line tools on macOS). The native build obtains CMake and CTest automatically through vcpkg.

After installing dependencies, `postinstall` now builds the Rust native addon automatically (`hydra-native/hydra-native.node`).

The native build includes a Rust wrapper around pinned libtorrent. Development and packaged torrenting no longer require Python.

On Windows x64 and Linux x64, the libtorrent bridge comes prebuilt from `native/torrent-bridge/prebuilt`, so installs skip compiling libtorrent through vcpkg. Other platforms build it from source. When a branch push changes `native/torrent-bridge` or `scripts/build-torrent-bridge.cjs`, the "Torrent bridge prebuilts" workflow rebuilds the binaries and commits them to that branch; until then, installs build the bridge from source. Set `HYDRA_TORRENT_BRIDGE_FROM_SOURCE=1` to always build it from source.

## Contributors

<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
  <img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
</a>

## License

Hydra is licensed under the [MIT License](LICENSE).

---

### Overview (`.codex/README.md`)

# Codex environment

`environments/environment.toml` contains inline setup commands for macOS
in `[setup]` and Windows PowerShell in `[setup.win32]`. Git must be on PATH.

The setup copies the main Git checkout's root `.env` to the new worktree
by locating its shared `.git` directory.
It preserves an existing destination and skips copying if the source is missing.
The file stays ignored by Git, and its contents are never logged.

Commit the `.codex` directory so the setup is available in new worktrees.
Keep a local `.env` in the main checkout on each machine.

---

### Overview (`native/epic-prerequisite-runner/README.md`)

# Epic prerequisite supervisor

This small Windows PE runs inside the game's Proton prefix. It does not receive Epic credentials. The launcher passes one Windows path to a private UTF-8 JSON request:

```json
{
  "schemaVersion": 1,
  "kind": "exe",
  "executable": "C:\\Game\\setup.exe",
  "args": [],
  "cwd": "C:\\Game",
  "nonce": "attempt-id",
  "resultPath": "C:\\Private\\result.json"
}
```

The request and result must share a private directory. `kind: "msi"` resolves `msiexec.exe` through `GetSystemDirectoryW` and prepends `/i`; `kind: "reboot"` requires an empty executable and argument array and resolves `wineboot.exe -r` internally. No command shell or caller-selected reboot command is accepted.

The root starts suspended and joins a job before it can spawn children. The supervisor waits for the root and every member of that job, then atomically publishes `{schemaVersion:1,nonce,exitCode}`. `exitCode` is the complete Windows DWORD, including failures. The helper's own zero exit only means this result was published; the launcher decides whether the installer succeeded. Closing or killing the supervisor kills its job and does not publish a result. An isolated `--probe` invocation exits zero without reading or writing a request, for the initial Proton bootstrap.

On Linux, install the Rust `x86_64-pc-windows-gnu` target and `gcc-mingw-w64-x86-64`, then run `node scripts/build-epic-prerequisite-runner.cjs` from the project root. This builds locked dependencies, removes the PE timestamp, verifies PE32+ x64 and writes a source/binary SHA-256 manifest alongside the ignored development output. Packaging rebuilds stale output and verifies the copied resource again.

Run native Windows tests with `cargo test --locked --manifest-path native/epic-prerequisite-runner/Cargo.toml`. Linux CI additionally runs `xvfb-run -a node scripts/test-epic-prerequisite-wine.cjs` with Wine, wixl and MinGW installed. These disposable-prefix fixtures cover full exit codes, argument preservation, descendants, cancellation, a real MSI registry effect, and reboot rename/delete and RunOnce effects. Wine tests complement validation with the actual UMU/Proton selected by Hydra; they do not replace it.

---

### Epic Linux Download Launch Sdd (`docs/epic-linux-download-launch-sdd.md`)

# Epic Games no Linux com Legendary e UMU

SDD para implementação em 14 de outubro de 2026. Status: desenho fechado; implementação e aceite real no Linux pendentes.

Base: launcher `a4dfafb6621fc4e274158db59604dcdccc41a244`, branch `codex/epic-achievements-sync`. Esta especificação complementa `docs/epic-download-launch-cloud-saves-sdd.md` e substitui, para o alvo abaixo, a decisão anterior de manter a instalação gerenciada restrita ao Windows.

## Resultado esperado

Um usuário Linux escolhe Epic Games no seletor atual, escolhe a pasta, baixa pela fila do Hydra e clica em Jogar. A preparação do Proton, do prefixo e das dependências acontece automaticamente. A experiência usa os mesmos modais, controles de download, janela de preparação e opções de compatibilidade já existentes. Conta Epic, autenticação de lançamento, instalação, saves e acompanhamento da sessão continuam sob o controle do Hydra.

O alvo de execução desta entrega é Linux x86_64, incluindo Steam Deck. A distribuição Linux atual é construída pelo job Ubuntu x64; uma versão ARM64 do Legendary, isoladamente, não comprova execução dos jogos Windows nesse hardware. Conexão e importação existentes em outras arquiteturas continuam disponíveis. Tradução de arquitetura, lançamento Epic em macOS, gerenciamento novo de DLCs e atualização automática de jogos não fazem parte desta entrega. A compatibilidade de cada jogo com Proton, DRM e anticheat continua determinando se ele executa; o fluxo precisa distinguir uma falha do jogo de uma falha da integração.

## Decisões de implementação

| Assunto                        | Decisão                                                                                                                                                         |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Download                       | Manter Legendary 0.21.1 e seu workspace gerenciado atual. Linux baixa o build Epic `Windows`.                                                                   |
| Execução                       | Legendary produz parâmetros com `launch --json --no-wine`; Hydra os executa pelo UMU.                                                                           |
| UMU                            | Reutilizar o pacote, o updater e o monitor existentes, acrescentando uma API de processo supervisionado.                                                        |
| Proton                         | Versão válida escolhida no jogo, depois padrão global válido, depois `UMU-Proton` gerenciado pelo UMU. A preparação automática passa esse token explicitamente. |
| Instalação de Proton e runtime | UMU faz o provisionamento; Hydra não cria outro downloader de Proton e não exige Steam instalado.                                                               |
| Python                         | Reutilizar o `python-runner` privado já distribuído para o Legendary Linux. Python de sistema deixa de ser requisito deste fluxo.                               |
| Prefixo                        | Reutilizar seleção e raiz configuradas no Hydra; novos prefixos Epic têm identidade estável por conta e artefato.                                               |
| Pré-requisitos                 | Executar no mesmo prefixo e Proton, com resultado Windows completo coletado por um pequeno supervisor PE incluído no pacote.                                    |
| Interface                      | Nenhuma nova tela. Somente estados, mensagens e correções nos controles atuais.                                                                                 |
| Saves e sessão                 | Preparar prefixo antes do restore; acompanhar o processo Windows real no Linux antes de contar tempo ou enviar saves.                                           |
| Backend                        | Nenhum endpoint, migration ou alteração de identidade de biblioteca necessário.                                                                                 |

O Proton é escolhido uma vez por operação e convertido em caminho absoluto. Preparação, pré-requisitos e lançamento não podem resolver versões diferentes no meio do mesmo fluxo. A opção Automático mantém a usabilidade atual; sua última alternativa nesta integração é explicitamente `UMU-Proton`, sem depender do default implícito de uma versão do UMU.

## Comportamento na interface

### Baixar

O seletor oferece Epic quando existe posse importada no escopo atual e o main informa suporte à instalação gerenciada. O modal atual mostra os tamanhos, permite escolher diretório e inicia ou enfileira o download. Não há seleção de Wine, Proton ou prefixo nesse modal.

O download usa a lista e o slot atuais. Quando os arquivos terminarem, a fase existente `prerequisites` também representa a preparação necessária da compatibilidade. O rótulo dessa fase passa a ser neutro, como “Preparando instalação”. Bytes em 100% com preparação pendente continuam sendo uma instalação ativa. Concluir exige validar os arquivos, preparar o ambiente, resolver os pré-requisitos e associar a instalação.

Pausa, cancelamento, retomada e erro continuam nas ações atuais. Uma falha ao baixar o Proton ou instalar uma dependência permite tentar novamente a finalização, reaproveitando os arquivos do jogo.

### Jogar

Jogar usa o membro Epic operacional já instalado, inclusive quando a página apresenta dados Steam. Abre a janela de preparação atual. Ela apresenta o estado existente de preparação da compatibilidade, depois os estados de saves e de início do jogo. Proton e prefixo continuam configuráveis nas seções atuais de compatibilidade, no desktop e no Big Picture.

Sem configuração manual, o primeiro uso baixa o runtime necessário automaticamente. Sem rede e sem runtime em cache, a janela existente explica que a primeira preparação precisa de conexão e permite tentar novamente. Se o runtime já existir e o jogo permitir offline, jogar offline usa o mesmo prefixo, preserva saves locais e mantém o envio remoto pendente.

Não acrescentar wizard, login Epic adicional, importação pelo Heroic, escolha obrigatória de versão, tela de instalação de Proton ou seletor de executável para instalações comprovadas pelo manifesto. Conflito real de save continua usando o modal de conflito existente. Erros de permissão, prefixo inválido ou dependência do host aparecem no fluxo atual, com a ação pertinente, sem exigir terminal como parte do caminho normal.

## Evidência na base atual

As referências de linha abaixo correspondem ao commit-base e orientam a implementação.

| Local                                                                       | Comportamento atual                                                                                    | Alteração necessária                                                               |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| `src/main/services/epic/installation.ts:315,393,480`                        | Bloqueia qualquer host diferente de `win32` antes de consultar opções.                                 | Política única de host, arquitetura e build; permitir Linux x64 com build Windows. |
| `src/renderer/src/hooks/use-epic-managed.ts:13`                             | Disponibilidade verifica apenas funções IPC.                                                           | Consumir suporte à instalação informado pelo main.                                 |
| `src/main/services/epic/download.ts:497,540,573`                            | Recupera build existente, valida arquivos, executa pré-requisito e faz bind.                           | Preservar o fluxo; acrescentar preparação Linux e pré-requisito por prefixo.       |
| `src/main/helpers/launch-game.ts:254,519,758`                               | Já resolve Proton do jogo/global, prepara prefixo e calcula contexto Linux.                            | Extrair resolução compartilhada para download, lançamento e saves Epic.            |
| `src/main/helpers/launch-game.ts:649,783`                                   | Epic faz `spawn` direto do executável e ignora o runner Linux.                                         | Despachar pelo UMU preservando o payload autenticado.                              |
| `src/main/services/umu.ts:366`                                              | Retorna `void`, fixa diretório, registra argv/ambiente e ativa `PROTON_LOG`.                           | API com handle, cwd, ambiente, cancelamento e diagnóstico seguro.                  |
| `src/main/services/epic/launch-protocol.ts:28`                              | Exige `launch_command` vazio.                                                                          | Manter a garantia; wrapper não entra pelo JSON Legendary.                          |
| `src/main/services/epic/managed-process-tracker.ts:15`                      | Considera executável do host dentro da instalação; intermediários externos deixam inspeção incompleta. | Prova específica de processo Windows executado por Proton.                         |
| `src/main/services/native-addon.ts:156`                                     | `processTree` descarta contexto Linux útil.                                                            | Projeção mínima com identidade e dados de atribuição, sem argv/ambiente completo.  |
| `src/main/services/process-watcher.ts:402`                                  | Match heurístico pode prevalecer sobre o tracker Epic.                                                 | Tracker gerenciado autoritativo para sessões Epic iniciadas pelo Hydra.            |
| `src/main/services/epic/cloud-saves.ts:178,244,274`                         | Prefixo implícito pode cair no home Linux em consultas e sync manual.                                  | Usar o mesmo resolvedor de prefixo em todas as operações.                          |
| `resources/legendary/linux-python.sh`, `src/shared/legendary-manifest.json` | Runtime Python privado já existe e é verificado no pacote.                                             | Expor seus caminhos para UMU sem passar pelo wrapper Legendary.                    |

O bloqueio inicial foi reproduzido por injeção de host Linux: `getOptions()` retorna `{ options: [], error: "unsupported-platform" }` antes de capturar a conta. Isso explica os tamanhos vazios; não é uma falha de download do Legendary.

## Responsabilidades e contratos

```mermaid
flowchart TD
  UI[Modal Epic e fila existentes] --> DL[EpicDownloadService]
  DL --> LG[Legendary gerenciado]
  DL --> CTX[Contexto Linux compartilhado]
  PLAY[Jogar existente] --> CTX
  CTX --> UMU[UMU e Proton]
  PLAY --> AUTH[Parâmetros Epic pelo Legendary]
  AUTH --> UMU
  UMU --> TRACK[Tracker da sessão Linux]
  TRACK --> SAVE[Saves e playtime existentes]
```

`installation.ts` continua responsável por identidade, posse, metadados, instalação e paths. `download.ts` conserva a fila e a finalização. `epic/launch.ts` conserva autenticação e identidade da sessão. `umu.ts` executa processos; não conhece contas Epic nem publica dados de biblioteca. O tracker decide atividade e encerramento; o serviço de saves conserva conflito, backup, restore e verificação remota atuais.

Extrair de `launch-game.ts` a resolução comum de Proton/prefixo para um serviço pequeno de compatibilidade Linux. Acrescentar `src/main/services/epic/linux-runtime.ts` para coordenar essa resolução com owner, artefato, preparação e recibos Epic. Evitar dependência circular entre helpers de lançamento e serviços Epic.

Contratos internos propostos:

```ts
interface PreparedEpicLinuxContext {
  pythonRunnerPath: string;
  umuBinaryPath: string;
  umuVersion: string;
  protonPath: string; // caminho absoluto efetivo
  protonFingerprint: string;
  winePrefixPath: string; // prefixo Wine canônico
  prefixGeneration: string;
  gameId: string; // umu ID comprovado ou umu-default
  store: "egs";
}

interface UmuProcessHandle {
  pid: number;
  invocationId: string;
  exited: Promise<{
    code: number | null;
    signal: NodeJS.Signals | null;
  }>; // saída do wrapper, não prova isolada de fechamento do jogo
  terminate(): Promise<void>; // termina e confirma a árvore desta invocação
}
```

A nova entrada supervisionada do UMU recebe executável, `args: string[]`, cwd, ambiente validado, contexto preparado, finalidade (`game`, `prerequisite` ou `prepare`), `AbortSignal` e callbacks de estado. Retorna o handle ao observar `spawn`, sem esperar três segundos para registrar a sessão. O comportamento público atual de `Umu.launchExecutable` pode permanecer como adaptador para os consumidores existentes.

O callback de pré-requisito passa a receber `{ game, target, installed, executable, args, signal }`, permitindo escolher prefixo e owner. O ramo Windows continua usando seu executor e UAC atuais. Os DTOs de iniciar download e jogar permanecem suficientes; caminhos privados, handles e parâmetros autenticados não atravessam IPC.

## Suporte de plataforma e compatibilidade OTA

Centralizar a decisão de suporte no main, usada em opções, enqueue, prepare, read/repair e launch. Nesta entrega: Windows nos alvos já suportados e Linux x64 com build `Windows`. Não criar `EpicInstallPlatform = "Linux"` e não liberar `Mac` como consequência da remoção do guard.

Acrescentar um campo opcional local a `EpicConnectionState`:

```ts
managedInstallation?: {
  available: boolean;
  reason?: EpicInstallError;
};
```

Esse campo usa o IPC/evento de conexão existente, mas descreve instalação separadamente de login/importação. A ausência de Proton baixado não torna a plataforma indisponível: ela exige preparação recuperável. Falta de arquivo obrigatório no pacote deve gerar erro de runtime, não erro de credencial.

Desktop e Big Picture compartilham a checagem. Em main antigo sem o campo, o renderer mantém o comportamento legado Windows e trata Linux como dependente de atualização do aplicativo. Não inferir suporte Linux apenas da presença dos IPCs. O main sempre revalida; a UI não é uma barreira de segurança. A entrega exige pacote nativo atualizado, pois renderer OTA sozinho não altera serviço, Python, supervisor ou addon.

## Runtime e escolha do Proton

### Python e UMU distribuídos

Manter Legendary 0.21.1 e o bundle Linux já descrito no manifesto: CPython 3.13.16, release 20261003, e glibc privada com hashes. Expor um resolvedor de caminhos que devolva o `python-runner` ao lado desse runtime. Invocar `python-runner <umu-run> ...` diretamente. O wrapper `legendary` altera o cache para seu workspace e não deve ser usado para iniciar UMU.

O loader privado recebe `--library-path` somente nessa execução, sem exportar `LD_LIBRARY_PATH` para o jogo. O zipapp UMU distribuído contém suas dependências Python. O pacote deve passar por smoke real de imports, HTTPS e execução; a compatibilidade estática desses arquivos não substitui esse teste.

Preservar o updater UMU e a seleção de binário gerenciado quando houver adaptador de protocolo validado para aquela versão. Inicialmente, o contrato é o UMU 1.4.4 distribuído. Uma versão gerenciada sem contrato conhecido usa o 1.4.4 incluído, sem bloquear o usuário por um update automático ainda não homologado. Fixar o caminho/versão do binário enquanto a operação existir; a limpeza de versões antigas não pode remover arquivos em uso.

### Resolução automática sem outro gerenciador

A prioridade é `game.protonPath` válido, `defaultProtonPath` válido e, por último, o token explícito `UMU-Proton`. Reutilizar validação e descoberta existentes. Um path removido segue a resolução válida seguinte, como no Hydra atual; uma operação já preparada não troca de runner silenciosamente.

UMU faz download e validação do Proton e do Steam Linux Runtime, conforme o [gerenciador de Proton 1.4.4](https://github.com/Open-Wine-Components/umu-launcher/blob/1.4.4/umu/umu_proton.py). Não consultar releases do GE-Proton em um segundo serviço. Um Proton customizado continua selecionável pelos controles existentes e precisa satisfazer os requisitos do UMU utilizado.

Para fixar o caminho efetivo da seleção automática, executar `createprefix` sem autenticação Epic, com `UMU_LOG=debug` capturado exclusivamente em pipe privado. O adaptador do UMU 1.4.4 extrai apenas a atribuição final `PROTONPATH=<caminho absoluto>` emitida após a resolução. Validar `realpath`, `proton`, `toolmanifest.vdf` e identidade da versão; descartar o restante da saída. Não persistir dump de ambiente, escolher a primeira pasta encontrada nem depender do nome de um symlink mutável.

Nota da implementação: o smoke do UMU 1.4.4 confirmou que `createprefix` pode preparar o prefixo e retornar `1` pela ausência de um executável. O bootstrap usa o supervisor distribuído em modo `--probe`, sem autenticação e com saída `0` comprovada, mantendo a resolução privada de `PROTONPATH` e evitando aceitar um código de falha ambíguo.

Esse diagnóstico não é uma API JSON pública: mantê-lo isolado, versionado e coberto por fixture do zipapp distribuído. Prefixo válido sem runner efetivo comprovado não conclui a preparação. Depois disso, passar `PROTONPATH` absoluto em todas as chamadas. Para o próximo lançamento, Automático pode resolver a versão estável então disponível; dentro da sessão e de sua finalização, a identidade é fixa.

Usar os locks de download/runtime do UMU e uma preparação compartilhada no Hydra para cliques concorrentes. Offline carrega o caminho absoluto e os fingerprints do último contexto preparado no recibo, revalida os arquivos locais, não chama o updater Hydra e passa `UMU_RUNTIME_UPDATE=0` com `PROTONPATH` absoluto. Não executar bootstrap com token de release nesse caminho. Sem recibo/cache válido, retornar erro recuperável antes de obter parâmetros autenticados.

No UMU 1.4.4, a flag de runtime não elimina toda sondagem de metadata remota. O requisito é executar com cache mesmo sem rede, sem exigir atualização bem-sucedida nem trocar arquivos/runner; a sondagem deve respeitar o timeout do upstream. Não prometer ausência de qualquer tentativa de rede. O teste offline precisa bloquear a rede de fato e comprovar a execução, inclusive o comportamento desse timeout.

### Identidade para protonfixes

Passar `STORE=egs`. Resolver `GAMEID` por correspondência exata de loja e `artifactId/app_name` em snapshot local do CSV oficial [Open-Wine-Components/umu-database](https://github.com/Open-Wine-Components/umu-database#readme). O gerador de snapshot filtra `egs`, normaliza somente as chaves conforme o upstream e registra commit, hash e licença do CSV importado. Correspondências conflitantes são rejeitadas no build. O arquivo não precisa conter títulos nem biblioteca do usuário.

Ausência de entrada usa `umu-default`. Não sintetizar um umu ID a partir de título, `namespace:item`, canonicalId ou Steam appId apenas visual. Esse mapa pequeno permite aplicar os fixes disponíveis sem uma consulta remota obrigatória ao jogar. Atualizá-lo no fluxo de release; o updater do UMU continua responsável pelo runtime.

## Prefixo e persistência

Respeitar um `game.winePrefixPath` explicitamente configurado e canônico. Para instalações novas sem escolha explícita, usar a raiz de prefixos já configurada no Hydra, com segmento `epic/<hash(environment,userId,epicAccountId,artifactId,platform)>`. Isso preserva a preferência global de local e separa contas, lojas e artefatos. O contexto Epic não usa o prefixo externo do Legendary/Heroic.

O resolvedor é a autoridade de download, pré-requisito, launch, consulta de saves, sync manual, conflito e pós-saída. Um prefixo automático é derivado dessa identidade; não precisa virar uma segunda preferência. Prefixos explícitos existentes são preservados, sem migração destrutiva.

Acrescentar a consulta de leitura `getGameLinuxCompatibility(shop, objectId)`, com bridge opcional e validação de sender/identidade, para os controles atuais do desktop e Big Picture. Ela retorna `{ winePrefixPath, protonName, protonSource }` efetivos a partir do resolvedor e do recibo local, sem download, refresh de conta ou criação de prefixo. `protonSource` identifica `game`, `global` ou `umu`; `protonName` pode ser nulo antes do primeiro bootstrap. Não devolver credenciais ou preferências duplicadas. Remover a concatenação local `root/objectId` para Epic e mostrar o path retornado nos controles atuais. Em bridge antiga, usar a indicação Automático existente; o renderer não inventa um path Epic. Proteger a chamada com checagem de existência e incluir no teste OTA.

Reutilizar as utilidades de canonicalização, validação de registry/perfil e geração durável de prefixo. Preparação sem registry e perfil válidos falha antes de restore ou spawn. Troca ou recriação do prefixo invalida recibos de pré-requisito e referências de save do ambiente anterior. Acrescentar a `EpicInstallation` os campos opcionais `savePathSource: "automatic" | "custom"` e `savePathPrefixGeneration`. A resolução automática e `commitAnchor()` mantêm a origem; uma escolha no seletor atual grava `custom`. Paths automáticos são recalculados na mudança de prefixo; os explicitamente escolhidos conservam a validação atual.

Na migração, um path legado igual ao path automático comprovado do prefixo corrente pode ser marcado como automático. Os demais são preservados sem movimentação ou sobrescrita. Se a origem for ambígua e o ambiente mudar, limpar o anchor e exigir escolha pelo controle existente de pasta de saves, em vez de adivinhar. O anchor anterior não autoriza sobrescrever uma cópia local em outro prefixo: usar a resolução de conflito atual ao detectar mudança de ambos os lados.

Serializar mutações por prefixo canônico. Prefixo customizado compartilhado e ocupado por outra sessão não pode receber instalação de dependências ou restore. Não executar a limpeza genérica que mata processos pelo prefixo antes de provar que pertencem a uma invocação encerrada do Hydra. Jogos já abertos nunca são tratados como processos órfãos apenas por usarem o prefixo padrão.

Guardar no workspace Epic um recibo não credencial, com escrita atômica, para a preparação Linux: versão do esquema, geração/caminho canônico do prefixo, caminho absoluto efetivo/versão/fingerprint Proton, versão/fingerprint UMU, artefato/build e fingerprint do pré-requisito concluído. O fingerprint inclui os arquivos de identificação da ferramenta, não apenas seu nome de pasta. Esse arquivo registra o ambiente preparado; não substitui `installed.json`, `Game.winePrefixPath` ou `Game.protonPath`. Ele fornece o contexto local para offline. Se não houver geração durável, não confiar no recibo entre execuções; preparar novamente.

## Download e finalização

Preservar o comando e a fila atuais:

```text
legendary -y install <artifactId> --platform Windows
  --base-path <pasta> --game-folder <pasta-validada>
  --skip-dlcs --skip-sdl
```

Executar como argv separado, com a sessão e o workspace atuais. Manter verificação de posse, conexão, espaço, contenção e identidade do alias. Não promover importação Epic a jogo Hydra nem passar pelo pipeline de extração de repacks.

No Linux, finalizar nesta ordem: confirmar manifesto/build e executável; resolver contexto; preparar runtime/prefixo; executar pré-requisitos necessários; confirmar contexto atual e ausência de processos pendentes; fazer bind e marcar completo. Preparar o runtime também para jogos sem pré-requisito, deixando o primeiro Jogar pronto para o caminho normal.

O Legendary retoma pelos manifests e arquivos já concluídos; não prometer retomada exata do último byte. Pausa preserva arquivos e workspace. Se a build já estiver instalada, o retry entra na preparação/finalização sem repetir transferência. Cancelar interrompe também bootstrap ou pré-requisito, aguarda a árvore desta operação e conserva a possibilidade de retry. Remover da lista continua diferente de excluir a instalação.

A preparação ocupa o slot enquanto fizer parte da finalização, como os pré-requisitos Windows. Ela não segura a fila global de refresh de credenciais Epic: essa fila mantém a liberação após a fronteira autenticada já existente. Revalidar geração de conta/conexão antes de cada gravação final.

### Caminhos Windows em filesystem Linux

Executáveis e pré-requisitos podem ter barras ou casing diferentes do arquivo real. Resolver primeiro o path exato; se ele não existir, procurar os segmentos sem distinguir caixa somente para referências provenientes do build Windows. Exigir um único resultado em cada segmento. Depois de resolver, aplicar `realpath`, tipo de arquivo e contenção novamente.

Aplicar a mesma canonicalização à comparação do JSON de lançamento com o executável instalado. Não transformar paths Linux arbitrários em lowercase, aceitar ambiguidades de nome ou seguir symlink para fora da instalação. Cobrir diretórios com espaços, Unicode, volumes montados e a raiz `/mnt/storage` do relato. O prefixo padrão fica no local Linux já configurado; não movê-lo para o disco do jogo automaticamente.

## Pré-requisitos no Linux

O `-y` do Legendary não entrega essa instalação no Linux, como mostra o [pós-instalação no CLI fixado](https://github.com/legendary-gl/legendary/blob/42f6bdeadde3a9526dc8eb713763476999ac217b/legendary/cli.py). O Hydra executa o pré-requisito informado pelo manifesto, no Proton e prefixo já preparados. Desabilitar overlays, GameMode, opções de lançamento do usuário e parâmetros autenticados nessa chamada. Preservar os argumentos do instalador, sem inventar flags silenciosas.

O booleano global `prereq_info.installed` não comprova que uma dependência existe no prefixo atual. Considerar o recibo por geração do prefixo, artefato/build, hash do instalador, argumentos e fingerprint Proton. Revalidar também antes de jogar, pois o usuário pode trocar ou recriar o prefixo depois do download.

### Resultado Windows completo

Acrescentar `native/epic-prerequisite-runner`, um supervisor PE x64 pequeno, compilado do fonte versionado e empacotado com o Hydra Linux. Implementar em Rust com `serde_json` e `windows-sys`, usando `CreateProcessW`, espera do handle e `GetExitCodeProcess`. Ele recebe um arquivo JSON privado com executável, argv, cwd, nonce e destino do resultado; não recebe código de shell. Usar build `x86_64-pc-windows-gnu` no CI Linux e toolchain MinGW. O build deve produzir e verificar o artefato antes do empacotamento.

Para `.exe`, executar o instalador comprovado. Para `.msi`, o supervisor resolve `msiexec.exe` pelo diretório Windows de sistema (`GetSystemDirectoryW`) e executa `/i <pacote> ...args`, sem busca por PATH/cwd. Converter os caminhos consumidos por Win32 usando os verbos `getcompatpath` e `getnativepath` do Proton, chamados pelo mesmo UMU/contexto, e validar o roundtrip. Não presumir a existência de `Z:` em prefixos customizados.

O supervisor escreve atomicamente o DWORD completo retornado por `GetExitCodeProcess`, associado ao nonce. O main aceita somente resultado válido de arquivo privado regular, para a tentativa atual, seguido de encerramento comprovado da árvore do instalador. A política de pré-requisitos acompanha também filhos MSI/system PE fora da pasta do jogo, por token, prefixo e ancestralidade comprovada, inclusive após reparentamento; não aplica a regra de PE dentro da instalação usada para contar atividade do jogo. O resultado do processo raiz, sozinho, não conclui essa etapa.

Sucesso admite `0`, `3010` e `1641` comprovados. Para os dois últimos, após supervisor e filhos encerrarem e sob exclusão do prefixo, executar `wineboot.exe -r` do diretório Windows de sistema pelo mesmo supervisor, UMU e Proton. Esperar a invocação e seus processos de reboot, revalidar registry/perfil e só então persistir a preparação concluída. Reboot interrompido ou inválido continua pendente para retry. Nunca solicitar reboot do host. No Linux, os valores POSIX `194` e `105`, isoladamente, não comprovam esses códigos Windows.

Ausência de recibo, timeout, processo ainda ativo, falha ou cancelamento mantém a preparação incompleta. Reutilizar `EPIC_PREREQUISITE_TIMEOUT_MS` e a política de interrupção existente; constantes adicionais ficam nomeadas. Após sucesso, persistir o recibo Linux e atualizar `prereq_info.installed` sob o lock do workspace. A próxima tentativa pode repetir a preparação sem baixar os arquivos novamente.

Esse supervisor é necessário para manter o resultado de instaladores do fluxo Windows; não cria um novo gerenciador de dependências ou uma tela de instalação. Instaladores que exibirem sua própria interação mantêm essa interação no Wine.

## Lançamento autenticado

Preparar contexto e pré-requisitos antes de pedir o código de autenticação do jogo. Depois executar o gate de saves existente com esse prefixo, obter `legendary launch <artifactId> --json --no-wine`, validar o payload e revalidar a sessão imediatamente antes do spawn. A preparação longa não consome a validade do código retornado pela Epic.

Manter `launch_command` vazio no parser. Adicionar `--wrapper` ao JSON atual quebraria esse contrato; o wrapper pertence ao runner do Hydra. Encaminhar `game_parameters`, `user_parameters`, `egl_parameters`, ambiente e cwd como campos separados. `%command%`, GameMode e MangoHud continuam usando a composição existente, aplicada uma única vez ao comando UMU.

Para jogos `requires_ot`, o [core Legendary fixado](https://github.com/legendary-gl/legendary/blob/42f6bdeadde3a9526dc8eb713763476999ac217b/legendary/core.py) pode produzir `-epicovt=<path POSIX>` em `egl_parameters`. Esse argumento exige tradução de path, não cópia literal para o processo Windows. Durante o preflight, preparar o diretório `tmp` real e privado do workspace, sem symlink, validar/remover o arquivo anterior do basename esperado e mapear esse diretório pelo contexto Proton com roundtrip comprovado. Isso precisa acontecer antes do Legendary, cujo arquivo OVT tem nome determinístico e é aberto para escrita.

Após gerar o payload, aceitar somente arquivo regular contido nesse diretório, da sessão/artefato atuais, com leitura limitada e sem symlink. Converter somente o valor de `-epicovt`, usando o mapeamento de diretório já comprovado e o basename validado. Não fazer nova chamada de bootstrap/conversão UMU depois de obter o código de autenticação. O conteúdo do token e os demais argumentos autenticados permanecem inalterados.

Manter o arquivo OVT privado durante a vida da sessão, pois o processo Windows pode lê-lo depois do spawn. O handle da sessão conserva esse recurso e o remove após fechamento/cancelamento comprovado; no startup, reconciliar arquivos remanescentes antes de limpar. Não apagar um OVT ainda usado por jogo ativo ao desconectar a conta. O conteúdo e seu path autenticado não são publicados ou registrados em logs. Fixture e teste real de jogo `requires_ot` fazem parte do aceite.

A precedência do ambiente é: herdado do Hydra, ambiente Legendary validado, opções de ambiente do usuário e campos reservados finais da execução. `WINEPREFIX`, `PROTONPATH`, `STORE`, `GAMEID`, verbo Proton, ID da invocação e política de logging não podem ser substituídos por argumentos ou pelo payload remoto. UMU gera seus próprios campos de compatibilidade Steam. Rejeitar override dos parâmetros de identidade/autenticação Epic para impedir que opções do usuário alterem o vínculo comprovado. Não remover configurações gráficas legítimas do usuário por uma limpeza genérica de todo `LD_*`.

Para Epic gerenciado, falha do UMU é uma falha recuperável no fluxo atual. Não cair em `.exe` direto ou Wine de sistema com outro prefixo/runner. Uma execução sem autenticação só usa a opção offline já existente e os metadados que comprovam seu suporte.

### Diagnóstico sem credenciais

O caminho autenticado não usa os logs atuais de comando completo de `umu.ts`. Fixar `PROTON_LOG=0`, impedir debug UMU e capturar stdout/stderr em pipes tanto em desenvolvimento quanto no pacote. O [Proton registra argv em seu log quando habilitado](https://github.com/ValveSoftware/Proton/blob/proton_10.0/proton#L1694-L1702), fora do logger Hydra.

Converter somente eventos reconhecidos em códigos/estados seguros para os logs e a janela de preparação. Não salvar saída bruta de UMU/Proton/jogo, comando, ambiente Legendary ou exceção `spawn` com `spawnargs`. Logs operacionais podem conter versão do runtime, finalidade, fase, PID e códigos controlados. Limitar buffers/linhas com constantes. O diagnóstico de `createprefix` descrito acima ocorre sem payload Epic e é descartado após extrair o runner.

## Detecção do jogo e encerramento

O wrapper, o Python e o Wine loader não são o jogo. Criar um ID aleatório por invocação, passado como variável própria do Hydra, sem credenciais. Capturar PID e identidade de início antes de entregar o handle ao gate. O addon nativo projeta apenas PID, parentPID, start time, nome, cwd, executável do host, prefixo, ID da invocação e caminho PE comprovado quando disponível.

A extração do caminho Windows pode consultar `/proc/<pid>/cmdline` e mapas localmente e resolver as unidades do prefixo. Ela devolve somente o caminho canônico do PE; não exporta argv ou ambiente completo. Atribuir o jogo exige identidade da invocação ou ancestralidade já comprovada, prefixo correto e PE dentro da instalação validada. Nome de processo, cwd ou prefixo isolados não são suficientes. Identidade de início impede reutilização de PID. Comparações de paths Linux preservam caixa.

`EpicManagedProcessTracker` passa a distinguir atividade comprovada, preparação e inspeção desconhecida. Para uma sessão Epic gerenciada, esse resultado substitui o match heurístico genérico do watcher. Preservar a implementação Windows em seu ramo próprio.

Um filho reparentado conserva atribuição por token/prova anterior. Um wrapper ou overlay fora da pasta não conta playtime e não deixa o fechamento eternamente incompleto por sua mera presença. Um PE da sessão ainda capaz de escrever saves mantém o estado de execução. Falha de acesso ou snapshot incompleto gera estado desconhecido; não é evidência de lista vazia.

O encerramento exige desaparecimento comprovado dos processos de jogo/escrita atribuídos, inspeção completa e estabilização de saves existente. Inspeção completa se refere aos candidatos da própria invocação, delimitados por UID, ancestralidade, token, prefixo ou identidade previamente capturada; permissões ausentes em processos root/de outros usuários e sessões não tornam toda a máquina desconhecida. Uma falha de inspeção de candidato próprio suspende contagem e envio; recuperação de inspeção retoma a decisão. O handle UMU, `PR_SET_CHILD_SUBREAPER` e `waitforexitandrun` ajudam na observação, mas não são prova isolada de saída de toda a sessão. Após um crash do Hydra, reconciliar apenas provas duráveis da sessão/prefixo e manter upload pendente se o fechamento não puder ser provado.

Iniciar `EPIC_LAUNCH_DETECTION_TIMEOUT_MS` após preparação e spawn do jogo, não durante downloads de runtime ou instalação de dependências. Timeout antes de detectar jogo termina a preparação com erro controlado e encerra somente processos pertencentes àquela tentativa. Não usar os três segundos do monitor atual como prova de sucesso ou falha de um jogo de execução curta.

## Saves e playtime

Todas as entradas de saves resolvem o mesmo prefixo efetivo: consulta de estado, automático, manual, conflito, offline e pós-saída. No Linux, ausência de prefixo/perfil válido deixa o path não resolvido; nunca usa `app.getPath("home")` como perfil Windows. Resolver registry e diretórios reais do perfil antes de criar um caminho alternativo com casing diferente. Manter os filtros compatíveis com o Legendary no host; não tornar todo filtro case-insensitive apenas por ser um build Windows.

Reutilizar restauração em staging, backup, decisão de conflito, anchor, upload e verificação de leitura de volta atuais. Prefixo pronto e contexto atual são obrigatórios antes de restaurar. O restore acontece antes do processo autenticado. O upload ocorre uma vez após o fechamento real e mantém `pendingSaveUpload` quando falhar. Reabrir espera o envio iniciado ou cancela a inspeção ainda cancelável, conforme o comportamento atual.

Usar os eventos de detecção real para o reporter Epic e preservar opt-in de playtime Hydra. Wrapper sozinho não conta tempo. A sincronização automática Hydra legada/V2 não disputa arquivos com a sessão Epic gerenciada. Desconectar ou trocar conta invalida callbacks, publicação e sync da sessão capturada; uma sessão já em execução pode continuar, com observação mínima para manter o prefixo ocupado, sem enviar dados à nova conta. Finalizar processos de preparação e pré-requisito continua fazendo parte da invalidação.

## Recuperação e limites operacionais

| Situação                                     | Comportamento exigido                                                                   |
| -------------------------------------------- | --------------------------------------------------------------------------------------- |
| Reinício com download parcial                | Retomar pelo mesmo workspace e destino após revalidar posse/conexão.                    |
| Arquivos completos e preparação interrompida | Repetir apenas preparação, recibos e bind necessários.                                  |
| Mudança de Proton ou prefixo                 | Refazer contexto e pré-requisito pertinente antes de jogar; preservar arquivos e saves. |
| Pausa durante bootstrap ou instalador        | Abortar e aguardar processos da tentativa antes de liberar o slot.                      |
| Conta muda durante finalização               | Callback antigo não marca completo nem associa instalação.                              |
| Falha de rede no primeiro Proton             | Erro recuperável na interface atual; download do jogo permanece aproveitável.           |
| Offline com cache pronto                     | Mesmo runner/prefixo e política offline Epic existente; sem refresh obrigatório.        |
| Prefixo compartilhado ocupado                | Impedir mutação/restore com mensagem atual de operação/jogo em execução.                |
| Exclusão do jogo falha                       | Preservar registro instalado para retry; não apagar prefixo ou saves implicitamente.    |
| Encerramento não comprovado                  | Manter sync pendente; não fazer upload por saída do CLI ou wrapper.                     |

Na retomada automática do startup, falha de rede/runtime ou de revalidação da posse/conexão deve persistir `paused` com `epic.error` seguro e `queued=false`, publicar a atualização e liberar o slot apenas se nenhum processo estiver ativo. Arquivos/recibos são preservados e os controles atuais permitem retry. Não deixar um registro `active` sem processo porque o caller apenas registrou a exceção. Se a parada da árvore ainda não tiver sido comprovada, manter a operação ocupada e expor a falha de limpeza.

### Erros no fluxo existente

Acrescentar a `EpicInstallError` os códigos `compatibility-runtime-unavailable`, `compatibility-runtime-download-failed`, `compatibility-prefix-invalid`, `compatibility-prefix-busy` e `compatibility-launch-failed`. Mapear esses códigos no namespace `game_details`, no mesmo alerta/retry de download ou de lançamento. Falha de download de Proton não vira pedido de reconectar à Epic; prefixo ocupado usa a mensagem de jogo/operação em execução; falha de preparação indica tentar novamente ou abrir a seção de compatibilidade existente. `prerequisites-required` permanece para instalador/recibo incompleto, com texto neutro sobre concluir a preparação. Atualizar os idiomas e o reconhecimento de erros de launch; nenhum campo novo é obrigatório em persistência antiga.

Não matar todos os processos Wine do usuário. Interrupção usa grupo/descendentes e token da invocação; no UMU, o subprocesso do runtime pode ter nova sessão de processo. `kill(-pid)` do wrapper sozinho não garante limpeza. Revalidar identidade do PID antes de sinalizar e confirmar desaparecimento dos descendentes antes de concluir o cancelamento. Não limpar credenciais enquanto um comando Legendary ainda puder usá-las.

O runtime/Proton é compartilhado e não deve ser apagado ao remover um jogo, limpar downloads ou desconectar Epic. A limpeza de credenciais continua separada dos arquivos de instalação, prefixo, recibos e saves.

## Alterações por componente

| Componente                                                                  | Trabalho                                                                                  |
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `epic/installation.ts`, tipos Epic                                          | Política Linux/build, resolução de caminhos e suporte gerenciado separado de conexão.     |
| Serviço comum de compatibilidade Linux e `epic/linux-runtime.ts`            | Contexto, prefixo, geração, runner efetivo, locks e recibos.                              |
| `umu.ts`, `umu-launch-monitor.ts`, `umu-updater.ts`                         | Python privado, API supervisionada, versões em uso, estados e logging seguro.             |
| `native/epic-prerequisite-runner`, scripts de build, `electron-builder.yml` | Supervisor PE, build reproduzível e verificação no pacote Linux.                          |
| `epic/download.ts`, `epic/index.ts`                                         | Preparação Linux na finalização, callback de pré-requisito e cancelamento/invalidação.    |
| `launch-game.ts`, `epic/launch.ts`, `launch-protocol.ts`                    | Despacho UMU, cwd/argv/env, contexto estável e detecção desde o spawn.                    |
| Addon Rust, `native-addon.ts`, tracker Epic e watcher                       | Prova Linux de invocação/PE, identidade de PID e fechamento real.                         |
| `epic/cloud-saves.ts` e utilidades de perfil                                | Prefixo efetivo em todas as entradas; segurança de restore/upload e mudança de ambiente.  |
| Hook, modais desktop/Big Picture e traduções                                | Disponibilidade correta, fase neutra, mensagens contextuais e controles existentes.       |
| Eventos, preload e declarações tipadas                                      | Campo de suporte opcional, consulta de contexto por jogo e guards de compatibilidade OTA. |
| Snapshot UMU e seu gerador                                                  | Mapa Epic por artefato com origem/licença verificadas.                                    |

Os HTTP 400 de `/profile/games/artifacts?shop=epic` e `/download-sources/changes` são contratos separados da API Hydra. Eles não originam o bloqueio local e não são dependência do downloader gerenciado. Não expandir `GAME_SHOP` globalmente como parte desta entrega. Se aparecerem na validação, registrar e corrigir no escopo próprio, sem mascarar falha de download.

## Ordem de implementação

1. Fechar fixtures de protocolo do Legendary/UMU, contexto compartilhado, Python privado, resolução de runner/prefixo e política de suporte. Adicionar o supervisor e o mapa UMU com seus builds/contratos.
2. Integrar a preparação e os recibos ao download; validar retry, pausa, encerramento da árvore e finalização sem baixar a build novamente.
3. Integrar o payload Epic ao UMU, composição de ambiente, diagnóstico seguro e tracker Linux. Corrigir as entradas de saves para o prefixo efetivo.
4. Ajustar disponibilidade, mensagens e estados nas interfaces existentes; validar desktop e Big Picture com os mesmos serviços.
5. Gerar pacote Linux, executar a matriz real abaixo, corrigir regressões Windows e só então habilitar suporte no release.

Trabalho de runtime/pré-requisito e trabalho de tracker podem avançar em paralelo após os contratos. O guard só é liberado no produto quando o caminho integrado estiver pronto. Remover o guard, esconder o botão ou provar apenas um download não conclui esta implementação.

## Testes e critérios de aceite

### Testes automatizados obrigatórios

| Área              | Prova necessária                                                                                                                                                  |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Plataforma        | Linux x64/build Windows e Windows legado aceitos; Mac/arquitetura não homologada recusados com motivo correto.                                                    |
| Disponibilidade   | Mesmo resultado em desktop/Big Picture; main antigo/bridge ausente não anuncia suporte Linux; consulta de contexto exibe o prefixo efetivo sem criar runtime.     |
| Seleção Proton    | Jogo, global e automático; path removido; caminho efetivo fixo após bootstrap e sem troca no meio da sessão.                                                      |
| Python/pacote     | Imports e zipapp com runtime privado; ausência do Python de sistema; hashes e todos os arquivos no pacote.                                                        |
| Parser UMU        | Fixture 1.4.4 com path absoluto, output fragmentado/ANSI, path inválido, nenhum path e versão sem adaptador.                                                      |
| Prefixo           | Isolamento por conta/artefato, customizado preservado, geração recriada, prefixo ocupado e falha de preparação.                                                   |
| Pré-requisito     | EXE/MSI, argv/cwd corretos, nonce, recibo ausente/falso, códigos Windows completos 0/3010/1641 e falhas 194/105 reais.                                            |
| Retry             | Pré-requisito/Proton falha com bytes completos; retomar só finalização sem transferência duplicada; startup offline/conta alterada não deixa active sem processo. |
| Cancelamento      | Durante download, bootstrap e instalador; árvore com subprocesso em nova sessão; slot retido até parar.                                                           |
| Paths             | Espaços, Unicode, casing de metadata, colisão case-insensitive, symlink de escape e volume montado.                                                               |
| Auth/env          | Argumentos Epic preservados e OVT convertido para Wine com lifetime correto; cwd; campos reservados protegidos; nenhuma credencial em logs, status ou erro spawn. |
| Prova de processo | Wine loader, PE filho, reparentamento, overlay, PID reutilizado, prefixo alheio e inspeção parcial.                                                               |
| Fechamento        | CLI JSON termina, wrapper termina e jogo continua; nenhum dos dois dispara upload. Fechamento real dispara uma vez.                                               |
| Saves             | Prefixo padrão em getState/sync/conflito; nenhum fallback HOME; restore antes do jogo; upload/readback após saída.                                                |
| Contexto          | Desconexão/troca de conta durante cada etapa; callbacks antigos não escrevem na nova sessão.                                                                      |
| Regressões        | Windows, fila de outros provedores, usuários Linux existentes, instalação pelas duas lojas e comportamento OTA.                                                   |

Executar as suites Epic de instalação, download, manager/orquestrador, IPC, launch protocol, tracker, cloud saves e playtime; as suites UMU, prefixo, matching Linux, composição de launch options e estados de launcher; e testes nativos das projeções Linux e do supervisor. Acrescentar cenários funcionais aos testes existentes em vez de testar apenas helpers que espelham o código.

Para a implementação, seguir `.github/workflows/lint.yml` do launcher: `yarn test`, `yarn typecheck`, `yarn lint`, `yarn format-check` e `npx commitlint --last --verbose`. Como `yarn lint` faz auto-fix, revisar seu diff. Executar também `yarn check:ota-bridge`, o build nativo alterado e `yarn build:linux` no ambiente Linux. O pacote produzido deve conter Legendary/Python, UMU, supervisor e mapa com integridade verificada. Se o escopo separado da API for alterado e enviado, aplicar o checklist próprio de `hydra-api/AGENTS.md`.

### Aceite real no Linux

Usar conta e jogos de teste possuídos. Antes de operar nuvem, copiar os saves e conferir seus digests. A evidência deve identificar pacote/commit, distro, arquitetura, runtime/Proton efetivos, prefixo e resultados, sem tokens.

1. Em perfil limpo, sem Steam, Python sistêmico ou Proton previamente instalado, baixar um jogo pelo modal atual em pasta com espaços/volume montado. Confirmar provisionamento automático e arquivos/versão corretos.
2. Pausar durante transferência, fechar o Hydra, reabrir, retomar e comprovar reaproveitamento de arquivos. Repetir interrupção durante preparação e durante pré-requisito; retry não repete o download completo.
3. Executar instalador EXE e MSI de teste com os códigos completos de sucesso, reboot e falha, incluindo filhos e cancelamento. Para 3010/1641, o fixture agenda rename/delete e uma ação RunOnce; comprovar o efeito nos arquivos após o reboot do prefixo, além do recibo e bloqueio de conclusão prematura. Exit zero do wineboot ou desaparecimento da chave de operações pendentes, isoladamente, não prova esses efeitos.
4. Jogar online pelo botão atual. Confirmar autenticação Epic, Proton/prefixo escolhidos, estados de preparação, processo real detectado e funcionamento do jogo. Repetir com padrão global e seleção customizada por jogo, GameMode/MangoHud e argumentos com espaços.
5. Preparar um save remoto conhecido, restaurar antes de jogar, alterar progresso, fechar pelo jogo e comprovar upload somente após a saída real, com leitura de volta e digest. Repetir conflito e falha de rede no envio.
6. Jogar offline quando permitido, com cache completo; depois reconectar e confirmar recuperação do envio. Sem cache, obter erro recuperável sem danificar os arquivos.
7. Repetir download/início/erro/retomada no Big Picture e por controle, sem fluxo adicional. Validar ao menos desktop Linux e Steam Deck/gamescope, incluindo a janela de preparação existente.
8. Abrir jogo com launcher/filho/overlay e confirmar que saída do wrapper não encerra a sessão. Testar inspeção temporariamente indisponível e outro jogo no mesmo prefixo customizado.
9. Trocar/desconectar a conta com operações em andamento; confirmar ausência de publicação no owner errado e preservação de jogos/saves. Validar cancelamento sem processos órfãos da tentativa.
10. Executar o fluxo Windows de referência e comparar seleção, download, fila, pré-requisitos, Jogar, saves e erros. Fazer busca por credenciais sintéticas nos logs Hydra, UMU, Proton e saída de desenvolvimento.

Escolher um jogo simples, como Celeste se estiver na conta de teste, e outro que exija pré-requisito/autenticação mais representativos. Os fixtures de instalador complementam a amostra real. Um jogo sem pré-requisito ou sem cloud save não comprova as outras etapas.

A funcionalidade só está entregue quando downloads e runs reais, recuperação, isolamento de prefixo, pré-requisitos e fechamento/saves passarem na matriz pertinente. Sem essas provas, manter o suporte em validação e os casos pendentes explícitos; não declarar suporte completo com base apenas em testes unitários.

### Evidência disponível ao escrever o SDD

Na base auditada, 29 testes existentes de instalação Epic, launch protocol, tracker, monitor UMU, preparação de prefixo e matching Linux passaram, sem skips, usando `node --import ./scripts/register-ts-node.mjs --test --test-isolation=none` com as seis suites correspondentes. Isso confirma o comportamento atual desses contratos; não executa Proton nem comprova o suporte Linux proposto. Nenhuma alteração funcional foi feita para produzir esta especificação.

## Fontes primárias

- [Legendary 0.21.1 e release](https://github.com/legendary-gl/legendary/releases/tag/0.21.1): versão mantida no manifesto Hydra.
- [CLI Legendary no commit do release](https://github.com/legendary-gl/legendary/blob/42f6bdeadde3a9526dc8eb713763476999ac217b/legendary/cli.py): instalação, pós-instalação e `launch --json`.
- [Core Legendary no mesmo commit](https://github.com/legendary-gl/legendary/blob/42f6bdeadde3a9526dc8eb713763476999ac217b/legendary/core.py): ambiente, parâmetros e resolução de saves Wine.
- [Downloader Legendary](https://github.com/legendary-gl/legendary/blob/42f6bdeadde3a9526dc8eb713763476999ac217b/legendary/downloader/mp/manager.py): estado de retomada.
- [UMU 1.4.4](https://github.com/Open-Wine-Components/umu-launcher/blob/1.4.4/README.md): uso de Proton fora da Steam, runtime e seleção de ferramenta.
- [Execução UMU](https://github.com/Open-Wine-Components/umu-launcher/blob/1.4.4/umu/umu_run.py): `createprefix`, ambiente efetivo, cwd, subreaper e subprocesso.
- [Gerenciamento de Proton pelo UMU](https://github.com/Open-Wine-Components/umu-launcher/blob/1.4.4/umu/umu_proton.py): tokens, download e ferramentas gerenciadas.
- [Verbos e paths UMU](https://github.com/Open-Wine-Components/umu-launcher/blob/1.4.4/umu/umu_consts.py): verbos Proton e diretórios de runtime.
- [Proton 10](https://github.com/ValveSoftware/Proton/blob/proton_10.0/proton): logging, execução e conversão de paths. A implementação deve validar esses contratos no runner efetivamente escolhido, inclusive versões customizadas.
- [Banco oficial UMU](https://github.com/Open-Wine-Components/umu-database): correspondência por loja/codename, cobertura e licença do snapshot.
- [Manual Wine msiexec](https://github.com/wine-mirror/wine/blob/master/programs/msiexec/msiexec.man.in): instalação de pacotes MSI.
- [CreateProcessW](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw) e [GetExitCodeProcess](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess): criação explícita do executável e coleta do resultado Windows completo.
- [Linux wait](https://man7.org/linux/man-pages/man2/wait.2.html): limite de oito bits observado pelo status de saída POSIX.

Fontes locais decisivas: `src/shared/legendary-manifest.json`, `scripts/legendary-linux-runtime.cjs`, `resources/legendary/linux-python.sh`, `src/main/services/umu-release.ts`, os componentes relacionados na tabela de evidência e os workflows de lint/build. As decisões de orquestração, supervisor, recibos, interface e aceite deste SDD são o desenho proposto para integrar essas capacidades.

---

### Epic Linux Runtime Validation (`docs/epic-linux-runtime-validation.md`)

# Epic Linux runtime validation

The runtime smoke is independent of an Epic account. It executes the production
runtime, prefix, path mapping and supervised process modules, replacing only the
Electron application/database and Legendary account boundary with local ports.
It uses the pinned UMU zipapp, the private Python interpreter and a real Proton
installation. It does not replace UMU or Wine with test doubles.

From a Linux x64 checkout with the development runtimes and PE fixtures built:

```sh
node scripts/test-epic-linux-runtime.cjs \
  --python-runner /absolute/path/to/private/python-runner \
  --umu /absolute/path/to/umu-run \
  --data-dir /absolute/path/to/disposable-smoke-directory
```

Optional `--helper` and `--fixture` paths override the built development PE files.
Use Node 22.15 or newer (validated with 22.21.0). The smoke uses Node's normal
module loader with TypeScript transformation limited to real files inside this
checkout's `src` directory. Its Electron/database/account ports are objects,
not generated code. TypeScript and `string-argv` come only from this checkout's
installed dependencies; both are portable JavaScript, including when a Linux
Node executable runs the source from a Windows/NTFS mount. There is no dependency
directory or source-code override in the CLI. Other product builds still need
their platform's own native dependencies. The data directory contains only
synthetic smoke state and can be retained to inspect the runtime receipt. The
first execution downloads UMU-Proton and its Steam runtime into UMU's normal
per-user cache; later executions reuse that installation.

The smoke checks automatic UMU-Proton selection, an explicit Proton/custom prefix,
prefix generations, Unicode/spaces in paths, mapping a future output through its
existing parent's verified roundtrip, cached offline preparation, a real PE
launch, arguments, environment and working directory, empty private output buffers,
and absence of synthetic credentials in persisted runtime files. It also runs a
prerequisite returning the full Windows DWORD 3010 and the supervised system
reboot path. Cached offline mode does not promise that upstream UMU makes zero
metadata network attempts when executing the game.

The installation path is canonical and fixed in `STEAM_COMPAT_INSTALL_PATH`
through bootstrap, both path verbs, prerequisites and launch. Without this
context UMU infers different mounts from POSIX and Windows arguments: a mounted
volume can map to a drive such as `S:` that disappears before the reverse
lookup. The smoke also roundtrips its source fixture outside the prefix using
that fixture's captured installation context; this covers a Windows NTFS mount
when the checkout is accessed from WSL.

The Windows fixture is intentionally given synthetic authentication-like
arguments. Its temporary record is deleted before inspecting persisted files;
no Epic credentials are requested or written by this smoke.
The fixture does not emit credentials to stdout/stderr; the supervised-process
unit test separately emits synthetic secrets and proves they are discarded.

The implementation was exercised on Ubuntu in WSL2 with private Python 3.13.16,
UMU 1.4.4 and UMU-Proton-10.0-4. Separate Linux process tests exercise a detached
child after wrapper exit, natural tree completion, deadline cleanup after wrapper
success, incomplete process inspection, and spoof rejection for protected
systemd user infrastructure. These tests run in the normal user session.

A separate isolated run on rebased commit
`5a3734cfeaa24f5924e7cc1995787649596e4d83` used the real Linux native addon and
the production Epic tracker while a benign PE waited for ten seconds under UMU.
The native projection proved the invocation marker, canonical prefix, kernel
start ticks and canonical PE path; the tracker included that running fixture.
After natural PE and full process-tree exit, the tracker returned
`{ complete: true, paths: [] }`. The run exited zero. Its local evidence is
retained in `.cache/tracker-smoke-final.log`; only the explicit process identity
and path projection was logged, without raw argv or environment dumps.

Two upstream behaviors discovered by the real smoke are explicit implementation
decisions: UMU 1.4.4 treats `UMU_ZENITY=0` as truthy and skips the Python Proton
download, so the managed runtime clears that variable. Its `createprefix` command
intentionally exits 1 after initialization; Hydra instead runs the shipped
supervisor's benign `--probe` PE command and requires successful Windows execution
plus a valid prefix. Successful wrapper exit alone never permits a receipt:
attributed descendants must finish naturally within the tool deadline.

This smoke does not establish real Epic authentication, a commercial game
download, cloud account synchronization, DRM compatibility, or graphical game
rendering. Those require the separate authenticated product acceptance flow.

## Git and validation environment

The feature starts at the published #2906 head
`a4dfafb6621fc4e274158db59604dcdccc41a244` on
`codex/epic-linux-download-launch`. Its PR initially targets
`codex/epic-achievements-sync`; the original checkout's uncommitted changes were
not copied. Before publication, the feature was rebased onto the dependency's
updated published head `6d3a53f0a424dd01f27717b92f14ed836a7954d2`, preserving its
preview cancellation, shared caches and external-launcher checks. The branch was
subsequently rebased onto `719b1c363e9a3a02f182d9d9816ea7b18a1f4dd4`, preserving
the dependency's interrupted-save recovery and replacement-sync warnings. That
commit is now the Linux branch's dependency boundary; the sync-fix PR retains its
separate original boundary. The Linux validation copy uses an independent ext4
checkout of the same feature commit and its own dependencies and Rust targets.

The validation environment is Ubuntu WSL2 x86_64 with Node 22.21.0, Yarn 1.22.22,
Rust/MinGW, WSLg and Vulkan. Native addon loading is verified with Electron on
Linux and Windows. Wine supervisor acceptance separately exercises EXE/MSI,
full DWORD results, descendants, cancellation and real reboot effects
(rename/delete/RunOnce); it does not replace the UMU smoke above.

This host also produced nondeterministic V8 GC aborts in unchanged baseline
tests. Local checks use Node's `--single-threaded-gc` and bounded test concurrency;
the repository workflow and dependency versions remain unchanged. The full
checklist is `yarn test`, `yarn typecheck`, `yarn lint`, `yarn format-check`,
`npx commitlint --last --verbose`, `yarn check:ota-bridge`, native builds and
`yarn build:linux`. For the last command, a local Electron distribution of the
same version can avoid the configured download mirror:

```sh
yarn build:linux --config.electronDist=node_modules/electron/dist --publish never
```

The authenticated product cases in the SDD remain pending acceptance:
real Epic ownership/authentication and `requires_ot`, a real game download and
pause/restart/resume, prerequisite failure/retry, offline play, actual cloud
account restore/upload/conflicts and real graphical close on desktop, Big Picture
and Steam Deck/gamescope. WSLg/Vulkan and synthetic PE acceptance do not establish
Steam Deck acceptance. No merge is performed automatically.

Once #2906 is merged, reapply only this branch's feature commits, including when
the dependency was squash merged:

```sh
git fetch origin main
git rebase --onto origin/main 719b1c363e9a3a02f182d9d9816ea7b18a1f4dd4 codex/epic-linux-download-launch
git push --force-with-lease origin codex/epic-linux-download-launch
gh pr edit <linux-pr-number> --base main
```

Repeat affected checks before publishing that rebase. Never merge the feature PR
as part of this procedure.

---

### Global Leaderboards Page Sdd (`docs/global-leaderboards-page-sdd.md`)

# Global leaderboards page in the launcher (LBX-1265)

SDD from 2026-10-21. Status: approved, scope = the full design screen.

Builds on the game page leaderboard (LBX-1266), reusing its ranking row, medal and value formatter. The design has three tabs: **Ranking**, **By game** and **Your stats**. Backend: the leaderboard framework (LBX-1259) and the global weekly boards (LBX-1260).

## Expected result

A **Leaderboards** page in the launcher, reached from the sidebar, that implements the design screen exactly, including what goes beyond the task text: the three tabs, badges, rank change, player totals, the review preview and the streak indicators. Where the API does not provide the data yet, the dependency is listed below.

## Task acceptance criteria

| Linear criterion                                                                                                                                                              | How this SDD meets it                                                                                                                                                                                                                                                                                                                                                                                                                                             | Status                       |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| Sidebar/header entry to a Leaderboards page with tabs per metric (playtime, games, friends, achievements, souvenirs, reviews, review upvotes, sessions, streak, as available) | "Leaderboards" item with a trophy icon in `components/sidebar/routes.tsx`, title in `header.tsx` and the `/leaderboards` route in `main.tsx`. On the Ranking tab, the "Leaders of the week" cards are the metric tabs (`role="tab"`) and show each metric's current leader.                                                                                                                                                                                       | Met                          |
| Global/Friends toggle and This week/Last week toggle; a countdown to the weekly reset                                                                                         | Two `SegmentedControl`s in the header: Global/Friends and **Week/All time, as in the design** (decided; replaces Linear's "Last week" with "All time"). Subtitle "Week 43 · Oct 20–26 · Resets in 3d 14h", from `weekEnd` (fallback: next Monday 00:00 UTC), updated every 30 s. On All time the first place badge becomes "Record holder", the ▲▼ change disappears from rows and the subtitle explains the all-time board. Logged out users do not see Friends. | Met, with the decided change |
| Rows: rank, avatar (with frame if equipped), name (links to profile), value formatted per metric. The viewer's own row is pinned at the bottom when outside the top N         | Top 3 podium + `LeaderboardRow` from 4th place on (medal, avatar, name, bar, value, ▲▼ change). Clicking opens `/profile/:id`. `buildLeaderboardRows` pins the viewer's row with the "…" gap when they are outside the visible rows or the returned top. **Avatar frame: does not exist in the product yet (confirmed), out until it does.**                                                                                                                      | Met                          |
| Loading skeletons, empty board state, and error state (the API returns an empty board on timeout)                                                                             | Skeletons for the podium, rows, cards and every block of the other tabs. Empty state per window and scope (no friends, with "Find friends"). Error with "Try again". A timeout arrives as an empty board and falls into the empty state.                                                                                                                                                                                                                          | Met                          |
| Strings in `src/locales/en/translation.json`                                                                                                                                  | `leaderboards` namespace plus a `leaderboards` key in `sidebar` and `header`, in `en`, `pt-BR`, `ru` and `es`.                                                                                                                                                                                                                                                                                                                                                    | Met                          |

## Screen scope (everything is in)

### Ranking tab

- "Leaders of the week" cards: one per metric, with icon, label, leader avatar, name and value, on a single row; the selected one has a light border.
- Metric board: icon + title + "Top 13 of 12,480 players"; podium (heights 288/260/240, gold/silver/bronze metal band with shine, profile banner, avatar 64/52, "Leader of the week" badge on 1st, ▲▼ change on 2nd and 3rd, regular weight values); rows 4–13 with optional banner, cyan to violet bar relative to 1st and ▲▼ change; the viewer's row highlighted with the "You" tag and a full strength bar; "Show more".
- Reviews: "Most upvoted review" card above the podium (cover, game, avatar, blue star score, capitalized relative date, two line text, cyan like icon with votes, "View on the {game} page" on its own line); clicking a podium card switches the review; hovering a row shows the review tooltip.
- Streak: cyan to violet flame for an active streak, grey value for an ended streak.
- "Your badges": "5 of 14 earned", cards with cover, chip (TOP 1%, rank, days), title, subtitle, date or a cyan to violet progress bar ("4 h to go"), animated ring per tier; the streak badge uses the cyan to violet border and chip; "Show more".

### By game tab

- "Your games' rankings" with search: one card per game on a single row (cover, your rank "#6 of 12,480", change, leader).
- Selected game: hero with the game art, "{game} ranking", "{N} players · You are #6", "Open game page", metric chips, podium and rows as on the Ranking tab and "Show more" (no "View full ranking": the tab already is the full ranking, decided).

- Deep link from the game page (LBX-1266): `/leaderboards?tab=games&shop=&objectId=` opens this tab with that game selected; a game outside "Your games" still opens, with title and art from `getGameAssets`.

### Your stats tab

- "Your week": banner + avatar, and one row per metric with ▲▼ change next to your rank.
- "Daily streak": current and record, a 15 week heatmap (levels with increasing brightness from dim cyan to glowing violet, 0 h to 3 h+), day tooltip with hours and games; same height as "Your week".
- "Streaks among friends": list with presence and days, empty state "No friends to compare with" with "Find friends", "See all friends".
- "Your badges" (same block as on the Ranking tab).

## API dependencies

| Screen data                                                                     | Source                       | Status                                                                              |
| ------------------------------------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------- |
| Global playtime/games/friends/achievements/souvenirs boards, `window=week\|all` | `GET /leaderboards/:metric`  | LBX-1260                                                                            |
| Sessions and streak boards                                                      | LBX-1262, LBX-1263           | SDD ready, no code                                                                  |
| Reviews board + the leader's review (game, score, text, votes)                  | no task                      | **needs a backend task**                                                            |
| `backgroundImageUrl` on the user                                                | framework                    | decided in LBX-1260                                                                 |
| Total eligible players per board                                                | no field                     | **needs to be added to the framework** (`total` in the response)                    |
| ▲▼ change (rank in the previous week)                                           | no field                     | **needs to be added to the framework** (`previousRank` per entry and on the viewer) |
| Active/ended streak per entry                                                   | LBX-1263                     | to confirm in the contract                                                          |
| "Your games" list with your rank, total and leader                              | no endpoint                  | **needs an endpoint** (on top of the LBX-1264 per-game boards)                      |
| Per-game board with metrics                                                     | LBX-1264                     | SDD ready                                                                           |
| 15 week daily heatmap and streak record                                         | LBX-1262/LBX-1263            | to confirm in the contract                                                          |
| Friends' streaks with presence                                                  | LBX-1263 + existing presence | to confirm                                                                          |
| Badges (earned, progress)                                                       | no task                      | **needs a backend task**                                                            |

Each block switches to the real API as its dependency ships.

## Proposed contracts

| Endpoint                                                           | Response                                                                                                                                                                                                                                                                                                     |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET /leaderboards/metrics?scope=global\|game`                     | `{ metrics: [{ id, valueType: "duration"\|"count"\|"days", label }] }`, in display order; `label` in English                                                                                                                                                                                                 |
| `GET /leaderboards/:metric?window=week\|all&scope&take`            | framework response + `total` (eligible players), `previousRank` per entry and on `viewer`, `streakActive` (streak only) and `review` `{ gameTitle, shop, objectId, coverImageUrl, score, createdAt, text }` (reviews only)                                                                                   |
| `GET /leaderboards/me/games?window&scope`                          | `{ games: [{ shop, objectId, title, coverImageUrl, heroImageUrl, rank, previousRank, total, leader }] }`                                                                                                                                                                                                     |
| `GET /games/:shop/:objectId/leaderboards/:metric?range&scope&take` | LBX-1264 route + `previousRank` per entry                                                                                                                                                                                                                                                                    |
| `GET /users/me/play-activity?weeks=15`                             | `{ currentStreak, bestStreak, playedToday, days: [{ date, seconds, games }] }`                                                                                                                                                                                                                               |
| `GET /users/me/friends/streaks`                                    | `{ friends: [{ user, days, bestDays, active, online }] }` (includes the viewer)                                                                                                                                                                                                                              |
| `GET /users/me/leaderboard-badges`                                 | `{ earned, total, badges: [{ id, kind, tier, metric, scope, gameTitle, coverImageUrl, value, week, currentRank, earnedAt, progress }] }`; `kind` = `top_percent\|weekly_leader\|top_rank\|friends_podium\|streak_record`, `tier` = `gold\|silver\|bronze\|streak`. Texts are built in the launcher with i18n |

## Decisions

1. **Scope = the design screen**, including what is not in the task text (decided).
2. **Avatar frame** stays out because it does not exist in the product yet (decided).
3. **Tabs = leader cards** on the Ranking tab. The page fetches every metric's board when it opens (in parallel, with the renderer cache and request dedupe, plus the backend Redis cache).
4. **Metrics come from the backend**: `GET /leaderboards/metrics?scope=global|game` returns the active metrics in display order, each with `id`, `valueType` (`duration`, `count` or `days`) and an English `label`. The launcher hardcodes no metric list: cards, chips and "Your week" follow that response, so a new metric (Sessions, for example) shows up without a launcher release. The name comes from the translations by `id` (`metric_{id}`) in every launcher language, falling back to the backend `label` until a translation exists; values are formatted by `valueType` in the app language; icons are mapped by `id` with a generic fallback. Streak flames and the review preview show when a board carries `streakActive` or `review` data, not by metric id.
5. **Shared code**: the page imports from `pages/game-details/leaderboard/` (types, `buildLeaderboardRows`, `LeaderboardRow`, `SegmentedControl`, styles). Moving them to `components/leaderboard/` is a refactor after LBX-1266 merges.
6. **Pagination**: the API returns up to 100 entries; the screen shows 13 and "Show more" adds 10 without a new request.
7. **Order and eligibility** come from the backend (profile photo required on global, subscriber tie-break; LBX-1259 addendum A). The launcher does not reorder.
8. **Period = Week / All time**, as in the design. The launcher sends `window=week|all`.
9. **Gamification colour**: the cyan to violet gradient (`#22c7ee` → `#b05cff`) marks progress and streaks: flames, heatmap, streak badge, badge progress bars, ranking bars, and the like and star icons on reviews. Medals, ▲▼ colours and the leader trophy keep their own colours.
10. **`GLOBAL_LEADERBOARDS` feature flag** (decided after review): while it is off, the sidebar item and the game page "View full ranking" link are hidden and `/leaderboards` redirects home without fetching. The API turns it on once the endpoints listed above ship.

## Performance and backend behaviour

- One renderer response cache (`createResponseCache`, 60 s, deduping identical in-flight requests). Switching to an already visited tab, period or scope does not refetch nor flash a skeleton, because the screen reads the cache synchronously.
- The countdown lives in `WeekSubtitle`; its 30 s tick does not re-render the page.
- When the week resets with the page open, the cache is cleared and every block reloads, retrying with backoff (30 s up to 5 min) while the API still returns the old week. The tabs remount only when the week start day changes.
- Heatmap days and "today" use UTC, matching the backend weeks (Monday 00:00 UTC), and "today" moves at UTC midnight while the page stays open.
- If `/leaderboards/metrics?scope=game` returns no metric, the By game board shows an empty state instead of an error.
- The game page "View full ranking" link shows only to signed-in users, since the By game tab needs a session.
- A missing `previousRank` hides the rank change instead of showing "no change".
- A metric is hidden only when `/leaderboards/metrics` does not list it. If a listed metric's board fails, its card stays visible with a load error and the board shows the error with "Try again", which requests it again. The game page leaderboard reads through the same response cache, so one `clear()` at the weekly reset covers every board; stale entries are deleted on read and the cache keeps at most 200 responses.
- Animations: the podium shine and the earned badge ring (`RankFrameRing`) use only `transform` (GPU).

## States

| State         | When                                   | What shows                                                                        |
| ------------- | -------------------------------------- | --------------------------------------------------------------------------------- |
| loading       | first load of a block                  | block skeleton                                                                    |
| ready         | there is data                          | content                                                                           |
| empty         | empty list (includes timeout)          | icon + text for the window                                                        |
| empty_friends | Friends with nobody besides the viewer | text + "Find friends"                                                             |
| error         | network/parse failure                  | message + "Try again"                                                             |
| logged out    | no session                             | Global only, no pinned row, no By game, Your stats or badges, with a sign-in hint |

## Changes per file

| File                                                                        | Change                                                                                              |
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `pages/leaderboards/leaderboards.tsx`                                       | page, header, toggles, page tabs                                                                    |
| `pages/leaderboards/week-subtitle.tsx`                                      | week label, countdown and week reset                                                                |
| `pages/leaderboards/ranking-tab.tsx`                                        | leader cards, board, selected review                                                                |
| `pages/leaderboards/by-game-tab.tsx`                                        | games list, hero, per-game board                                                                    |
| `pages/leaderboards/stats-tab.tsx`                                          | your week, daily streak + heatmap, streaks among friends                                            |
| `pages/leaderboards/board-view.tsx`                                         | podium + rows + show more shared by the tabs                                                        |
| `pages/leaderboards/leaderboard-podium.tsx`                                 | top 3 podium                                                                                        |
| `pages/leaderboards/leaderboard-bits.tsx`                                   | rank change, streak flame, block states                                                             |
| `pages/leaderboards/badges-section.tsx`                                     | your badges                                                                                         |
| `pages/leaderboards/metric-icon.tsx`                                        | metric icons                                                                                        |
| `pages/leaderboards/global-leaderboard.ts` (+ `.test.ts`)                   | metrics, request, normalizers, states, ISO week, countdown, change, heatmap, badges                 |
| `pages/leaderboards/leaderboards-api.ts`                                    | API access, viewer, cached resource hook                                                            |
| `helpers/response-cache.ts` (+ `.test.ts`)                                  | shared response cache: per account keys, TTL, dedupe, generation on clear, stale eviction, size cap |
| `services/leaderboards-cache.ts`                                            | the single cache instance used by the Leaderboards page and the game page board                     |
| `hooks/use-global-leaderboards-feature.ts`                                  | `GLOBAL_LEADERBOARDS` flag for the sidebar, the route and the game page link                        |
| `pages/leaderboards/use-global-leaderboards.ts`                             | global boards for every metric                                                                      |
| `pages/leaderboards/use-global-leaderboard-value.ts`                        | value formatting per metric                                                                         |
| `pages/leaderboards/leaderboards.scss`                                      | BEM styles                                                                                          |
| `pages/game-details/leaderboard/leaderboard-controls.tsx`                   | exports `SegmentedControl`                                                                          |
| `pages/game-details/leaderboard/leaderboard-row.tsx`, `leaderboard.scss`    | optional rank addon, value prefix, muted value and tooltip slots                                    |
| `main.tsx`, `components/sidebar/routes.tsx`, `components/header/header.tsx` | route, sidebar item with the design trophy icon, title                                              |
| `src/locales/{en,pt-BR,es,ru}/translation.json`                             | strings                                                                                             |

## Tests and acceptance

- `global-leaderboard.test.ts` (`node:test`, same pattern as `game-leaderboard.test.ts`): request, normalizers, metric definitions, rows below the podium, states, rank change, viewer ranking, ISO week, countdown, heatmap and badge progress. `response-cache.test.ts` covers keys, TTL, dedupe, uncached errors, clear, account isolation and housekeeping.
- Goal: 80% or more of the new logic lines. Components are validated in the launcher.
- Manual test in the launcher: the three tabs, toggles, countdown, podium, review, streak, badges, "Show more", pinned row, profile link, logged out.
- Once the backend ships: validate each block against a local `hydra-api`.

---

### Leaderboard Top Position Styling Sdd (`docs/leaderboard-top-position-styling-sdd.md`)

# Top position styling for leaderboards (LBX-1267)

SDD written on October 21, 2026, revised on October 22, 2026 after review. Status: implemented, in review in PR #2959.

Base: launcher `be492f5e3`, branch `feat/LBX-1267-leaderboard-top-position-styling`. Visual references: the "Game Ranking Card" prototype (boards "Game page sidebar · Ranking", "View all · Full board" and "Ranking states") and the launcher Design System.

## Expected result

Every leaderboard in the launcher highlights the top positions the same way. The first three get gold, silver and bronze medals, each with a ribbon, a rim and a shine that sweeps across it from time to time. Their rows get an animated ring in the medal color. Ranks 4 to 10 show a stronger number than the rest. The styling works with custom themes and respects the system's reduced motion setting.

This change ships the reusable building blocks. The first screen to use them is the game page board (LBX-1266). The global leaderboards page (LBX-1265) uses the same components. Gamification is not a dependency, following Hélio's decision recorded on the task.

## Task acceptance criteria

| Linear criterion                                                                                          | How it is met                                                                                                                                                                                                                                                                                                                      | Status                  |
| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| Distinct gold/silver/bronze treatment (color, rank icon, subtle glow) for ranks 1 to 3                    | Each of the three ranks has its own color and an SVG medal with the number. The subtle glow is a `drop-shadow` in the medal color.                                                                                                                                                                                                 | Met                     |
| Lighter accent for the top 10                                                                             | Ranks 4 to 10 show the number at full text color and weight 800. Other ranks use 55% of the text color and weight 700. There is no medal.                                                                                                                                                                                          | Met                     |
| Applied consistently on the global page, the game page board and the friends views                        | One set of components (`RankMedal`, `RankFrameRing`, `rank-frame`) for every view. The game page board and the friends view come with LBX-1266, and the global page with LBX-1265.                                                                                                                                                 | Met once LBX-1265 lands |
| Works with existing themes (theme CSS variables where possible, readable on light and dark custom themes) | The theme system defines no color variables besides `--color-primary`. All ranking colors are custom properties (`--rank-*`) with fallbacks that theme CSS can override, and the BEM class names are stable. Plain numbers follow the row text color. The number on the medal is dark on a light disc, readable on any background. | Met                     |
| Built inside the Leaderboards project, not dependent on Gamification                                      | No Gamification imports or data.                                                                                                                                                                                                                                                                                                   | Met                     |

## Decisions

| Topic          | Decision                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Components     | `RankMedal` and `RankFrameRing` in `src/renderer/src/components/rank-medal/`, exported from `components/index.ts`. `RankMedal` takes `rank` and an optional `highlighted`.                                                                                                                                                                                                                                                                                                                                                                   |
| Tier rule      | Pure `getRankTier(rank)`: 1 → `gold`, 2 → `silver`, 3 → `bronze`, 4 to `TOP_RANK_ACCENT_CUTOFF` (10) → `top10`, anything else or invalid → `default`.                                                                                                                                                                                                                                                                                                                                                                                        |
| Colors         | Prototype values as SCSS variables in `globals.scss` (`$rank-gold-color` and so on). In the compiled CSS they are fallbacks of custom properties (`--rank-gold-color`, `--rank-gold-ribbon` and so on) that themes can override.                                                                                                                                                                                                                                                                                                             |
| Medal          | Inline 24×26 SVG from the prototype. The rank number is an SVG `<text>` centered on the disc.                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Glow           | `filter: drop-shadow(0 0 4px …)` in the medal color at about 45% opacity. This is the "subtle glow" the task asks for. It is static.                                                                                                                                                                                                                                                                                                                                                                                                         |
| Shine          | The prototype's diagonal streak sweeps every `$medal-shine-duration` (3.6 s), staggered by rank (`MEDAL_SHINE_STAGGER_SECONDS`). It animates `transform: translateX()` only, so it stays on the compositor and never triggers layout.                                                                                                                                                                                                                                                                                                        |
| Top three ring | The row gets `rank-frame rank-frame--gold/silver/bronze` and renders `RankFrameRing`. The ring container holds a static soft tint in the medal color and is masked down to a 1.5 px border. Inside it, a larger element carrying a static `conic-gradient` (medal color with a white highlight) rotates with `transform: rotate()` every `$frame-spin-duration` (4.5 s). Silver and bronze start offset by a third and two thirds of a turn. Only `transform` is animated, so there is no per-frame style recalculation or gradient repaint. |
| Reduced motion | `prefers-reduced-motion: reduce` stops the shine and the ring spin. The glow and the static ring stay.                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Accessibility  | The medal has `role="img"` and a translated label ("1st place, gold"). Color is never the only signal, because the number stays visible.                                                                                                                                                                                                                                                                                                                                                                                                     |
| Strings        | New `leaderboard` namespace in `en` and `pt-BR`. Other languages fall back to English, which is the configured fallback.                                                                                                                                                                                                                                                                                                                                                                                                                     |
| Backend        | No changes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |

## Out of scope

- Profile banners on rows, which belong to LBX-1266.
- The "Hydra paw" animation on the Play button. It is in the prototype, but it is not part of the leaderboards.
- A "you" marker on the medal itself when the viewer is in the top three. The row's "You" tag already covers it.

## Changes by file

| File                                                                    | Change                                                                        |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `src/renderer/src/components/rank-medal/rank-tier.ts`                   | Tier rule, cutoff and types.                                                  |
| `src/renderer/src/components/rank-medal/rank-medal.tsx`                 | `RankMedal` and `RankFrameRing`.                                              |
| `src/renderer/src/components/rank-medal/rank-medal.scss`                | Medal, shine, ring, animations and reduced motion.                            |
| `src/renderer/src/components/index.ts`                                  | Exports.                                                                      |
| `src/renderer/src/scss/globals.scss`                                    | Six medal color variables.                                                    |
| `src/locales/en/translation.json`, `src/locales/pt-BR/translation.json` | `leaderboard` namespace with `medal_gold`, `medal_silver` and `medal_bronze`. |

## Tests

Automated (`node:test`, in `rank-tier.test.ts`):

- 1, 2 and 3 map to gold, silver and bronze.
- 4 and 10 map to `top10`, and 11 maps to `default`.
- 0, negatives, decimals and `NaN` map to `default`.
- `isMedalTier` only matches the three podium tiers.

Coverage of `rank-tier.ts` is 100%. The visual components are checked by hand, since the repository has no rendering tests.

Manual checks:

- The medals and ring match the prototype on the default theme.
- With the `Light`, `White`, `Dark` and `All Black` themes, and with a simulated theme that lightens the card and overrides the gold color, every number stays readable.
- In the running page, the only animated property is `transform`.
- With reduced motion turned on, the shine and the ring spin stop.

---

### Steam Epic Achievement Union (`docs/steam-epic-achievement-union.md`)

# Combined Steam/Epic achievement presentation

The optional combined request has a five-second timeout. Source-only update
events from other copies are ignored on fallback; only the operational copy may
replace that page's source-specific list. Library totals and unlocked counts are
both derived from the returned list, and rejected requests retain the existing
count pair.

Requires the API's `/profile/achievements/combined` endpoint and its additive
canonical achievement identity migration. The API groups equivalent provider
definitions and applies the existing owner/session/subscription visibility rules.

The launcher displays the union of the owner's variants, preferring Steam only
when the equivalent achievement is unlocked there. An Epic-only unlock stays
Epic. Distinct achievements stay separate; original localized names are kept.
Both provider timestamps remain in `sources`.

Unsynced local unlocks are matched only within their original store and game.
The original selected-copy catalogue, remote-unlock and souvenir refresh always
runs first, including when the combined response succeeds. This keeps source
caches current for subsequent detection/sync without importing the union into them.
The combined list is a read-only presentation projection: it never writes into
`AchievementMemoryStore`, uploads one store's unlocks to another, or changes
Steam/Epic synchronization. API absence/failure falls back to the previous
single-store flow. Hidden locked descriptions remain hidden and list keys use the
canonical achievement UUID. Grouped game pages listen to updates from both stores;
large library cards use the union's counts when available. Global API statistics,
feeds and cross-profile comparison remain unchanged.

Deploy the API migration/endpoint first. Real-account staging validation must
check X+Y, duplicate X+X, Epic-only unlocks, local unsynced progress and offline
fallback before shipping the launcher.

---

--- METRICS ---
- Files Extracted: 8
- Estimated Token Budget: ~27084 tokens
- Canonical Reference: https://codewiki.google/github.com/hydralauncher/hydra
