Skip to content

Sync 4.x → feat/squads + remoção de multi-tenancy#418

Merged
gvieira18 merged 24 commits into
feat/squadsfrom
chore/sync-4.x-into-squads
Jul 18, 2026
Merged

Sync 4.x → feat/squads + remoção de multi-tenancy#418
gvieira18 merged 24 commits into
feat/squadsfrom
chore/sync-4.x-into-squads

Conversation

@sirelves

@sirelves sirelves commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

O que é

Sincroniza origin/4.x em feat/squads e remove o multi-tenancy do lado squads, deixando a branch pronta para virar a base atual dos squads.

A maior parte do diff é o próprio 4.x entrando em feat/squads (merge de sync). A parte escrita à mão é: a resolução dos conflitos + a des-tenancy dos módulos squads/onboarding.

Merge 4.x → feat/squads

13 conflitos resolvidos (o merge herda remoção de tenancy do #413 + melhorias do 4.x):

  • Fica com 4.x: TenantUser/GenerateDiscordTenant deletados, $tenantId fora do Feed, Skill::search(exclude:), sibling-rows no ProfilePage, twitch:link-channel config-based.
  • Fica com squads: OnboardingServiceProvider mantém boot() + loadMigrationsFrom (o 4.x só tinha stub vazio).
  • Testes: mantidos os 4 de GithubPullRequestApproved (feat(integration-github): emit domain event GithubPullRequestApproved #378); removidos os de isolamento/fan-out por tenant.

Des-tenancy de squads/onboarding

  • squads/onboarding: dropa tenant_id, params Tenant e whereBelongsTo. Uniques colapsam para chave global — squads: UNIQUE(slug), onboarding: UNIQUE(user_id, type).
  • profile: remove métodos duplicados (workExperiences/profileSkills/skills) deixados por conflito de merge — corrige o fatal Cannot redeclare.
  • identity: remove teste duplicado acoplado a tenant (o equivalente sem tenant continua cobrindo o reassign de timeline no merge de contas).

Docs

  • CONTEXT.md de squads e onboarding atualizados para o schema atual.
  • ADRs afetados ganham nota de supersede (feat(tenancy)!: remove multi-tenancy #413) no topo, preservando a decisão histórica (não reescreve o registro).

Testes

vendor/bin/pest904 passed, 0 falha localmente (test_heartdevs).

fernanduandrade and others added 19 commits July 6, 2026 22:01
## Contexto

A página inicial da comunidade reúne diversos acessos importantes, porém
o link para a documentação (`/docs`) não estava em destaque, o que pode
dificultar que novos membros descobrissem um dos principais recursos da
plataforma.

Este PR adiciona um **Call to Action (CTA)** para a documentação na
landing page da comunidade, tornando o acesso mais visível e
incentivando os usuários a explorarem os guias, tutoriais e demais
conteúdos disponíveis.

### Arquivos afetados

- `app-modules/portal/resources/views/sections/hero.blade.php` —
adiciona um CTA para `/docs` na landing page da comunidade.

### Alterações

- Novo botão/CTA direcionando para `/docs`.
- Ajuste da interface para destacar a documentação como um dos
principais pontos de entrada da comunidade.

### ANTES
<img width="1919" height="866" alt="image"
src="https://github.com/user-attachments/assets/0a40dd21-f36e-4721-93cc-38fba48b6281"
/>


### DEPOIS
<img width="1919" height="868" alt="image"
src="https://github.com/user-attachments/assets/4f68c4ac-ab6d-4771-8dc0-7ac157fbabf2"
/>
## Summary
- Add a caller workflow that reuses
`he4rt/.github/actions/dependabot-automerge` to approve and squash-merge
Dependabot **github-actions** patch/minor PRs. Majors and other
ecosystems stay manual.
- Mirrors how this repo already consumes
`he4rt/.github/actions/auto-assign`.

## Requires
- Org/repo setting **Allow GitHub Actions to create and approve pull
requests** must be enabled (used by the approve step).
Members can attach skills to their profile, each with a proficiency
level and years of experience, chosen from a curated catalog.

- skills catalog (global, seeded inline in the create migration) +
profile_skills pivot (tenant-scoped via profile) with UNIQUE(profile_id,
skill_id) and INDEX(skill_id, proficiency)
- SkillCategory and SkillProficiency enums with pt_BR/en labels
- Skill/ProfileSkill models, Profile relations, per-request memoized
label lookup
- SyncProfileSkills action + ProfileSkillDTO (validates catalog, dedup,
range)
- skills repeater on the profile page with a server-side searched select
(getSearchResultsUsing over name/slug, capped) + preview card rendering
- ADR-0001 documenting the data-modeling decision

---------

Signed-off-by: Daniel Reis <danielhe4rt@gmail.com>
Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
Co-authored-by: Gabriel Vieira <48625433+gvieira18@users.noreply.github.com>
Signed-off-by: Daniel Reis <danielhe4rt@gmail.com>
Co-authored-by: danielhe4rt <danielhe4rt@gmail.com>
## Contexto

O componente de terminal iniciava com uma altura reduzida e aumentava
gradualmente conforme a animação dos comandos era executada. Esse
comportamento causava mudanças de layout (layout shift), prejudicando a
experiência visual da página.

Este PR corrige a altura inicial do componente para que ela permaneça
estável durante toda a animação.

### Arquivos afetados

- `app-modules/portal/resource/view/components/terminal.blade.php` —
ajuste da altura inicial do componente de terminal.

### Alterações

- Definida uma altura inicial consistente para o terminal.
- Evitado o aumento progressivo da altura durante a animação dos
comandos.
- Reduzido o layout shift, proporcionando uma experiência visual mais
estável.

### ANTES
<img width="1280" height="579" alt="agora"
src="https://github.com/user-attachments/assets/88b02cf5-c854-4073-bc85-31f2e91559aa"
/>


### DEPOIS
<img width="1280" height="579" alt="fix_terminal"
src="https://github.com/user-attachments/assets/942aa745-4780-49cb-bd37-fb61219333a5"
/>
Signed-off-by: Clinton Rocha <50300876+Clintonrocha98@users.noreply.github.com>
Co-authored-by: danielhe4rt <danielhe4rt@gmail.com>
…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 ✅
Merge de origin/4.x. 13 conflitos resolvidos:
- tenancy removida (#413) + melhorias do 4.x (Skill exclude, ProfilePage
  sibling rows, twitch command, Feed sem tenantId): fica com 4.x
- OnboardingServiceProvider: mantido o da squads (4.x tem so stub)
- TenantUser/GenerateDiscordTenant: seguem a delecao do 4.x
- GithubWebhookTest: mantidos os 4 testes de GithubPullRequestApproved (#378),
  removido o teste de fan-out por tenant

PENDENTE (branch NAO sobe): modulos squads e onboarding ainda dependem de
tenancy (Tenant::class, tenant_id, whereBelongsTo). Precisa des-tenancy antes
de virar PR. Aguardando decisao do Dani.
@sirelves
sirelves requested a review from a team July 13, 2026 21:11
Completa o merge de origin/4.x em feat/squads (que já removeu multi-tenancy,
#413), propagando a remoção para os módulos que ainda dependiam dela:

- squads/onboarding: dropa tenant_id, params Tenant e whereBelongsTo; uniques
  colapsam para chave global (squads: UNIQUE(slug); onboarding: UNIQUE(user_id,
  type)).
- profile: remove métodos duplicados (workExperiences/profileSkills/skills)
  deixados por conflito de merge — corrige fatal "Cannot redeclare".
- identity: remove teste duplicado acoplado a tenant (o equivalente sem tenant
  permanece cobrindo o reassign de timeline no merge de contas).
- docs: CONTEXT.md dos dois módulos refletem o schema atual; ADRs ganham nota
  de supersede (#413), preservando a decisão histórica.

Suíte completa: 904 passed.
@sirelves
sirelves force-pushed the chore/sync-4.x-into-squads branch from baa7ab8 to 54d8f2d Compare July 13, 2026 21:14
#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).
Artefato de resolução de conflito do sync 4.x disparava
AccountsMerged duas vezes, fazendo listeners (ex.: reatribuição de
timeline #395) rodarem em dobro no merge de contas.
@gvieira18

Copy link
Copy Markdown
Member

🔄 Novos commits neste push (54d8f2dd..ead091fb)

Rodada de fechamento do sync do 4.x. Resumo do que subiu:

🐛 b0a7c6b6 — fix(identity): remove AccountsMerged duplicado

Durante a resolução de conflitos do sync, o MergeAccountsAction ficou disparando o evento AccountsMerged duas vezes seguidas. Isso fazia os listeners (ex.: reatribuição de timeline — #395) rodarem em dobro no merge de contas. Removida a linha duplicada; agora dispara (confirmado por teste).

🔗 c6462f4b — chore(squads): sync 4.x (#415) into feat/squads

Merge do 4.x trazendo o #415 (ddface9eharden Twitch OAuth setup & one-click connect), único commit do 4.x que ainda faltava aqui.

Feito via git merge (não cherry-pick) de propósito: mantém o 4.x como ancestral real do branch, então git log HEAD..4.x fica vazio e um próximo sync do 4.x não vai re-aplicar/reconflitar o #415. Consistente com o merge de sync anterior (e5c82bf8). Toca só arquivos twitch (integration-twitch / panel-admin), sem sobreposição com squads/onboarding — merge limpo, zero conflitos.

🧪 ead091fb — chore(tests): update pest shard timings

Regeneração do tests/.pest/shards.json (timings de sharding do Pest).


✅ Validação

Check Resultado
PHPStan (projeto inteiro) 0 erros
Pest (Twitch + MergeAccounts) 35/35 · 91 assertions
Pint / Rector passed
Merge do #415 sem conflitos

Conferência: o branch contém exclusivamente o sync do 4.x + des-tenancy de squads/onboarding — nenhuma mudança fora de escopo. HEAD..4.x está vazio (branch em dia com o 4.x).

@guisaliba

Copy link
Copy Markdown

up

@gvieira18

Copy link
Copy Markdown
Member

@Clintonrocha98 dropa um aprove aqui

@YuriSouzaDev YuriSouzaDev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@gvieira18
gvieira18 merged commit fbac2de into feat/squads Jul 18, 2026
8 checks passed
@gvieira18
gvieira18 deleted the chore/sync-4.x-into-squads branch July 18, 2026 15:14
danielhe4rt added a commit that referenced this pull request Jul 18, 2026
Closes #348

## Motivação

O programa de Squads precisa de uma jornada de entrada persistida e
auditável antes que uma pessoa possa, futuramente, se candidatar ou
propor um Squad. Essa jornada pertence ao módulo `onboarding`: o módulo
`squads` será apenas consumidor do gate de conclusão, conhecido no
domínio como **APTO**.

A issue #348 entrega o primeiro recorte específico dessa jornada. Ela
conecta o tipo `Squads` ao seu fluxo, declara a ordem das etapas e
estabelece o comportamento inicial do formulário sem antecipar as regras
das issues seguintes.

## O que mudou

### Novo `SquadsOnboardingFlow`

- Implementa o contrato `OnboardingFlow`.
- Declara as etapas na ordem `form` e `git_challenge`.
- Mantém `prerequisites()` vazio neste recorte; a exigência de Welcome
concluído pertence à #351.
- Avança a etapa pendente persistida para `done` e registra
`completed_at`.
- Considera o onboarding concluído apenas quando a quantidade de etapas
concluídas corresponde à quantidade de etapas declaradas.
- Mantém o onboarding Squads em `in_progress` após a conclusão do
formulário, pois `git_challenge` ainda não foi concluído.

### Resolução do fluxo pelo enum

- `OnboardingType::Squads->handler()` passa a resolver
`SquadsOnboardingFlow`.
- O antigo caminho que tratava Squads como um tipo sem handler deixa de
ser necessário.
- O `match` permanece exaustivo para os tipos atualmente declarados.

### Estado inicial da jornada

Ao iniciar um onboarding do tipo Squads:

- o onboarding é criado com status `in_progress`;
- somente a primeira etapa declarada, `form`, é persistida;
- o formulário começa com status `pending`;
- `git_challenge` é declarado como a próxima etapa do fluxo, mas ainda
não é materializado nem habilitado.

Ao avançar o formulário:

- `form` passa para `done`;
- `completed_at` da etapa é preenchido;
- o onboarding continua `in_progress`;
- a pessoa ainda não se torna APTO.

## Testes

- Adiciona testes de feature dedicados ao `SquadsOnboardingFlow`.
- Verifica a resolução de `OnboardingType::Squads` para o handler
correto.
- Verifica a ordem exata das etapas: `form` antes de `git_challenge`.
- Verifica que o início persiste somente `form / pending`.
- Verifica que o avanço conclui o formulário sem concluir o onboarding
Squads.
- Remove do teste de Welcome a expectativa obsoleta de que Squads não
possui handler.
- Adiciona um teste unitário exaustivo garantindo que todos os casos de
`OnboardingType` resolvem uma implementação de `OnboardingFlow`.

## Fora de escopo

Este PR não implementa:

- validação ou persistência de payload do formulário e a action genérica
`AdvanceStep`, que pertencem à #346;
- gate público de conclusão, que pertence à #349;
- gate de identidade GitHub vinculada, que pertence à #350;
- pré-requisito de Welcome concluído, que pertence à #351;
- materialização ou conclusão de `git_challenge` e transição para APTO,
que pertencem à #352;
- listener de aprovação de PR no GitHub, que pertence à #353;
- candidatura, criação, associação ou governança de Squads.

## Por que este PR aponta temporariamente para
`chore/sync-4.x-into-squads`

Este PR depende do #418, que sincroniza `4.x` com `feat/squads` e remove
o multi-tenancy do código de onboarding e Squads. Em especial, #432 foi
implementado e testado sobre a assinatura sem tenant de
`StartOnboarding`, introduzida nessa base.

Usar `chore/sync-4.x-into-squads` como base temporária mantém a
comparação do #432 restrita às mudanças da #348. Se ele apontasse
diretamente para `feat/squads` antes da integração do #418, o diff
também exibiria a remoção de tenancy e os demais ajustes do PR pai,
misturando dois escopos independentes e dificultando a revisão.

A relação atual é:

```text
feat/squads
  └── chore/sync-4.x-into-squads  (#418)
        └── feat/348-squads-onboarding  (#432)
```

Depois que o #418 for integrado em `feat/squads`, a base deste PR poderá
ser alterada para `feat/squads` sem atribuir ao #432 as mudanças de
remoção de tenancy.

---------

Co-authored-by: danielhe4rt <danielhe4rt@gmail.com>
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.

Sync origin/4.x em feat/squads + remoção de multi-tenancy em squads/onboarding