Skip to content

chore(events): merge 4.x e remove tenancy do módulo de eventos - #482

Merged
gvieira18 merged 52 commits into
feat/eventsfrom
chore/merge-4x-into-feat-events
Aug 15, 2026
Merged

chore(events): merge 4.x e remove tenancy do módulo de eventos#482
gvieira18 merged 52 commits into
feat/eventsfrom
chore/merge-4x-into-feat-events

Conversation

@danielhe4rt

Copy link
Copy Markdown
Contributor

Contexto

A PR #309 (feat/events4.x) estava com 23 arquivos em conflito. A causa
raiz não é textual: a 4.x recebeu o feat(tenancy)!: remove multi-tenancy
(#413)
depois que a branch de eventos saiu, e o módulo events ainda era todo
escopado por tenant.

Esta PR mergeia a 4.x dentro de feat/events, resolve todos os conflitos e
adapta o módulo de eventos ao mundo sem tenancy. O push direto na feat/events
é bloqueado por ruleset, por isso vem como sub-PR.

Alterações

Conflitos resolvidos (23 arquivos)

Arquivo Resolução
composer.json, package.json, theme.css, shards.json versões da 4.x; mantido bacon/bacon-qr-code do módulo de eventos
composer.lock, package-lock.json regenerados a partir da 4.x
PanelAdminServiceProvider, PanelAppServiceProvider os dois lados (registros de eventos + DiscordCluster/FilamentAsset)
AppPanelProvider páginas de eventos mantidas; import de Tenant removido
EventAgendaFactory, EventSubmissionFactory, SponsorFactory, GenerateDiscordTenant, TenantUser mantida a remoção
TimelineFeedQueryTest, ThreadPageTest, ProfilePageTest, CommunityRetrospectiveTest, Feed, timeline/header versão da 4.x (já contém autor removido + avatar)
LinkTwitchChannelCommand versão da 4.x (#415)
EventFactory versão da branch de eventos, de-tenantizada

De-tenancy do módulo events

  • events.tenant_id sai da migration; idx_events_tenant_slugidx_events_slug
    (único em slug), idx_events_tenant_windowidx_events_window.
  • Event perde a relação tenant(), o @property e o campo em $fillable.
  • CheckInRequested perde tenantId; HandleBotCheckIn deixa de filtrar
    evento e ExternalIdentity por tenant.
  • Painéis admin/app deixam de escopar eventos por tenant (EventForm,
    EventInfolist, EventsTable, EventsList, EventDetail, MyEventsList,
    EventPage) — inclusive a coluna/label tenant das traduções.
  • EventsSeeder e EventFactory sem tenant.
  • Suíte de testes ajustada; testes de isolamento por tenant que perderam sentido
    foram removidos, e o de 404 virou "evento em rascunho não é visível".

Fora do escopo de eventos

Plano de Testes

  • vendor/bin/pest --parallel1108 passed / 0 failed
  • vendor/bin/phpstan analyse — 0 erros
  • vendor/bin/pint --test --parallel — passou
  • vendor/bin/rector process --dry-run — 0 mudanças
  • php artisan migrate:fresh em banco limpo
  • tests/.pest/shards.json regenerado com --update-shards

Issues Relacionadas

Desbloqueia #309.

BrunaDomingues and others added 30 commits July 7, 2026 23:16
…hi) (#388)

## Summary

- Adiciona selects dinâmicos de **país**, **estado** e **cidade** na
página de perfil (`ProfilePage`), com busca assíncrona e dependência
entre os campos
- Países e estados usam **Sushi** com cache Laravel sob demanda (sync
lazy via jobs); cidades ficam em **Postgres** (`geo_cities`) e são
buscadas na API apenas quando necessário
- Integração com a [World API](https://world.bmbc.cloud/api)
(`WorldApiClient` + `config/geo.php`), sem seedar o mundo inteiro no
banco
- Normaliza colunas de endereço existentes: ISO3 → ISO2, UF brasileira →
nome completo do estado, `state` expandido para 100 chars
- Tooltip na cidade orientando o usuário a pesquisar caso a cidade não
apareça na listagem

## Arquitetura

| Camada | País / Estado | Cidade |
|--------|---------------|--------|
| Modelo | `GeoCountry`, `GeoState` (Sushi) | `GeoCity`
(Eloquent/Postgres) |
| Cache | `GeoSushiStore` (Laravel cache) | tabela `geo_cities` |
| Sync | `SyncWorldCountriesJob`, `SyncWorldStatesJob` |
`SyncWorldCitiesJob` |
| UI | `GeoSelect` (helpers para Filament selects) | busca local + API
com `GeoSearch` (ignora acentos) |

## Migrations

- `2026_07_07_180000_create_geo_tables.php` — cria `geo_cities`
- `2026_07_07_180001_normalize_address_location_columns.php` — normaliza
dados legados em `addresses`

## Config

```env
GEO_WORLD_API_URL=https://world.bmbc.cloud/api

Test plan
 

- [ ] Abrir perfil e verificar preload de países

 - [ ] Selecionar país → estados carregam; selecionar estado → cidades do cache aparecem
 - [ ] Buscar cidade com/sem acento (ex.: sao → São Paulo)
 - [ ] Salvar perfil e confirmar persistência em addresses (country ISO2, state nome, city nome)
 - [ ] Rodar php artisan test tests/Feature/Geo tests/Feature/AddressTest.php app-modules/panel-app/tests/Feature/ProfilePageTest.php

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: danielhe4rt <danielhe4rt@gmail.com>
Co-authored-by: BrunaDomingues <bruna@users.noreply.github.com>
## Problema

O feed da timeline retorna **500** com `Attempt to read property "name"
on null` em
`panel-app/resources/views/components/timeline/header.blade.php` sempre
que o autor de um post resolve para `null`. Como o feed renderiza todos
os posts, **um único post órfão derruba a página inteira** para todos.

## Causa raiz — merge de contas

Quando duas contas são mergeadas, `MergeAccountsAction` reatribui
`ExternalIdentity` e tenants para a conta sobrevivente e faz **hard
delete** da conta perdedora — mas **nunca reatribui
`activity_timeline.user_id`**. Não existe FK nessa coluna, então os
posts da conta mergeada ficam órfãos e o feed quebra.

```
 MergeAccountsAction (identity)                 activity module
   reatribui ExternalIdentity + tenants
   event(AccountsMerged(survivor, merged)) ──►  ReassignTimelineOwnership (listener)
   currentUser->delete()                         UPDATE activity_timeline
                                                 SET user_id = survivor
                                                 WHERE user_id = merged
```

## Fix — 3 camadas (defense-in-depth)

1. **Merge (causa raiz):** novo evento `AccountsMerged` disparado dentro
da transação do merge; listener em `activity` reatribui as linhas da
timeline para o sobrevivente. **Preserva os posts** e previne órfãos
futuros. Feito via evento para não criar dependência circular entre
módulos (`activity` já depende de `identity`). `activity_reactions` não
tem `user_id`, então a timeline é a única tabela a reatribuir.
2. **Filtro do feed:** `TimelineFeed` usa `whereHas('user')` para
blindar o feed contra os **órfãos que já existem em prod** (o mapeamento
id-antigo→novo se perdeu no hard delete e não dá pra reatribuir
automaticamente).
3. **View guard:** `header`/`post-show` toleram autor null e mostram
"Usuário removido" em vez de quebrar.

## Testes

22/22 verdes (3 novos + os existentes de timeline/merge):
- `MergeAccountsActionTest` — merge reatribui os posts ao sobrevivente
(exercita evento→listener ponta a ponta).
- `TimelineFeedQueryTest` — feed exclui posts de autor inexistente.
- `ThreadPageTest` — `PostShow` renderiza sem quebrar quando o autor foi
deletado.

## Observação

Os órfãos **já existentes** em produção continuam no banco — o filtro só
os esconde do feed. Uma limpeza/reatribuição retroativa precisaria de
uma fonte pro mapeamento (logs / `ExternalIdentity`) e fica fora deste
PR.
## Summary

- Corrige visibilidade do círculo de iniciais no dark mode (header e
preview card do perfil)
- Evita que o hover de upload da capa fique por cima do avatar
- Melhora visibilidade do avatar no menu lateral no dark mode
- Adiciona borda discreta na sidebar, no mesmo peso das bordas dos
inputs do form


**Antes:** no dark mode, o círculo das iniciais quase sumia no perfil; o
hover de upload da capa cobria parte do avatar; o avatar do menu lateral
ficava sem contorno visível; e não havia separação clara entre sidebar e
conteúdo.
<img width="1906" height="907" alt="Captura de tela 2026-07-08 131101"
src="https://github.com/user-attachments/assets/bfa3d0d3-0975-4d3d-b1d6-f02cc1ff067e"
/>

**Depois:** iniciais com borda/ring visível no header e no preview card;
avatar sempre acima do hover da capa; contorno discreto no avatar do
menu lateral; e borda suave na sidebar, no mesmo peso das bordas dos
inputs do form.

<img width="1906" height="911" alt="Captura de tela 2026-07-08 130813"
src="https://github.com/user-attachments/assets/5faa3fc0-7819-4aa6-9d91-552868f72c5f"
/>

## Commits

1. `fix(profile): keep initials avatar visible in dark mode preview
card`
2. `fix(profile): keep initials avatar visible in dark mode header`
3. `fix(profile): keep avatar above cover upload hover overlay`
4. `fix(panel): keep sidebar user avatar visible in dark mode`
5. `style(panel): add subtle sidebar border` *(fácil de reverter
isoladamente se necessário)*

## Test plan

- [ ] Abrir `/app/{tenant}/profile` em light e dark mode
- [ ] Sem foto: conferir se o círculo com iniciais aparece no header e
no preview card
- [ ] Passar o mouse na capa: hover não deve cobrir o avatar
- [ ] No menu lateral: avatar do usuário visível no dark mode
- [ ] Conferir se a borda da sidebar está discreta (similar aos inputs
do form)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
> 📄 <strong><a
href="https://waifuvault.moe/f/b2e0aef7-16c7-4c95-9dcf-7e51626f7ddb.html"
target="_blank" rel="noopener noreferrer">Visual overview of this
PR</a></strong> — one-screen interactive summary of every change.

> [!NOTE]
> **Why `tests/.pest/shards.json` is committed — it's intentional.**
Pest v4.6.0 balances the suite across shards using recorded per-test
timings. Committing this baseline (instead of regenerating it every run)
makes the split **deterministic and evenly balanced from the first CI
run** — no warm-up pass and no cross-run artifact upload to wire up.
It's refreshed automatically as the suite grows. See [Time-based
sharding in Pest
v4.6.0](https://laravel-news.com/time-based-sharding-in-pest-v4-6-0).

## Summary
- **Guidelines** — sync AI guidelines to he4rt conventions
(namespace/naming, issue-tracker repo `he4rt/heartdevs.com`, added
typed-JSON-cast + enum-contract guidance); drop 3 stale `docs/agents/*`
mirrors.
- **Modular** — fix `make:module` stubs + `config/app-modules.php` so a
module's ServiceProvider scaffolds at `src/` root (not `Providers/`);
migrate `squads` and `onboarding` to match.
- **Testing DX** — add tracked `.env.testing.example`, untrack the
per-dev `.env.testing`, bootstrap it via `post-root-package-install`;
use `LazilyRefreshDatabase` for feature tests.
- **CI** — run the Pest suite in 2 balanced shards (committed timings
baseline); gate the pipeline on canonical branch prefixes (`feature/**`,
`bugfix/**`, `chore/**`, `story/**`, `*.x`).
- **Deps** — pin intra-repo `he4rt/*` modules to `^1.0.0`; bump
dependencies.
- **Refactor** — apply the Rector named-argument convention across
models, commands, jobs and Livewire components.
- **Cleanup** — remove 24 redundant `.gitkeep` placeholders.

## Test plan
- [ ] `composer check` — Rector, Pint, PHPStan all clean
- [ ] `php artisan test` — 881 pass (539 feature + 342 unit, 2515
assertions)
- [ ] Fresh clone: `make setup` creates `.env.testing`; `make
setup-test-db` migrates it
- [ ] Scaffolding a new module places its ServiceProvider at `src/` root
Arquivo-fonte referenciado por config/app-modules.php para os
placeholders .gitkeep que o internachi/modular copia para cada novo
módulo (tests e database). O #402 removeu o arquivo, o que quebrava o
comando make:module ao tentar copiar um stub inexistente.
Escrito pelo 7k galera
…nadas (#400)

Closes #394

## Summary
No repeater de skills do perfil, uma skill já escolhida em outra linha
continuava aparecendo no dropdown das demais linhas, podendo ser
selecionada de novo. Agora ela é removida dos resultados da busca — cada
skill só pode ser escolhida uma vez.

## What Changed
- `Skill::search` ganhou o parâmetro `exclude`, que filtra os IDs direto
na query (`whereNotIn`, antes do `limit`).
- `ProfilePage::skillIdsInSiblingRows` coleta os IDs já selecionados nas
outras linhas do repeater (via `$get`) e os passa ao `Skill::search` no
`getSearchResultsUsing` do campo `skill_id`.

## Tests
- Novo `SkillSearchTest` cobrindo o `exclude` do `Skill::search`.

---------

Co-authored-by: Elves Santos <168841287+sirelves@users.noreply.github.com>
Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
## Summary
- Add a `make libdave` target that symlinks the libdave (DAVE E2EE)
install into the `.cache/libdave` probe path, so the Discord bot boots
without the `failed to initialize voice class /
LibDaveNotFoundException` error (DAVE is mandatory for voice since
2026-03-01).
- Symlinking the probe path avoids relying on `DISCORDPHP_DAVE_LIBRARY`,
which the Forge daemon (Supervisor ignores shell rc) and `config:cache`
(skips `.env`) both defeat.
- gitignore `/.cache` (machine-local, like `/vendor`).

Deploy: run `make libdave` after `composer install` in the Forge deploy
script.
## Decisões de arquitetura

  ### Renderer do mapa: `chartjs-chart-geo` (MIT), não Highcharts
O mockup inicial usava Highcharts, mas ele exige **licença comercial
paga** para
uso não-pessoal. Trocamos pelo `chartjs-chart-geo` (MIT, grátis), que
reusa o
Chart.js que os dashboards já carregam. Resultado visual equivalente,
sem custo
  de licença.

  ### Geometria: GeoJSON do IBGE versionado (não uma API em runtime)
Fronteira de estado é **dado estático de referência** — como uma fonte
ou um
  SVG. Por isso versionamos um arquivo (padrão que o módulo já usa em
`resources/fonts/`) em vez de buscar de uma API a cada request (que
adicionaria
dependência de rede/CORS numa página core, sem ganho, já que a geometria
não
  muda). Origem: **IBGE** (dado aberto, oficial), simplificado offline
(topojson-simplify + arredondamento) de ~251 KB para ~83 KB, com as
propriedades
reescritas para `{ name, uf }`. Não usamos a malha da
Highsoft/Highcharts
(proprietária). Os anéis foram reorientados para **CW** para casar com a
  convenção esférica do d3-geo (senão o mapa preenche a tela inteira).

  ### Nomes e contagens: domínio, não o arquivo do mapa
- **Nomes canônicos dos 27 estados** vêm de `GeoLocation` (World API) —
a mesma
fonte que o perfil usa —, cacheados via Redis (`Cache::remember`, 30
dias),
    padrão idêntico ao `App\Geo\Models\GeoCountry`.
  - **Contagens** vêm da tabela `addresses` (domínio identity).
- A reconciliação entre o valor salvo em `addresses.state` e o mapa é
feita por
**nome normalizado, sem acento** (mesma normalização no PHP e no JS).
Medimos:
    os nomes do World API batem 27/27 com os do GeoJSON.
- O GeoJSON é usado **só para desenhar** — se apagado, só o mapa quebra;
os
    números continuam corretos.

  ### Widgets nativos do Filament, não grid manual
  A divisão da tela usa a grade nativa de widgets do Filament
(`getHeaderWidgetsColumns()` + `$columnSpan`), não um grid Tailwind na
Blade.
Motivo: as classes de grid custom dependiam de rebuild do CSS (davam
purge e
quebravam o layout); a grade do Filament emite CSS inline via custom
properties,
  imune a isso.

  ### Retornos tipados com DTOs, não array shapes
As queries retornam **value objects** (`readonly`) em vez de
`array{...}`. Segue
o guideline (`prefer DTOs/VOs over associative arrays`), dá autocomplete
e mata a
chave-string mágica. Os valores derivados (`webTrend()`,
`discordTrend()`,
`coverage()`) ficam **dentro** do `CommunityActivitySnapshot`, tirando
lógica de
apresentação do widget. (`BrazilStatesGeometry` segue retornando array —
é o
  GeoJSON cru consumido pelo `@js`, onde array é o formato natural.)

  ### Definição das métricas
  - **Ativos na web**: `user_id` distintos com **post ou reply** em
    `activity_timeline` nos últimos 30 dias.
- **Ativos no Discord**: `external_identity_id` distintos com mensagem
em
    `messages` nos últimos 30 dias.
- **vs período anterior**: rolling comparison — janela atual (últimos
30d) contra
a janela imediatamente anterior (30–60d atrás), mesmo padrão do Discord
Dashboard.
- **Membros com localização** / **Estados alcançados**: derivados de
`addresses`.

  ## Test plan

  - [ ] `php artisan test app-modules/panel-admin/tests` — verde (Pest)
  - [ ] Pint, Rector (dry) e PHPStan (level 7) sem erros
- [ ] `/admin/{tenant}/location` renderiza: 4 stat cards, mapa por
estado e top-5
- [ ] Mapa: hover mostra tooltip `Estado (UF)` + nº e %, siglas
centralizadas,
        tooltip não corta nas bordas (mobile)
- [ ] Layout responsivo: cards empilham no mobile, dividem 3/2 no
desktop
  
  


https://github.com/user-attachments/assets/cc3440c8-fff4-467d-945c-f5030cc389f1

---------

Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
## Contexto

Esta PR **remove por completo o multi-tenancy** da plataforma. O modelo
de tenants (guilds do Discord / orgs de evento) adicionava complexidade
de escopo em praticamente todos os módulos sem entregar valor real para
o produto atual, que opera como uma única comunidade.

## O que muda

- **`feat(tenancy)!` — BREAKING**: dropa colunas `tenant_id`, colapsa
índices únicos que dependiam do tenant e remove as tabelas de tenant.
- Remove o model `Tenant` e limpa `tenant_id` dos domain models.
- Tira a tenancy dos painéis Filament e as rotas de tenant.
- Desacopla o fluxo de OAuth e o bot do Discord do tenant.
- Passa a ler credenciais do bot/Twitch da **config** em vez do tenant.
- Remove scaffolding morto (`GenerateDiscordTenant`,
`GuestTenantIdentifier`, etc).
- Ajusta suíte de testes: remove `TenantFactory`, bootstrap de tenant e
`tenant_id` das factories.

## Migrations

- `2026_07_07_000000_drop_multi_tenancy` — drop de
colunas/índices/tabelas.
- `2026_05_01_200459_delete_orphaned_tenant_records` e
`2026_03_21_192332_deduplicate_external_identities` — limpeza de dados
órfãos antes do drop.

## Impacto

- **239 arquivos**, `+822 / -2808` (remoção líquida de ~2k linhas).
- Suíte verde localmente (805/805) com a migration aplicada.

## ⚠️ Atenção para review / deploy

- Migration **destrutiva e irreversível** — exige backup antes de rodar
em produção.
- Credenciais do bot/Twitch agora vêm de `config`
(`config/services.php`, `config/he4rt.php`) — conferir env vars no
ambiente antes do deploy.

## Checklist

- [x] Testes passando localmente
- [x] Migration aplicada local
- [ ] Revisão da migration destrutiva
- [ ] Backup de produção antes do merge/deploy
## Resumo

Refina o cluster Discord do painel admin, que estava no estado cru do
scaffold do CLI. Todos os 5 resources viram **somente leitura** (List +
View — os dados são espelho do Discord sincronizado pelo bot/ETL;
edições seriam sobrescritas no próximo sync), ganham tabelas calibradas
com os payloads reais de produção, i18n en/pt_BR e um dashboard com
widgets.

Plano completo (com as decisões tomadas):
`app-modules/panel-admin/docs/plans/2026-07-12-refinamento-cluster-discord.md`.

## Destaques

- **Event logs (237k linhas)**: índice novo em `created_at`, badges por
categoria de evento, filtros de tipo/período, payload JSON pretty na
view com link cruzado pro membro. Mantém cursor pagination de propósito
(evita `COUNT(*)`).
- **Channels**: agrupados por categoria espelhando a sidebar do Discord
(categorias viram títulos de grupo), tipo como badge via contratos
Filament no enum `DiscordChannelType`.
- **Members (26k)**: ativos por padrão (filtro ternário em `left_at`),
avatares via CDN com fallback derivado do snowflake, busca global
corrigida para username/global_name/nickname.
- **Roles**: cor decimal → swatch hex, ordenação pela hierarquia do
Discord (`position desc`).
- **Guild**: relation managers de Channels/Roles/Members com links
cruzados.
- **Dashboard**: stats (membros ativos, entradas/saídas 7d, eventos 24h,
boosters, canais), eventos por dia e crescimento de membros (fonte
`discord_members`, sobrevive a prune futuro de event logs).

## Fix notável

O painel configura `PaginationMode::Cursor` **globalmente**; cursor
pagination quebra com `ORDER BY` por expressão (grouping por
relacionamento e sorts por agregado) — 500 real reproduzido em dev com
dados de prod. Channels/Members/Roles fazem override para length-aware,
com teste de regressão de >1 página em tabela agrupada.

## Como foi feito

Workflow multi-agente: plano por Fable, implementação paralela por
Sonnet (fundações → resources → drill-down → testes), revisão
adversarial final por Fable (4 issues encontradas e corrigidas, incl.
`formatStateUsing` morto em `CodeEntry` → `jsonFlags()` nativo).

## Testes

- Suite completa: **876 passed (2512 assertions)** com `--parallel
--update-shards`.
- 14 smoke tests novos em
`app-modules/panel-admin/tests/Feature/Discord/`.
- Pint ✅ · PHPStan 0 erros ✅ · migration aplicada e índice confirmado ✅
#415)

## Contexto

Ao dar setup da Twitch em produção (foco: EventSub pra detectar lives),
o fluxo de conectar/registrar esbarrou em bugs latentes do módulo
`integration-twitch` — o módulo nunca tinha sido exercido com env
incompleta. Esta branch adiciona um botão de conexão em um clique,
corrige os crashes e remove a necessidade do `TWITCH_BROADCASTER_ID`.

## Commits

### `feat(panel-admin)` — botão "Connect Twitch" (1 clique)
Header action na página **Twitch → Subscriptions** que inicia o OAuth
link flow apontando pra `route('oauth.redirect', panel: admin, provider:
twitch)`. Como o admin já está logado, vincula a Twitch ao usuário atual
(intent `Link`) com o scope set de admin. Rótulo dinâmico: **Connect** /
**Reconnect (login)**.

### `fix(integration-twitch)` — hardening contra env incompleta
Três crashes pré-existentes:
- **`TypeError` no redirect** — `TwitchHelixConnector` buscava o app
token no construtor; só montar a URL de redirect já forçava o fetch.
Agora o token é resolvido **lazy** no `defaultAuth()`.
- **`getToken()` retornava `null`** — closure tipada `: string`. Passa a
lançar `OAuthFlowException` (capturada pelo `OAuthController`) + protege
`expires_in`.
- **`config()->string()` estourava em `null`** —
`TWITCH_EVENTSUB_CALLBACK` unset. Defaults `''` no config + leitura
null-safe.

### `feat(integration-twitch)` — broadcaster vem do OAuth do admin
Remove a dependência de `TWITCH_BROADCASTER_ID`. O broadcaster passa a
ser a conta Twitch conectada ao admin autenticado (via
`ExternalIdentity`), eliminando a divergência entre o que o modal exibia
(ExternalIdentity) e o que de fato registrava (config).
- `RegisterTwitchSubscriptionsAction` recebe o broadcaster id como
argumento; a presentation resolve o "quem".
- `RegisterSubscriptionsAction` resolve a Twitch do admin logado e
bloqueia com aviso quando não há nenhuma.
- `twitch:link-channel` passa a imprimir o comando `twitch:subscribe` em
vez do env removido.

## Testes
Suíte Twitch verde (integration-twitch + panel-admin). Testes de
regressão de UI do modal `RegisterSubscriptions` com o broadcaster do
usuário autenticado ficaram de fora por ora (issue de resolução de
componente Livewire em teste) — a lógica de domínio está coberta em
`RegisterTwitchSubscriptionsActionTest`.

## Setup de produção pendente (fora do código)
- `TWITCH_OAUTH_CLIENT_SECRET` válido (a Twitch retornou `403 invalid
client secret` em dev).
- Registrar o redirect `https://<dominio>/auth/oauth/twitch` no dev
console da Twitch.
- Conectar a Twitch pelo novo botão → `twitch:subscribe <id>
--type=stream.online` (o id agora vem do OAuth).
## Summary
- Esconde da retrospectiva os repos sem PR no recorte (só
review/issue/comentário): o card some, mas as contribuições seguem
contando em `meta`/`people`/`highlights`.
- Remove o empty-state morto "Sem PRs nesse recorte…" do card de repo.
- Panorama passa a mostrar "em N repositório(s)" (repos com PR = cards
exibidos) no lugar de "N arquivos tocados"; plural via `@choice`.

## Test plan
- [x] `php artisan test --compact --filter=CommunityRetrospective` (30
passed)
- [x] Abrir `/comunidade/retrospectiva` num recorte com repo só de
review/issue/comentário (ex.: `gvieira18/sycorax`) → card sumiu, stats
gerais iguais
- [x] Panorama: contagem de repositórios bate com o nº de cards e
pluraliza (1 → "repositório", N → "repositórios")
## Contexto

O `drop_multi_tenancy` (#413) colapsou corretamente os índices
**únicos**
tenant-scoped, mas dropou ~17 índices **secundários** (de performance)
sob o
comentário _"no collapsed replacement needed"_ — sem nunca recriar o
equivalente sem `tenant_id`. Resultado: as páginas de **Marketing** do
painel
admin rodam 7+ agregações por load como *sequential scans* sobre as
maiores
tabelas do banco (`messages`, `voice_messages`, `activity_timeline`).

Dois achados extras da investigação:

1. **Bug no drop**: `idx_github_contributions_type_time (type,
occurred_at)`
nunca teve `tenant_id` (a integração GitHub nunca teve tenancy), mas foi
   dropado mesmo assim — o comentário na migration o rotula errado como
   índice de tenant.
2. **Colunas quentes nunca indexadas**: `voice_messages (occurred_at,
state)`
e `addresses (state)` nunca tiveram índice, mas os dashboards de
marketing
   filtram por elas em todo load.

## O que muda

Quatro migrations puramente aditivas (uma por módulo dono da tabela):

| Módulo | Tabela | Índice novo |
|---|---|---|
| activity | messages | `(sent_at)`, `(channel_id, sent_at)`, `(kind)` |
| activity | voice_messages | parcial `(occurred_at) WHERE state =
'joined'` |
| activity | activity_timeline | `(created_at)`, `(parent_id,
is_ignored, created_at)` |
| activity | activity_reactions | `(emoji_key, created_at)` |
| activity | message_mentions | `(mentioned_identity_id)`,
`(mentioned_provider_account_id)` |
| activity | message_embeds | `(source_domain)`, `(kind)` |
| activity | membership_events | `(kind, occurred_at)` |
| activity | moderation_events | `(type, occurred_at)` |
| activity | interactions | `(occurred_at)` |
| moderation | moderation_cases | `(status, priority, created_at)` |
| moderation | moderation_rules | `(is_active)` |
| moderation | moderation_audit_log | `(created_at)` |
| integration-github | github_contributions | `(type, occurred_at)` —
restaura o drop indevido |
| (app root) | addresses | `(addressable_type, state)` |

Não recriados: `twitch_subscriptions_tenant_id_index` (só tenant, nada
sobra)
e o composto de `external_identities` (já coberto pelo standalone
`(model_type, model_id)`).

## Verificação

- `php artisan migrate` local: 19 índices criados, confirmados em
  `pg_indexes`; re-run é no-op (`IF NOT EXISTS`).
- `EXPLAIN` em `github_contributions` (única tabela local com volume):
  `Index Only Scan using idx_github_contributions_type_time`.
- Pint, PHPStan e Rector limpos na raiz; suíte `--filter=Marketing`
verde
  (5 testes, 35 assertions).
## Contexto

Este PR implementa a solução da issue #430.

Atualmente, a seção **Skills** da página de Perfil (`/profile`) não
disponibiliza frameworks e ferramentas de automação de testes entre as
opções cadastráveis. Isso dificulta que profissionais de Quality
Assurance (QA) representem corretamente suas competências técnicas na
plataforma.

Para resolver esse problema, este PR adiciona ao seed de skills as
principais ferramentas utilizadas pelo mercado de QA, permitindo que
elas passem a ser selecionáveis no perfil dos usuários.

## Alterações

Adiciona novas skills relacionadas à área de Quality Assurance,
incluindo ferramentas de:

- **Testes Web (E2E/UI)**
  - Selenium
  - Cypress
  - Playwright
  - WebdriverIO
  - TestCafe
  - Puppeteer

- **Testes Mobile**
  - Appium
  - Espresso
  - XCUITest
  - Detox

- **Testes de API**
  - Rest Assured
  - Postman
  - Newman
  - Karate
  - Supertest

- **BDD**
  - Cucumber
  - SpecFlow
  - Behave
  - Robot Framework

- **Testes Unitários e de Integração**
  - JUnit
  - TestNG
  - PyTest
  - Jest
  - Mocha
  - Chai
  - NUnit
  - xUnit

- **Testes de Performance**
  - JMeter
  - K6
  - Gatling
  - Locust

## Motivação

- Permite que profissionais de QA representem melhor seu stack técnico.
- Amplia o catálogo de skills disponíveis na plataforma.
- Facilita a busca por profissionais com conhecimentos específicos em
ferramentas de testes.
- Mantém o catálogo de skills centralizado e versionado por meio dos
seeds, facilitando futuras atualizações.

## Test Plan

- Executar os seeders da aplicação.
- Verificar que as novas skills foram inseridas corretamente no banco de
dados.
- Confirmar que as ferramentas aparecem como opções na seção **Skills**
da página de Perfil (`/profile`).
- Validar que o seeder pode ser executado novamente sem criar registros
duplicados (quando aplicável).
## Contexto

Melhora a interação com quem entra na comunidade. A mensagem pública no
canal `auto-report` **continua igual** — a mudança toca só a **DM
enviada ao novo membro**, que antes era uma cópia byte-a-byte do post do
canal.

> Sugestão da @kalanedev

## O que muda

DM com a marca He4rt e um **CTA único** pra ativar o `/apresentar`:

- Embed roxo He4rt (`#782bf1`), thumbnail = ícone do servidor, footer `©
He4rt Developers` + timestamp.
- Descrição de acolhimento **personalizada com o nome** do membro + o
que é a He4rt.
- `field` dedicado **🙋 Comece se apresentando** com o passo-a-passo do
`/apresentar`.
- 3 botões: **✍️ Me apresentar** (deep-link pro canal de apresentações)
· **🌐 Portal** · **🔗 Nossas redes**.

## Resiliência (DM fechada)

Muitos usuários bloqueiam DM de membros do servidor → a promise do
`sendTo()` **rejeita**. Agora tratamos com `->catch()`:

- Loga `warning` no canal de log `bot-discord`.
- Posta um **follow-up público no `#geral`** com o mesmo embed + botões
e uma menção que **notifica** a pessoa (via `body()`, acima do embed).
- Se `HE4RT_GERAL_CHANNEL_ID` não estiver setado, degrada
silenciosamente pra só-logar.

## Detalhes técnicos

- Corrige a semântica `content()` (dentro do embed) × `body()` (texto
acima, onde menção pinga) da API do Laracord.
- Extrai `WelcomeMember::guildIconUrl()` (puro) pra montar a URL do
ícone do servidor no CDN do Discord (trata `a_…` → `.gif`), com teste
unitário.

## ⚠️ Config necessária

Adicionar ao `.env` de produção:

\`\`\`
HE4RT_GERAL_CHANNEL_ID=<id do canal #geral>
\`\`\`

## Testes

- `WelcomeMemberGuildIconTest` (4 casos: png, gif animado, sem ícone,
sem guild) — verdes.
- Pint + PHPStan passam nos arquivos tocados.
- Camada de transporte (envio Discord) não é testada — segue a convenção
do módulo (o `IntroductionCommandTest` também só testa o lado de
domínio).

## Como testar manualmente

1. Setar `HE4RT_GERAL_CHANNEL_ID`.
2. Entrar no servidor com DM aberta → recebe a DM rica.
3. Entrar com DM fechada → recebe o follow-up no `#geral` com menção.
O login com qualquer OAuth (Discord/GitHub/Twitch) não guardava a
informação de login porque a variável remember não era passada no
controller. Assim a sessão expirava com base no SESSION_LIFETIME — não
sei qual o valor em prod, mas no .env.example está 120, ou seja, 2 horas
após o login — e o usuário era deslogado, com o painel pedindo para
reconectar o Discord.

Resolvi isso passando o remember como true por padrão no OAuth

  - Auth::login($result->user);
  + Auth::login($result->user, remember: true);

Testei localmente deletar a tabela de sessões
(php artisan tinker --execute="DB::table('sessions')->delete();") para
reproduzir as informações do perfil desaparecendo, mas não consegui
reproduzir essa parte. Ainda assim, o remember por padrão deve resolver
esse comportamento.

Closes #409

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
#435)

## Contexto

Este PR implementa a solução da issue #428 .

Atualmente, o upload de avatar e capa na página de Perfil
(`/app/profile`) não oferece nenhuma opção de recorte antes do
salvamento. Como consequência, imagens com proporções diferentes das
esperadas podem ser exibidas de forma distorcida, esticada ou com cortes
indesejados.

Para melhorar a experiência do usuário e garantir consistência visual
entre os perfis, o fluxo de upload foi atualizado para permitir o
recorte e redimensionamento das imagens antes do envio.

## Alterações

- Substitui o componente de upload do avatar pelo `FileUpload` do
Filament.
- Adiciona suporte ao editor de imagens para permitir o recorte (crop)
antes do upload.
- Configura proporções específicas para cada tipo de imagem:
  - Avatar com proporção quadrada.
  - Capa/banner com proporção widescreen.
- Configura redimensionamento automático das imagens para as dimensões
esperadas pela aplicação.
- Exibe o avatar do usuário no feed quando houver uma imagem de perfil
cadastrada.
- Adiciona traduções ausentes para os novos textos e componentes
utilizados, garantindo suporte aos idiomas disponíveis na plataforma.

## O que mudou 🧐

### Removido

- Propriedades públicas `$avatarUpload` e `$coverUpload` com
`#[Validate]`
- Método privado `saveMedia()` (chamado dentro de `save()`)
- Chamada `$this->saveMedia()` no fluxo de salvamento do perfil
- Lógica de `temporaryUrl()` nos computed `avatarPreviewUrl` /
`coverPreviewUrl`
- Reset das propriedades (`$this->avatarUpload = null`) nos métodos
`removeAvatar` / `removeCover`
- Import de `Livewire\Attributes\Validate`

### Adicionado

- `editAvatarAction(): Action` — abre modal com `FileUpload` configurado
com editor circular (1:1), `storeFiles(false)`, e persiste imediatamente
via `replaceMedia()`
- `editCoverAction(): Action` — abre modal com `FileUpload` configurado
com editor de imagem (3:1), redimensionamento automático para 1800×600,
e persiste imediatamente via `replaceMedia()`
- `replaceMedia(string $collection, TemporaryUploadedFile $file): void`
— método unificado que limpa a coleção e adiciona a nova mídia com
fallback de extensão (`getClientOriginalExtension() ?: guessExtension()
?: 'jpg'`)
- Import de `Filament\Forms\Components\FileUpload`

## Motivações

- Melhora a experiência de upload de imagens.
- Garante maior consistência visual entre os perfis.
- Evita imagens achatadas, esticadas ou cortadas de forma inesperada.
- Permite ao usuário visualizar e ajustar o enquadramento da imagem
antes de salvá-la.
- Torna o feed mais personalizado ao exibir o avatar dos usuários.

## ANTES
<img width="1280" height="579" alt="antes"
src="https://github.com/user-attachments/assets/001f550e-beb4-4e6a-9fe6-c8cf1435f4e4"
/>


## DEPOIS
<img width="1280" height="579" alt="chrome-capture-2026-07-18"
src="https://github.com/user-attachments/assets/26f8e3b1-81cc-47fd-94bd-0490641b88cd"
/>
## Summary
- Configure the Vite `build` block: terser JS minification, CSS
minification, and per-package `manualChunks` (via `rolldownOptions`,
since this repo runs Vite 8 + rolldown) for better long-term vendor
caching.
- Add `terser` as an explicit build dependency required by the terser
minifier.
- Bump ambient JS and PHP dependencies that were pending in the working
tree.

## Test plan
- [ ] `npm run build` completes without error and does not emit `terser
not found`
- [ ] Buildable output lands in `public/build/` and main screens still
render
#445)

Closes #436 

## feat(portal): Adicionar botão de redirecionamento para loja da He4rt

Adiciona um botão na navbar do portal que redireciona para a loja da
He4rt (`https://loja.heartdevs.com/he4rt/`), tanto na versão desktop
quanto mobile.

### Mudanças

**Desktop (navbar principal):** Ícone de sacola de compras
(`heroicon-s-shopping-bag`) com `aria-label="Loja"`, exibido apenas como
ícone ao lado dos ícones de redes sociais e Discord.

**Mobile (menu hamburguer):** Botão com ícone + texto "Compre na Loja",
dentro da lista de links do menu mobile.

## Preview
Desktop:
- Novo ícone de sacola entre "Redes sociais" e "Área do Usuário"
<img width="1063" height="91" alt="Screenshot 2026-07-22 at 23 00 46"
src="https://github.com/user-attachments/assets/7c44555a-fdc8-4542-a4fa-069f32385f25"
/>

Mobile:
- Novo item "Compre na Loja" entre "Redes sociais" e "Área do Usuário"
<img width="333" height="268" alt="image"
src="https://github.com/user-attachments/assets/2a5f7989-3548-4d3b-a6c4-e51e14d9488f"
/>

Redes:
- Novo item "Loja" no final da página
<img width="539" height="626" alt="image"
src="https://github.com/user-attachments/assets/1e8f8972-b49a-46b2-8273-421564e93a20"
/>
Update Filament (5.7.3), Laravel framework (13.21.1), Sanctum,
Telescope, medialibrary, phiki, and npm dev deps (lint-staged,
prettier).
## Summary
- Bump Filament and media-library plugin to `^5.7.3`
- Bump Laravel framework to `^13.21.1`, Sanctum `^4.3.3`, Telescope
`^5.21.0`
- Bump `spatie/laravel-medialibrary` `^11.23.3` and `phiki/phiki`
`^2.2.1`
- Bump npm dev deps: `lint-staged` `17.2.0`, `prettier` `3.9.6`
- Lockfiles (`composer.lock`, `package-lock.json`) updated to match
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0
to 7.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>skip running unsafe pr check if input is default by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2518">actions/checkout#2518</a></li>
<li>trim only ascii whitespace for branch by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2521">actions/checkout#2521</a></li>
<li>escape values passed to --unset by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2530">actions/checkout#2530</a></li>
<li>Various dependency updates</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v7...v7.0.1">https://github.com/actions/checkout/compare/v7...v7.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.1</h2>
<ul>
<li>Skip running unsafe pr check if input is default by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2518">actions/checkout#2518</a></li>
<li>Trim only ascii whitespace for branch by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2521">actions/checkout#2521</a></li>
<li>Escape values passed to --unset by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2530">actions/checkout#2530</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/3d3c42e5aac5ba805825da76410c181273ba90b1"><code>3d3c42e</code></a>
prep v7.0.1 release (<a
href="https://redirect.github.com/actions/checkout/issues/2531">#2531</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/28802689a136bfcdb721715abd713740beecbe07"><code>2880268</code></a>
escape values passed to --unset (<a
href="https://redirect.github.com/actions/checkout/issues/2530">#2530</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/12cd2235efa0937479335606d7c3ac9f6c0973b1"><code>12cd223</code></a>
trim only ascii whitespace for branch (<a
href="https://redirect.github.com/actions/checkout/issues/2521">#2521</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/62661c4e71a304b2823ed026347b8d34c3eac541"><code>62661c4</code></a>
skip running unsafe pr check if input is default (<a
href="https://redirect.github.com/actions/checkout/issues/2518">#2518</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/e8d4307400f9427dba7cb98e488d6ab85f1cec5f"><code>e8d4307</code></a>
Bump the minor-actions-dependencies group with 2 updates (<a
href="https://redirect.github.com/actions/checkout/issues/2499">#2499</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/631c942040754b6e095e929c1677c07e10ed4f87"><code>631c942</code></a>
eslint 9 (<a
href="https://redirect.github.com/actions/checkout/issues/2474">#2474</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/4f1f4aec02e41874fa0262ea8ff5172d7978ad1e"><code>4f1f4ae</code></a>
Bump actions/upload-artifact from 4 to 7 (<a
href="https://redirect.github.com/actions/checkout/issues/2476">#2476</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/ba097532fb203f7e88c9c3c0b899b49469908a92"><code>ba09753</code></a>
Bump actions/checkout from 6 to 7 (<a
href="https://redirect.github.com/actions/checkout/issues/2488">#2488</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/b9e0990d219a03df7633c93f6f005a8fecbcab22"><code>b9e0990</code></a>
Bump docker/login-action from 3.3.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2479">#2479</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/e8cb398be4a550817e382abf69e4c12c76fce1f2"><code>e8cb398</code></a>
Bump docker/build-push-action from 6.5.0 to 7.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2478">#2478</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=7.0.0&new-version=7.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
## Summary
- Add `/sala-empresarial empresa:<slug>`, converting a `/sala`-tracked
voice room into a private company room by stamping Discord permission
overwrites — `deny @everyone` and `allow` the selected Empresa
Parceira's Partner Role for `connect`, `speak`, `use_vad`,
`send_messages` and `read_message_history` (voice + the room's built-in
text chat). `VIEW_CHANNEL` stays untouched (visible-but-locked) and
`MENTION_EVERYONE` keeps category inheritance.
- Put the decision (guards + role resolution + overwrite plan) in a
pure, unit-tested `ConfigureEmpresarialRoomAction`; the command edge
only performs the `setPermissions` I/O.
- Gate by Partner Role membership, **not** room ownership — any member
of the selected company can privatize the room (rejects
`not-in-tracked-room` / `unknown-company` / `missing-partner-role`, all
ephemeral).
- Add a flat `bot-discord.roles.partners` registry (`slug => role id`),
seeded with `brd` and `3p`; onboarding a new partner is one env + one
config line.
- Ship the module glossary (CONTEXT) entries and ADR-0001 documenting
the design.

## Notes
- Deviates slightly from the original #448 permission list:
`send_messages` was added after a live server test showed outsiders
could still post in the room's text chat.
- Stateless by design — only `/sala`-tracked rooms are eligible and
`DynamicVoiceTask` auto-deletes them when empty, so overwrites die with
the channel. No revert command.

## Test plan
- [ ] Member holding the partner role, inside their `/sala` room →
`/sala-empresarial empresa:brd` privatizes it; outsiders can't connect,
speak, or post in chat, but the room stays visible.
- [ ] Same room, run by a teammate who didn't create it → still succeeds
(role-gated, not owner-gated).
- [ ] Pick a company whose role you don't hold → ephemeral rejection, no
overwrites stamped.
- [ ] Run outside any `/sala` room (permanent channel or no voice) →
ephemeral rejection.
- [ ] Re-run on an already-configured room → harmless re-stamp, no
error.

Closes #448

## Screenshots

_Ephemeral replies, captured on a test guild (`/sala-empresarial`)._

**Company picker — choices built from the `partners` registry (`BRD`,
`3P`):**
<img width="418" height="244" alt="Screenshot from 2026-07-24 09-30-16"
src="https://github.com/user-attachments/assets/68ff734a-18f6-45f4-ad93-ef3cfc540371"
/>

**Success — room privatized for BRD:**
<img width="418" height="187" alt="Screenshot from 2026-07-24 09-42-33"
src="https://github.com/user-attachments/assets/e98f104c-6a12-4e4a-9d40-b8e02dc5e550"
/>

**Rejected — caller doesn't hold the selected company's Partner Role
(`missing-partner-role`):**
<img width="418" height="187" alt="Screenshot from 2026-07-24 09-30-39"
src="https://github.com/user-attachments/assets/ecdca69f-b90c-4ef6-8e26-79ff33cb483a"
/>

**Rejected — caller isn't inside a `/sala`-tracked room
(`not-in-tracked-room`):**
<img width="444" height="201" alt="Screenshot from 2026-07-24 09-29-14"
src="https://github.com/user-attachments/assets/74397524-73f7-4289-bd3f-f1abfe9863ee"
/>
## Summary
- Resolve GitHub avatars in the portal hero section via the stable
numeric `external_account_id` (`avatars.githubusercontent.com/u/{id}`)
instead of parsing the `metadata` username/name, which could be stale or
renamed.
- Filter to identities with a numeric `external_account_id` present,
dropping the ad-hoc metadata handle extraction.
- Add `HeroAvatarsTest` feature coverage for the active-user +
GitHub-identity avatar resolution.

## Coverage (GitHub identities, non-deleted)
| Strategy | Resolvable avatars |
| --- | --- |
| By user id (`external_account_id`, new) | **2643** |
| By username (`metadata.username\|name`, old) | 2643 |
| Resolvable by user id **only** | 0 |
| Resolvable by username **only** | 0 |

Coverage is identical (all 2643 identities carry both), so no users lose
their avatar. The change is about **stability**: numeric user-id URLs
survive GitHub username renames, whereas the old username-based URLs
break on rename.

## Test plan
- [ ] `php artisan test --compact --filter=HeroAvatars`
- [ ] Visit the portal homepage and confirm hero avatars render for
active users

---------

Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
Closes #441

## Contexto

O modal de confirmação de unificação era renderizado dentro da coluna
lateral sticky. Isso criava um contexto de empilhamento próprio e fazia
os campos Apelido e Data de Nascimento aparecerem por cima do modal
quando o topo do perfil estava visível.

## O que mudou

O modal agora usa o teleport do Livewire para ser renderizado
diretamente no body. A lógica de confirmação e cancelamento continua no
mesmo componente, mas o modal deixa de ficar preso ao contexto de
camadas da coluna lateral.

## Testes

1. `php artisan view:cache`
2. `vendor/bin/pest
app-modules/panel-app/tests/Feature/ProfilePageTest.php --compact` com
16 testes e 92 assertions
3. `vendor/bin/pint --test --parallel`
4. `npm run build`

## Validação manual

Testei o modal no topo da página e depois de rolar 700 pixels. A camada
escura ficou acima de todo o perfil nos dois casos. O botão Cancelar
fechou o modal normalmente e o botão Unificar manteve o binding com
`confirmMerge`.
## Summary

No layout mobile, o menu de navegação está posicionado na parte inferior
da tela, o que reduz sua visibilidade e dificulta o acesso aos
principais recursos da plataforma.

Esta alteração reposiciona o menu para uma área mais visível e
reorganiza seus itens para destacar as funcionalidades de maior
importância. A nova ordem de exibição passa a ser:

1. Loja
2. Entrar no Discord
3. Área do Usuário
4. Redes Sociais

O objetivo é melhorar a usabilidade da navegação em dispositivos móveis,
tornando o acesso às principais ações mais intuitivo.

### Alterações

- Alterado o arquivo navbar.blade.php removendo margin-auto para os
links exibirem no topo.

## Plano de Testes

- [x] Acessar a aplicação em um dispositivo mobile ou utilizando o modo
responsivo do navegador.
- [x] Verificar se o menu de navegação é exibido na parte superior da
tela.
- [x] Confirmar o menu inicia na superior e não mais na inferior na
seguinte ordem:
  - Loja
  - Entrar no Discord
  - Área do Usuário
  - Redes Sociais
- [x] Validar que todos os links permanecem funcionando corretamente
após a reorganização.
---

## Evidências (Adicione o comportamento anterior, como prints de tela ou
GIFs.)

### Antes
<img width="450" height="768" alt="image"
src="https://github.com/user-attachments/assets/679e47f0-54d9-4068-a463-cfd820032462"
/>

### Depois (Opcional)
<img width="451" height="766" alt="image"
src="https://github.com/user-attachments/assets/601ffe43-2268-4870-a1c1-ab0f21912588"
/>


## Issues Relacionadas

Closes [#464](#464)
fernanduandrade and others added 22 commits August 2, 2026 13:13
…ect (#443)

## Contexto

Este PR implementa a solução da issue #427 e traz melhorias adicionais
relacionadas à experiência de edição do perfil.

Atualmente, a seção **Skills** exige múltiplos cliques para adicionar
várias habilidades e bloqueia o salvamento do perfil quando existem
linhas de skills completamente vazias. Esses comportamentos tornam o
preenchimento mais lento e geram fricção desnecessária durante a edição
do perfil.

Além disso, foram realizadas melhorias complementares na experiência de
visualização e gerenciamento de links sociais.

## Alterações

### Skills


- Permite salvar o perfil quando existirem linhas de skills
completamente vazias.
- Ajusta a validação para ignorar registros vazios que não possuem dados
preenchidos.
- Mantém as validações para skills efetivamente preenchidas.
- Implementa comportamento reativo utilizando `live()` e
`afterStateUpdated()` do Filament para monitorar alterações nos campos
da skill.
- Adiciona automaticamente uma nova linha de skill quando o último campo
da skill atual é preenchido.
- A nova linha é criada automaticamente apenas quando:
  - A skill possui um valor selecionado.
  - O nível de proficiência foi informado.
  - O usuário está preenchendo a última linha disponível do repeater.
- Utiliza geração automática de UUIDs para manter a consistência dos
itens adicionados dinamicamente ao repeater.

### Social Media

- Adiciona suporte ao LinkedIn na seção de links sociais do perfil.
- Implementa tratamento para aceitar tanto:
  - URLs completas (`https://linkedin.com/in/usuario`)
  - Apenas o username/handler (`usuario`)
- Normaliza automaticamente os dados para gerar o link correto.

### Preview do Perfil

- Adiciona links de redirecionamento nos ícones e informações exibidas
no card de preview do perfil.
- Permite acessar diretamente os perfis sociais a partir da visualização
do usuário.

## Benefícios

- Reduz significativamente a quantidade de cliques necessários para
cadastrar múltiplas skills.
- Torna o fluxo de edição mais rápido e intuitivo.
- Evita bloqueios de salvamento causados por linhas vazias sem dados.
- Melhora a experiência de configuração de redes sociais.
- Facilita a navegação a partir do preview do perfil.

## Issue relacionada

Closes #427


## Links com redirect no preview profile
<img width="441" height="619" alt="adiiconar links com redirect no
profile preview"
src="https://github.com/user-attachments/assets/a3b91d12-e82f-400d-a83b-a3415b13df31"
/>

## Linkedin adicionado no socio media
<img width="988" height="493" alt="adicionado linkedin na socials links"
src="https://github.com/user-attachments/assets/7727402c-5ca9-406d-ae8b-78d382e43bd5"
/>


## Permite salvar com campos em branco ainda não preenchidos
<img width="1280" height="579" alt="adicionar skill com dados em branco"
src="https://github.com/user-attachments/assets/bc8671d4-1e00-43fc-8cba-3259d11715b7"
/>

## Adiciona novo campo automático quando a última coluna é preenchida
<img width="1280" height="579" alt="adicionar novo campo automatico"
src="https://github.com/user-attachments/assets/6c7de681-8520-4417-b232-564557e63cf1"
/>

---------

Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
## Contexto
Este PR implementa a solução da issue
[461](#461) e remove a
opção de fixar post do feed da aplicação

## Descrição do problema
Atualmente existe a função de fixar disponível na timeline, porém ela
pode ser utilizada por qualquer usuário e não possui um propósito
concreto definido. Como não há uma funcionalidade real por trás dessa
ação, ela acaba disponível sem trazer valor claro e pode gerar confusão
no uso. A sugestão é desativar essa função até que exista uma proposta
concreta e bem definida para ela.

Alterações
- Adicionado feature flag com o valor **false** como default para
desabilitar a função de fixar
- Desabilita do engament.blade.php a opção de fixar/desfixar

## ANTES
<img width="1206" height="191" alt="image"
src="https://github.com/user-attachments/assets/b93b0da4-de84-4109-9e44-57ec33606c97"
/>


## DEPOIS
<img width="1158" height="614" alt="image"
src="https://github.com/user-attachments/assets/26515963-b63e-4aeb-91cc-157a27d9eadc"
/>


## Issue relacionada
Closes #461

Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
#462)

## Descrição

Corrige a exibição do avatar nas threads. Antes, as respostas exibiam
apenas as iniciais do usuário, mesmo quando ele possuía uma imagem de
perfil cadastrada. Agora, o comportamento é o mesmo do feed, exibindo o
avatar quando disponível e utilizando as iniciais apenas como fallback.

## Mudanças

* Exibe o avatar do autor nas respostas da thread, seguindo o mesmo
padrão do feed.
* Exibe o avatar do usuário logado no composer de respostas.
* Adiciona eager loading de `user.media` nas replies para evitar
consultas N+1.
* Adiciona testes para garantir a renderização correta do avatar nas
replies.

## Como testar

1. Faça login com um usuário que tenha um avatar cadastrado.
2. Acesse uma thread que contenha respostas desse usuário.
3. Verifique que o avatar é exibido tanto nas respostas quanto ao lado
do campo de resposta.

> **Nota (teste local):**
> O Spatie Media Library gera URLs absolutas a partir do `APP_URL`. Como
o `php artisan serve` roda em `localhost:8000`, é necessário configurar:
>
> ```env
> APP_URL=http://localhost:8000
> ```
>
> Em seguida, limpe o cache de configuração:
>
> ```bash
> php artisan config:clear
> ```
>
> Sem isso, as URLs dos avatares apontam para a porta `80`, fazendo com
que as imagens apareçam quebradas. Esse comportamento também afeta o
feed e não é específico deste PR.
Closes #459

Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
## Descrição

Após salvar o perfil, a página agora rola automaticamente para o topo
para que o usuário visualize imediatamente a notificação de sucesso.

## Mudanças

- `app-modules/panel-app/src/Pages/ProfilePage.php`
  - Dispara o evento `scroll-to-top` após o salvamento do perfil.
- `app-modules/panel-app/resources/views/pages/profile.blade.php`
- Adiciona um listener para o evento `scroll-to-top`, realizando o
scroll da página até o topo.

## Como testar

1. Acessar `/profile`.
2. Alterar qualquer informação do perfil.
3. Salvar o formulário.
4. Verificar que, após o salvamento, a página rola automaticamente para
o topo e a mensagem de sucesso fica visível.

## Testes

Testado manualmente seguindo os passos acima.

---------

Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
## O que foi feito

Adiciona um contador de caracteres no campo **Sobre** da página de
perfil, exibindo o progresso no formato `atual/500`.

A mudança mantém o limite já existente de 500 caracteres e apenas
melhora o feedback visual para o usuário durante o preenchimento do
perfil.

## Detalhes técnicos

- Adicionado contador no `Textarea::make('about')`.
- Mantido `maxLength(500)`.
- Trocado `live(onBlur: true)` por `live()` para atualizar o campo
durante a digitação.
- Usado `mb_strlen()` para respeitar caracteres multibyte.
- O contador visual é atualizado no cliente para evitar atraso/perda de
sincronia durante re-renders do Livewire.

## Evidência visual

<img width="992" height="462" alt="image"
src="https://github.com/user-attachments/assets/8ee8bb26-877d-46fe-82f9-01af298a42f5"
/>


## Testes

```bash
vendor\bin\pest app-modules\panel-app\tests\Feature\ProfilePageTest.php --compact
```

## Teste manual

1. Acesse a página de perfil.
2. Digite no campo **Sobre**.
3. Verifique se o contador acompanha a digitação no formato `atual/500`.

Closes #426

---------

Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
## Problema

Atualmente, o usuário não consegue identificar qual provedor OAuth
utilizou anteriormente para
  acessar a plataforma.

  ## Solução

Foi implementado o armazenamento do último provider OAuth utilizado no
localStorage, exibindo um
  badge de “Último acesso” no botão correspondente na tela de login.

A preferência é salva somente após um login OAuth concluído com sucesso
e não exige alterações no
banco de dados (salvei na localStorage que normalmente é o mais usado
pra esse tipo de badge).

  ## Evidências

  ### Antes

<img width="1409" height="647" alt="Screenshot From 2026-07-14 09-16-06"
src="https://github.com/user-attachments/assets/42fc6a54-6881-4868-88c2-74551e2e5b60"
/>

  ### Depois

<img width="1409" height="647" alt="Screenshot From 2026-07-14 09-20-27"
src="https://github.com/user-attachments/assets/0d1bb8bc-aee9-4bf6-b81a-49be7c010963"
/>

  ## Testes

- Teste de login OAuth concluído no painel `app`, validando o provider
na URL de retorno.
  - Teste de OAuth negado, garantindo que nenhum provider seja salvo.
  - `OAuthControllerTest.php`: 2 testes e 5 asserções aprovadas.
  - Rector e Pint validados dentro do container PHP.
  - Build do frontend validado.

---------

Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
## O que mudou

Este PR melhora o texto do modal de mesclagem de contas para deixar mais
claro o que acontece antes do usuário confirmar a ação.

Agora o modal informa explicitamente que:

- a conta existente será mantida como principal;
- a conta atual será absorvida/removida;
- o histórico associado à conta mantida será preservado;
- após finalizar, o usuário será autenticado novamente na conta mantida.

A regra de mesclagem não foi alterada. A mudança é apenas na mensagem
exibida para reduzir ambiguidade no fluxo.

## Observação

Não configurei o OAuth real localmente para este PR, porque o fluxo
depende de credenciais e callbacks cadastrados nos provedores.

O teste cobre o estado pós-OAuth simulando o `oauth_merge_pending`, que
é exatamente o ponto em que o modal de confirmação é exibido.

Também fiz uma validação visual local forçando temporariamente a
abertura do modal apenas para gerar o screenshot abaixo. Essa alteração
temporária foi desfeita antes do commit.

## Screenshots
<img width="1212" height="872" alt="image"
src="https://github.com/user-attachments/assets/2208f06a-8562-4e77-9464-421c91a5c28e"
/>


## Testes

- `php artisan test tests\Feature\ConnectionHubMergeModalTest.php`

Closes #429

Signed-off-by: Daniel Reis <danielhe4rt@gmail.com>
Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
This pull request introduces a small user experience improvement to the
profile save process. After saving the profile, the page will now
automatically scroll to the top, ensuring users see any relevant
notifications or messages.

* Added a call to dispatch the `scroll-to-top` event after saving
profile skills in the `save` method of `ProfilePage.php`.
## Summary

Atualmente, o repositório não possui um padrão definido para a descrição
de Pull Requests. Como consequência, cada PR é estruturado de forma
diferente, dificultando a revisão, a reprodução dos testes e a
identificação das alterações realizadas.

Este PR adiciona um template de Pull Request para padronizar a abertura
de novas contribuições, orientando os colaboradores a documentarem:

- O contexto e objetivo da alteração;
- As principais modificações realizadas;
- O plano de testes utilizado para validação;
- Evidências visuais, quando aplicável;
- As issues relacionadas.

Além de tornar o processo de revisão mais consistente, este template
também serve como um guia para novos contribuidores. Como a comunidade
incentiva a participação de pessoas com diferentes níveis de
experiência, o template fornece uma orientação clara sobre quais
informações devem ser incluídas em um Pull Request, tornando o processo
de contribuição mais simples, organizado e acolhedor.

### Alterações

- Adicionado template padrão para Pull Requests.
- Incluída seção **Summary** para contextualização da alteração.
- Incluída seção **Alterações** para listar as modificações realizadas.
- Incluído **Plano de Testes** em formato de checklist para documentar
os passos de validação.
- Incluída seção **Evidências** para anexar prints, GIFs ou vídeos
quando aplicável.
- Incluída seção **Issues Relacionadas** para vincular automaticamente o
PR às issues correspondentes.
## Summary
- Substitui o `<input type="date">` nativo do perfil por um `DatePicker`
do Filament (`native(false)`), na seção Pessoal, junto com o nickname.
- Destaca o dia atual no calendário para melhorar o contraste,
especialmente no dark mode.
- Adiciona ícone de calendário no campo e abre o picker ao clicar nele.

Fixes #397

## Test plan
- [ ] Abrir `/app/profile` e confirmar que "Data de Nascimento" usa o
DatePicker do Filament (não o nativo do browser)
- [ ] Abrir o calendário e confirmar que dias de meses adjacentes não
aparecem como números ambíguos
- [ ] No dark mode, confirmar que o dia de hoje fica visualmente
distinto
- [ ] Clicar no ícone de calendário e confirmar que o painel abre
- [ ] Salvar uma data de nascimento e confirmar que persiste após reload


Mergir o PR #401 antes e resolver conflitos!!!!

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Closes #471

`make check`, `make pint` e `make format` estavam quebrados. O
`CLAUDE.md` manda rodar `make check` antes de commitar — quem segue a
doc bate num erro que não é dele.

## O bug

```make
pint:
	@export XDEBUG_MODE=off      # morre aqui: não alcança a linha de baixo
	@$(CURDIR)/vendor/bin/pint --parallel
	@Unset XDEBUG_MODE           # ← e esta linha mata o target
```

Cada linha de receita do make roda no **próprio shell**, então o
`export` nunca teve efeito nenhum sobre o Pint. E o `unset` derruba o
target: o make executa a linha direto, sem shell, e `unset` é builtin,
não binário — `make: unset: No such file or directory` → `Error 1`.

Reproduzi isolado: acontece **mesmo com a sintaxe correta** (`unset
XDEBUG_MODE`). O `=off` sobrando no `test-pint` era só o segundo erro na
mesma linha, não a causa. Por isso `make pint` e `make format` caíam
junto, não só o `test-pint`.

A variável agora vai inline no comando, que é onde ela precisa valer:

```make
test-pint:
	@XDEBUG_MODE=off $(CURDIR)/vendor/bin/pint --test --parallel
```

## Também neste PR

- **`test-phpstan` ganha `--memory-limit=2G`**, que o target `phpstan`
já tinha. Sem ele o `make check` estoura os 128M padrão do PHP CLI e
morre com fatal de memória — ou seja, `make check` tinha *dois* motivos
independentes pra falhar.
- **Help do `env-down`** dizia "Start the development environment"
(cópia do `env-up`).

## Por que o CI não pegou

Os workflows chamam `vendor/bin/pint`, `vendor/bin/phpstan` e
`vendor/bin/pest` direto — nenhum passa pelo Makefile. O pipeline seguia
verde com o gate local quebrado. Vale considerar se o CI deveria
consumir os mesmos targets, pra não haver duas definições de "checar o
projeto" que podem divergir de novo.

## Verificação

Todos exit 0, e nenhum arquivo tocado além do Makefile:

| | |
|---|---|
| `make check` | ✅ 0 |
| `make test-pint` | ✅ 0 |
| `make test-phpstan` | ✅ 0 |
| `make pint` | ✅ 0 |
| `make format` | ✅ 0 |

## Pergunta — `env-down` é destrutivo de propósito?

```make
@docker compose --file docker-compose.yml down --rmi all --volumes
```

`--volumes` apaga o banco de desenvolvimento e `--rmi all` remove as
imagens. Quem digita `make env-down` esperando só parar os containers
perde o Postgres local e paga o download/build de novo no próximo
`env-up`.

Se for intencional, o help novo já avisa e tá resolvido. Se não for, eu
proporia `env-down` = `down` puro e um `env-destroy` separado pro
comportamento atual — mas não mudei nada aqui porque é decisão de vocês.
)

## Contexto

O Meeting Showcase podia encontrar a mensagem no período informado e
ainda assim não identificar corretamente o participante. Isso acontecia
porque os dados do Discord chegam em formatos diferentes e a tela só
tratava parte deles.

Também existia um corte no último minuto escolhido. Ao informar 23:40
como horário final, por exemplo, mensagens enviadas depois de 23:40:00
ficavam fora da consulta.

Este PR corrige a leitura e exibição dos participantes que já foram
ingeridos. O fluxo de ingestão das mensagens não foi alterado.

### Alterações

- Suporte aos formatos de metadata na raiz, em `metadata.user` e em
`metadata.author`
- Fallback para o usuário vinculado quando a identidade não possui
metadata suficiente
- Inclusão de identidades desconectadas que ainda possuem mensagens
históricas
- Inclusão de todo o último minuto selecionado no filtro de período
- Testes cobrindo os formatos de metadata, fallback de usuário,
identidade desconectada e mensagens dentro e fora do limite final

---

## Plano de Testes

- [x] Rector nos arquivos alterados sem sugestões
- [x] Pint completo aprovado
- [x] PHPStan completo sem erros
- [x] Testes de Marketing com 8 testes e 69 assertions
- [x] Teste da importação de `metadata.author` com 1 teste e 4
assertions
- [x] Testes da persistência de mensagens com 3 testes e 9 assertions
- [x] Validação local da tela com participantes nos diferentes formatos
de metadata

A suíte completa local executou 933 testes. Foram 931 aprovados e 2
falhas preexistentes em `MergeDuplicateDiscordProfilesTest`, causadas
pelo tratamento de caminho absoluto do Windows no parâmetro
`--pairs-file`. Esses testes ficam fora dos arquivos e do fluxo alterado
neste PR. O CI em Linux fará a validação completa novamente.

---

## Issues Relacionadas

Closes #477
## Contexto

No login do painel (`/app/login`), a logo mobile da He4rt ficava preta
sobre fundo escuro no dark mode, com contraste baixo e quase invisível.
Isso acontecia porque o SVG é carregado via `<img>` com
`fill="currentColor"`, e classes como `text-purple-500` não alteram a
cor nesse caso.

A alteração aplica filtro CSS apenas no dark mode (`dark:brightness-0
dark:invert`), tornando a logo branca no dark e mantendo a preta no
light. O impacto esperado é melhorar a legibilidade da marca na tela de
login sem mudar o comportamento no tema claro.

### Alterações

- Ajuste da classe CSS da logo mobile em
`app-modules/panel-app/resources/views/auth/login.blade.php`
- Troca de `text-purple-500` por `dark:brightness-0 dark:invert`
- Light mode preservado; dark mode passa a exibir a logo com contraste
adequado

---

## Plano de Testes

- [ ] Executar `make check`
- [ ] Executar `make test`
- [ ] Abrir `/app/login` em viewport mobile (< `lg`) no **dark mode** e
confirmar que a logo fica visível (branca)
- [ ] Abrir `/app/login` em viewport mobile (< `lg`) no **light mode** e
confirmar que a logo preta original permanece
- [ ] Confirmar no desktop (`lg+`) que o painel esquerdo com a logo
branca não foi alterado

---

## Evidências

### Antes

<img width="940" height="946" alt="image"
src="https://github.com/user-attachments/assets/05fe5557-3eb3-466c-ac98-cde54594aa8e"
/>

### Depois

<img width="667" height="837" alt="image"
src="https://github.com/user-attachments/assets/adc125f3-c971-457a-8885-57c854745d4c"
/>

---

## Issues Relacionadas

Fixes #468
Closes #467 

Neste PR estou adicionando templates para as principais frentes que hoje
abordados, que são features, bug's e PRD.

---------

Signed-off-by: Sther <72408918+stherzada@users.noreply.github.com>
Co-authored-by: Tiago Góes <tiago.goes2009@gmail.com>
…481)

## Contexto
Esta alteração visa corrigir a omissão do texto "Entrar" na tela de
login para usuários que usam light theme. Foi aplicado suporte para o
texto ser visivél tanto em light/dark theme


### Alterações
- O login.blade.php foi alterado garantindo suporte para os dois temas.
- Ajustado background para o span "ou" que também não estava de acordo
para usuários light theme


## Plano de Testes
- [x]  Executar `make check`
- [x] Executar `make test`
- [x] Abrir /app/login usando light theme e confirmar que o texto
"Entrar" é exibido

## Evidências

### Antes
<img width="1919" height="964" alt="image"
src="https://github.com/user-attachments/assets/e940a6f0-f24d-4176-a59f-28b4a0742055"
/>

### Depois
<img width="1919" height="959" alt="image"
src="https://github.com/user-attachments/assets/3c94bef6-0825-4808-878f-d4e515549b15"
/>


## Issues Relacionadas
Fixes #480
Resolve conflitos e adapta o módulo de eventos à remoção do multi-tenancy
(#413), que entrou na 4.x depois que esta branch saiu.

Conflitos resolvidos:
- composer.json / package.json / theme.css / shards.json: versões da 4.x,
  mantendo `bacon/bacon-qr-code` do módulo de eventos.
- Lockfiles regenerados a partir da 4.x.
- PanelAdmin/PanelApp ServiceProviders e AppPanelProvider: mantidos os dois
  lados (registros de eventos + DiscordCluster/FilamentAsset da 4.x).
- Factories antigas do módulo events, `GenerateDiscordTenant` e `TenantUser`:
  mantida a remoção.
- Testes de timeline/perfil: versão da 4.x, que já contém as correções de
  autor removido e avatar.

De-tenancy do módulo de eventos:
- `events.tenant_id` removido da migration; índices viram `slug` único e
  `starts_at`.
- `Event` perde a relação `tenant()` e o campo do PHPDoc/fillable.
- `CheckInRequested` perde `tenantId`; `HandleBotCheckIn` deixa de filtrar
  por tenant.
- Painéis admin/app deixam de escopar eventos por tenant.
- Seeder, factory e suíte de testes ajustados; testes de isolamento por
  tenant que perderam sentido foram removidos ou reescritos (o de 404 agora
  cobre evento em rascunho).

`ProfilePage` volta à implementação da 4.x — o upload de avatar/capa da
branch foi substituído pelo fluxo com FileUpload do Filament (#435).
@danielhe4rt
danielhe4rt requested a review from a team August 13, 2026 17:05
@gvieira18
gvieira18 merged commit 9e50a62 into feat/events Aug 15, 2026
8 checks passed
@gvieira18
gvieira18 deleted the chore/merge-4x-into-feat-events branch August 15, 2026 00:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.