Sync 4.x → feat/squads + remoção de multi-tenancy#418
Conversation
## 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.
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.
baa7ab8 to
54d8f2d
Compare
#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.
🔄 Novos commits neste push (
|
| 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).
|
up |
|
@Clintonrocha98 dropa um aprove aqui |
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>
O que é
Sincroniza
origin/4.xemfeat/squadse remove o multi-tenancy do lado squads, deixando a branch pronta para virar a base atual dos squads.Merge 4.x → feat/squads
13 conflitos resolvidos (o merge herda remoção de tenancy do #413 + melhorias do 4.x):
TenantUser/GenerateDiscordTenantdeletados,$tenantIdfora doFeed,Skill::search(exclude:), sibling-rows noProfilePage,twitch:link-channelconfig-based.OnboardingServiceProvidermantémboot()+loadMigrationsFrom(o 4.x só tinha stub vazio).GithubPullRequestApproved(feat(integration-github): emit domain event GithubPullRequestApproved #378); removidos os de isolamento/fan-out por tenant.Des-tenancy de squads/onboarding
squads/onboarding: dropatenant_id, paramsTenantewhereBelongsTo. 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 fatalCannot redeclare.identity: remove teste duplicado acoplado a tenant (o equivalente sem tenant continua cobrindo o reassign de timeline no merge de contas).Docs
CONTEXT.mddesquadseonboardingatualizados para o schema atual.Testes
vendor/bin/pest— 904 passed, 0 falha localmente (test_heartdevs).