feat(community): retrospectiva multi-fonte - #437
Merged
Conversation
Registra o design acordado: contrato de Source em community, snapshot ao publicar, fluxo faseado e as divergencias conscientes da spec-base.
…d (Fase 1) (#438) ## Fase 1 — Juntar as fontes Primeira fatia da [retrospectiva multi-fonte](#437). Transforma a retrospectiva (hoje só GitHub, read model ao vivo no `portal`) numa peça **plugável por fonte**: adicionar uma plataforma passa a ser 1 classe + 1 tag no módulo dono do dado, sem tocar o `portal`. Aponta para a branch de integração `feature/retrospective-multi-source`. ### O que entra **`community` (domínio) — o contrato** - `RetrospectiveSource`: `key()` + `collect(Period, SourceFilters): SourceResult`. - DTOs tipados: `Period`, `SourceFilters` (hideBots + exclusions), `Metric`, `HeadlineMetrics`, `SourceResult`, e a interface `Slide` (`kind()` + `toArray()`). - Nenhum import de Integration: o contrato mora no domínio, as fontes o implementam. **`integration-github` — `GithubSource`** - Refatora o antigo `CommunityRetrospective` do portal preservando o cálculo **1:1** (bots, PRs merged/unmerged, repos só com PR no recorte, destaques por linhas). - Empacota a saída em slides tipados (`github.panorama`, `github.repos`, `github.highlights`, `github.core`, `github.community`). **`activity` — `DiscordSource`** (mora aqui porque o dado — `Voice`, `Message`, `Reaction`, `MembershipEvent` — é do `activity`) - 5 slides: `discord.voice_board`, `discord.messages`, `discord.new_members`, `discord.reactions`, `discord.top_message`. - Agrega **tudo em SQL** escopado pela janela (as tabelas são grandes em prod; `messages` ~2GB). Filtra por `sent_at`/`occurred_at` (tempo do evento), nunca `created_at`. `hideBots` via `source_kind`. Nome de exibição resolvido só para o topo de cada ranking. **`portal` — orquestração** - `RetrospectiveDeck` resolve as fontes por **tagged service** (`retrospective.source`), ordena (github → discord) e descarta as sem dado. - A página compõe: cover (chips por fonte, sem soma cruzada) → bloco de cada fonte (`kind` → componente Blade por convenção) → closing. - Os filtros ricos do visitante (repos/tipos/desfecho/pessoa/ordenação) foram **aposentados**: viram configuração editorial na Fase 2. Restam recorte de período e ocultar bots. ### Fora de escopo (fica pra depois) - Persistência/snapshot da edição e curadoria (Fase 2). - Deck Builder visual (Fase 3). - WhatsApp (PR aditivo). ### Notas - **Sem migrations** nesta fase (nenhuma tabela nova). - Testes: `GithubSource` (golden 1:1, herdado do read model antigo), `DiscordSource` (agregações + escopo temporal + hideBots), `RetrospectiveDeck` (ordem/descarte) e página multi-fonte. - Na suíte completa local há 3 falhas em `BackfillRepositoryTest` (timezone, `+00:00` vs `-03:00`) e alguns erros de `out of shared memory` no `DROP` sob paralelismo. **Ambos são pré-existentes/ambientais** (confirmado: o backfill falha igual na base, e os testes de `activity` passam isolados) — não têm relação com esta mudança.
…ase 2) (#444) ## Fase 2 — Persistir e publicar Segunda fatia da [retrospectiva multi-fonte](#437). A Fase 1 tornou a retrospectiva plugável por fonte, mas ao vivo. Esta fase a transforma numa **peça editorial persistida**: o operador cura e publica; ao publicar, o dado é **congelado num snapshot**; a página pública lê o congelado, sem tocar as fontes. Aponta para a branch de integração `feature/retrospective-multi-source`. ### O que entra **`community` (domínio) — a entidade e o ciclo** - Entidade `Retrospective` (`community_retrospectives`, sem `tenant_id`) + enum `RetrospectiveStatus` (`draft|publishing|published`, contratos Filament). - VOs `DeckConfig` (curadoria: ordem, on/off, exclusions) e `RetrospectiveSnapshot` (`SourceResult[]` congelados) via casts tipados (`AsDeckConfig`, `AsRetrospectiveSnapshot`) — nunca `array` solto. - `FrozenSlide`: reidrata o snapshot honrando o contrato `Slide` sem o domínio importar as classes de slide de `integration-github` (Domain → Integration é proibido). - Actions `CompileSnapshot` (coleta + congela) e `ComposeDeck` (aplica a curadoria sobre o snapshot), `PublishRetrospective` + job `CompileRetrospectiveSnapshot` (congela na fila). **`portal` — leitura do congelado** - `RetrospectiveDeck` removido; a orquestração migrou para o domínio. - A página pública lê a **edição publicada mais recente** (sem filtros do visitante). Rota de preview `/comunidade/retrospectiva/{retrospective}/preview` para o operador (rascunho coletado ao vivo), com guard `403` no `mount`. **`panel-admin` — o CRUD editorial** - `RetrospectiveResource`: **completo em capacidade** (ordem/on-off de fonte por repeater, exclusions, textos de capa/fecho, publicar). Feio de propósito — o Deck Builder visual é a Fase 3. - Preview do admin e página pública compartilham o **mesmo render path** (`ComposeDeck`), então "ver preview" bate com o publicado. ### Refinamentos registrados nos docs (spec + ADR-0002) - `label()` novo no contrato `RetrospectiveSource` (identidade estática das fontes, para o CRUD listar sem coletar). - `FrozenSlide` no lugar de um registro `kind → classe` (forçado pela regra Domain → Integration). - Exclusions **recompilam** o snapshot (mexem no dado); ordem/on-off **re-derivam** dele. ### Fora de escopo (Fase 3) - Deck Builder 3 colunas (drag-drop, preview ao vivo, inspector) — puro upgrade de UX. ### Notas - Migration nova: `community_retrospectives` (colunas `timestamptz`, jsonb para `deck_config`/`snapshot`). - Testes: 63 novos/reescritos (VOs round-trip, casts, `CompileSnapshot`/`ComposeDeck`, publish+job, página pública lê snapshot, preview autenticado, CRUD Filament). Bateria completa: 920/923 verdes — as 3 falhas são o `BackfillRepositoryTest` (timezone `+00:00` vs `-03:00`), **pré-existentes/ambientais**, sem relação com esta mudança.
…fetivas (Fase 3, parte 1)
…ospectiva (Fase 3, parte 2)
A Fase 2 entregou capacidade editorial completa, mas sem noção do resultado: o
operador editava um repeater e só descobria o que fez abrindo o preview em outra
aba. Esta parte é o upgrade de UX desse mesmo poder — montar o deck vendo o deck
—, sem inventar capacidade nova (ADR-0002 do panel-admin).
O builder OCUPA a chave `edit` do resource com rota `/{record}/deck`: a chave
preserva o clique na tabela e o `getUrl('edit')`, a rota deixa a URL honesta.
`EditRetrospective`, `RetrospectiveForm` e `DeckConfigForm` saem — duas telas
editando o mesmo `deck_config` seriam duas fontes de verdade de curadoria.
Três colunas: estrutura seleciona (capa, blocos de fonte com chips de slide,
fecho), preview só lê, inspector edita. O preview é um iframe da MESMA rota
pública de preview, que passa pelo mesmo `ComposeDeck` da página publicada — a
única garantia de que ele não mente é ser literalmente a mesma coisa.
O inspector tem quatro modos e cada um escreve onde a Fase 2 já escrevia; nenhuma
coluna nova, nenhuma migration (`hidden_slides` já persistia sem UI que o
editasse). O picker de exclusions é alimentado por `exclusionCandidates()` e diz
em voz alta que exclusion exige republicar, porque mexe no dado. Refs que caíram
fora do teto da varredura são preservados no salvamento: a UI não pode derrubar
aquilo que não consegue exibir.
Reordenação por botões subir/desce — drag and drop exigiria dependência de
frontend nova (o SortableJS do Filament é interno) para ordenar entre 2 e 5
blocos. Fica como incremento, sem mexer no formato persistido.
Curadoria entra por `instanceof CuratableSource`: fonte que não cura aparece na
timeline com ordem e on/off, sem catálogo nem picker, e o deck segue montando.
- `DeckConfig` ganha `with*` imutáveis (fonte, slide, ordem, exclusions por fonte)
- `DeckStructure` monta a timeline pelo mesmo `position()` do `ComposeDeck`
- `InspectorMode`/`InspectorSelection` tipam a seleção que viaja pela wire
- `ExclusionPicker` agrupa candidatos por kind e isola os refs órfãos
- spec, ADR-0001 do community e CONTEXT.md do panel-admin atualizados
… spec O prettier reinterpretou a continuacao "+ config" como item de lista aninhado. Reescrito em prosa para o formatador nao ter o que remontar.
… preview)
O preview do Deck Builder devolvia 403 para operador logado. Root cause: as rotas
do portal eram registradas no boot() do ServiceProvider SEM grupo de middleware —
nem `web`. Sem StartSession não há sessão, então `auth()->check()` no mount do
CommunityRetrospectivePage é sempre falso e o abort_unless reprova todo mundo.
O modular carrega os arquivos de rota dos módulos sem grupo, e declarar `web` é
obrigação de quem registra (identity/routes/authentication-routes.php já fazia
certo). O portal nunca fez. Efeito colateral silencioso: a página pública da
retrospectiva, `/` e `/redes` são componentes Livewire e estavam sem sessão nem
CSRF.
Por que os testes não pegaram: `actingAs()` seta o usuário direto no container,
sem passar por sessão — passa com ou sem StartSession. Os testes novos olham o
middleware da rota e o cookie de sessão da resposta, não o corpo da resposta.
O módulo `docs` tem o mesmo defeito (`docs`, `docs/{section}/{path?}` sem
middleware); fica fora deste PR por ser outro módulo.
…ift no builder
Três lacunas de acabamento do Deck Builder, todas de leitura do estado editorial:
**Status visível.** O builder mostrava só o título; o operador não sabia se a
edição era rascunho, estava publicando ou já estava publicada — informação central
quando a regra é "exclusion exige republicar". Agora há badge com label, cor e
ícone do RetrospectiveStatus.
**Publicação acompanhada.** Publicar despacha um job, mas nada refletia o fim
dele: a tabela e o builder ficavam em "Publicando" até recarregar na mão. A tabela
ganha `poll('10s')`; o builder faz poll de 3s apenas enquanto o status é
Publishing, chamando refreshStatus().
**Aviso de drift honesto.** Para dizer "republique" só quando importa, o snapshot
passou a guardar os SourceFilters que o produziram (campo novo no VO, jsonb, sem
migration; snapshot antigo reidrata com os filtros padrão). `needsRepublish()`
compara os filtros congelados com os atuais — e só eles: ordem e on/off re-derivam
na composição e nunca pedem republicação. Comparar `updated_at` com `published_at`
avisaria também nesses casos, apagando justo a distinção que a fase defende.
Também renomeia a descrição do preview de "Rascunho ao vivo" para "Prévia ao vivo":
"Rascunho" colidia com o label do enum e tornava vacuosa a asserção de status.
As três colunas do ADR-0002 não caem bem no 7xl padrão do painel: o preview do meio é um deck inteiro, não um card. `maxContentWidth = Width::Full` nesta página apenas — o resto do painel segue no default.
Troca a proporção 3/6/3 por laterais fixas (16rem estrutura, 18rem inspector) com o preview em 1fr. A estrutura é uma lista e o inspector é um formulário: o que ambos precisam não cresce com a tela. O preview é um deck inteiro, então passa a absorver cada pixel extra de monitor em vez de ficar preso a metade da largura. `min-w-0` nas três colunas para conteúdo largo (chips, campos) não estourar a grade em vez de truncar.
A coluna do inspector tinha um cabeçalho com o label e a descrição do
InspectorMode ("Bloco de fonte" / "Exibir a fonte e curar...") logo acima da
Section do formulário, que já nomeia o alvo de forma específica ("Bloco: Discord")
e explica o efeito concreto. Dois títulos para a mesma coisa, o de cima mais vago.
Sai o cabeçalho; o ícone do modo passa para a própria Section (Filament suporta
`->icon()`), então a âncora visual continua — sem duplicar texto. A Section de
Exclusions ganha ícone próprio.
Um `migrate:fresh --seed` passa a entregar 13 meses de atividade de GitHub e
Discord mais uma edição de retrospectiva para cada estado que o Deck Builder
precisa saber renderizar, para dar como testar a fase 3 sem depender de backfill
de produção.
Cada módulo semeia apenas o dado que possui e a composição cross-module mora na
raiz: `community` é domínio e tem `"require": {}`, então não pode conhecer
`integration-github` nem `activity`. O orquestrador é o único dono da linha do
tempo e passa a janela para os três, além de repassar às edições os refs
plantados por cada fonte (`exclusionBaits()`) — é o que faz o picker de
exclusions e o aviso de republicar terem conteúdo real.
Volume vai por insert em lote (~6.000 linhas em ~3s); factory por linha
arrastaria uma ExternalIdentity e dois User a cada mensagem.
O que o dado exercita de propósito:
- bots pelos dois caminhos de detecção (sufixo `[bot]` e `metadata.is_bot`) e
~12% das mensagens sem `source_kind`, que o filtro de bots precisa MANTER;
- repositórios sem PR no recorte, que não viram card mas seguem contando em
pessoas/issues/comentários;
- commits sem `additions`/`deletions`, porque o GithubSource soma essas chaves
sobre todas as contribuições e repeti-las inflaria o total de linhas;
- reações construídas junto das mensagens, para `reactions_total` e
`reactions_count` baterem com as linhas de `activity_reactions`;
- um PR de +18.420/-9.310, um ator de spam e uma mensagem de golpe no topo por
reação: os quatro casos que motivaram a curadoria existir.
Datas sempre via Carbon, nunca strings ISO-8601 misturadas: o Postgres de dev
roda em America/Sao_Paulo e o Eloquent grava sem offset, então misturar os dois
estilos apartaria os dados em 3h e jogaria linhas fora do recorte na borda.
Testes prendem o que falha calado: o picker tem de oferecer de volta exatamente
as iscas que a edição curada já esconde (uma janela deslocada em um mês abre o
playground vazio sem erro nenhum), e o primeiro teste passa pelo DatabaseSeeder
inteiro, único caminho que exercita a convivência com o BaseSeeder — que cria
`danielhe4rt` antes, com o mesmo username que a lista de membros reivindica.
…e 3) (#474) Fecha a **Fase 3** da retrospectiva multi-fonte. Dois commits, cada um verde por si só: contrato de curadoria (parte 1) e o Deck Builder que o consome (parte 2). ## Problema A Fase 2 entregou um CRUD Filament completo **em capacidade**: dava para ordenar fontes, ligar e desligar blocos, escrever os textos, listar exclusions e publicar. O que ele não dava era **noção do resultado** — o operador editava um repeater de linhas e só descobria o que fez abrindo o preview em outra aba, sem relação visual entre o campo que mexeu e o slide que mudou. Ao implementar a curadoria apareceu um buraco herdado: `SourceFilters::excludes()` existia desde a Fase 1 e o `deck_config` já gravava os refs, mas **nenhuma fonte chamava o método**. Exclusion era campo morto. Um picker em cima disso seria UI para um botão que não faz nada. ## Solução ### Curadoria entra por interface segregada `CuratableSource` (`slideCatalog()` + `exclusionCandidates(Period)`) no `community`, implementada por `GithubSource` e `DiscordSource`. O `RetrospectiveSource` **não muda** — é o crescimento por adição de interface que o ADR-0001 já previa. O builder checa `instanceof`: fonte que não cura aparece na timeline com ordem e on/off, sem catálogo de slides nem picker, e o deck segue montando. `slideCatalog()` é estático, resolvido sem tocar o banco. `exclusionCandidates()` varre dado, então é obrigação da implementação escopar pelo `Period`, aplicar `LIMIT` (30 no GitHub, 20 no Discord) e cachear por `(fonte, período)`. ### Exclusion passa a valer de verdade Cada fonte aplica os refs dentro do `collect()`, **antes de qualquer agregação**: o que é excluído some dos slides e também dos números. Não é capacidade nova da Fase 3, é a Fase 1 sendo completada — o ADR-0001 já definia exclusion como filtro que mexe no dado. Consequência editorial que a UI diz em voz alta: **mexer em exclusion exige republicar**, porque recompila o snapshot. Ordem e on/off não, esses re-derivam. Os refs são namespaced por prefixo (`pr:`, `issue:`, `actor:` no GitHub; `message:`, `member:` no Discord). `DeckConfig::allExclusions()` achata tudo numa lista só antes de virar `SourceFilters`, então o prefixo distinto é o que faz cada fonte reconhecer apenas o que emite — sem disputa de ref e sem tabela de tradução. ### O builder substitui a página de edição `EditRetrospective`, `RetrospectiveForm` e `DeckConfigForm` saem; entra uma `Page` de resource registrada na chave `edit` com rota `/{record}/deck`. A chave preserva o clique na tabela e o `getUrl('edit')`; a rota deixa a URL honesta. `List` e `Create` continuam padrão — criar uma edição é preencher título e período, não montar deck. Duas telas editando o mesmo `deck_config` seriam duas fontes de verdade de curadoria, com risco de uma sobrescrever a outra. ``` ┌──────────────────┬───────────────────────────┬───────────────────────┐ │ [Estrutura] │ [Preview] │ [Inspector] │ │ capa │ iframe da rota de │ formulário do que │ │ blocos de fonte │ preview da Fase 2 │ está selecionado │ │ chips de slide │ (mesmo ComposeDeck) │ 4 modos │ │ fecho │ │ │ └──────────────────┴───────────────────────────┴───────────────────────┘ seleciona só leitura edita e salva ``` O inspector é contextual e cada modo escreve **onde a Fase 2 já escrevia**: | Seleção | Edita | Persiste em | | --- | --- | --- | | Capa | título, período, ocultar bots, título e introdução da capa | colunas da edição | | Bloco (fonte) | exibir, exclusions daquela fonte | `hidden_sources`, `exclusions`, `order` | | Slide | exibir | `hidden_slides` | | Fecho | mensagem de fecho | coluna `closing_text` | **Nenhuma coluna nova, nenhuma migration**: o `DeckConfig` da Fase 2 já tinha `hidden_slides` persistindo sem UI que o editasse. ### O preview é um iframe da rota pública de preview Nada de reimplementar o deck dentro do painel. O centro aponta para `/comunidade/retrospectiva/{id}/preview`, a mesma rota que o operador já abria em outra aba, que passa pelo mesmo `ComposeDeck` da página pública. Preview que mente é pior que preview nenhum, e a única garantia de que ele não mente é ser literalmente a mesma coisa. Custo aceito: o iframe recarrega inteiro ao salvar, em vez de atualizar o slide alterado no lugar. O buster é `?v={updated_at}-{contador}` — só o `updated_at` não bastaria, porque dois salvamentos no mesmo segundo dariam o mesmo token. ### Decisões de escopo (adiadas por escolha, não esquecidas) - **Reordenar por botões, não por drag.** DnD exigiria dependência de frontend nova (o SortableJS que o Filament usa é interno, não é API pública) para ordenar entre 2 e 5 blocos. Fica como incremento posterior, sem mexer no formato persistido. - **On/off é por kind, não por instância de slide.** `github.repos` rende um slide por repositório; o toggle esconde o bloco inteiro. Ligar e desligar repo a repo exigiria identidade estável por instância, que o snapshot congelado não carrega. - **Não editamos cada slide** (título, máximo de itens, ordenação interna): obrigaria o `ComposeDeck` a conhecer a semântica de cada kind. ### Detalhe de correção que vale destacar O picker mostra o **topo** do recorte, nunca a tabela inteira. Um ref já excluído que caiu fora do teto da varredura não aparece em opção nenhuma — então `ExclusionPicker::orphans()` o isola e o salvamento o reescreve. A UI não pode derrubar por omissão aquilo que não consegue exibir. ## Arquivos Alterados **Contrato (community):** - `Retrospective/Contracts/CuratableSource.php` — interface segregada - `Retrospective/DTOs/{SlideDescriptor,ExclusionCandidate}.php`, `Enums/ExclusionKind.php` - `Retrospective/DTOs/DeckConfig.php` — `with*` imutáveis (fonte, slide, ordem, exclusions por fonte) - `Retrospective/DTOs/Period.php` — `cacheKey()` para cachear as varreduras por recorte **Fontes:** `integration-github/.../GithubSource.php` e `activity/.../DiscordSource.php` — implementam `CuratableSource` e aplicam exclusions dentro do `collect()`. **Builder (panel-admin):** - `Pages/BuildDeck.php` + `resources/views/retrospective/build-deck.blade.php` - `Support/DeckStructure.php` — timeline pelo mesmo `position()` que o `ComposeDeck` usa - `Support/{InspectorMode,InspectorSelection}.php` — tipam a seleção que viaja pela wire - `Support/ExclusionPicker.php` — agrupa candidatos por kind, isola órfãos - removidos: `EditRetrospective`, `RetrospectiveForm`, `DeckConfigForm` **Docs:** ADR-0002 do `panel-admin` (novo), ADR-0001 e spec do `community` atualizados (`CuratableSource` saiu de "futuro" para "implementado"), glossário do `CONTEXT.md` do `panel-admin`. ## Testes `BuildDeckTest` (17 casos, feature): - `o builder atende na chave edit com a rota /deck` — a chave e a URL, e a página responde 200 - `abre com a timeline das fontes e o iframe do preview` - `desliga e religa uma fonte pelo inspector` — ida e volta, incluindo o estado pré-preenchido - `desliga um kind de slide sem tocar os outros kinds da fonte` - `sobe e desce um bloco na ordem editorial` - `reordenar não mexe em on/off nem em exclusions` — a garantia de que os eixos são independentes - `o picker oferece os candidatos que a fonte varreu no recorte` - `salva no deck_config as exclusions escolhidas no picker` - `preserva refs já excluídos que ficaram fora do teto do picker` — o caso dos órfãos - `avisa que exclusion exige republicar, e só quando ela muda` — cobre os dois lados - `a fonte que não cura entra na timeline com on/off, mas sem picker` — via duplo em `tests/Support` - `salva capa e período nas colunas da edição`, `a capa exige título`, `salva a mensagem de fecho` - `publicar pelo builder marca publicando e enfileira o job`, `apagar pelo builder volta para a lista` - `o preview fura cache com a versão do registro` `DeckStructureTest` (6, unit): ordem curada com fontes desconhecidas no fim, projeção de on/off, catálogo só de quem cura, fonte crua aceita, deslocamento e recusa nas pontas. `DeckConfigTest` (+7): cada `with*` imutável, incluindo não duplicar o já escondido, remover a chave da fonte quando a lista esvazia e normalizar refs (sem vazios nem repetidos). `GithubSourceTest` / `DiscordSourceTest` (+): catálogo estático, candidatos escopados pelo período e exclusion derrubando item e número. ### Verificação `rector --dry-run`, `pint --test` e `phpstan` limpos. Suíte completa: **958 de 961 passando**. As 3 falhas são **pré-existentes** nesta branch de integração e não vêm deste trabalho — `integration-github/tests/Feature/BackfillRepositoryTest.php`, todas por offset de timezone (`+00:00` esperado vs `-03:00` obtido) em asserções de `occurred_at`. Confirmado rodando o arquivo com as mudanças deste PR guardadas: mesmas 3 falhas, mesmas linhas.
…multi-source # Conflicts: # app-modules/integration-github/src/Retrospective/CommunityRetrospectivePage.php # app-modules/integration-github/src/Retrospective/GithubSource.php # app-modules/panel-admin/src/PanelAdminServiceProvider.php # app-modules/portal/src/PortalServiceProvider.php # app-modules/portal/tests/Feature/CommunityRetrospectivePageTest.php # app-modules/portal/tests/Feature/CommunityRetrospectiveTest.php # database/seeders/DatabaseSeeder.php
O slide de voz mostrava só participantes, XP e uma lista de canais por evento — um recorte estreito de um dado que a comunidade vive todo dia. Agora ele carrega o recorte inteiro: totais (pessoas em call, entradas, XP, quem tirou XP e o dia de pico), as arenas por entrada com as salas temporárias de mesmo nome agrupadas (×N), quem mais viveu no voice e o histograma de entradas por hora no fuso de exibição. - topVoiceChannels agora ordena por entradas (state = 'joined') e conta COUNT(DISTINCT channel_id) para revelar as salas repetidas - voiceTotals resolve participantes, entradas, XP e ganhadores numa query - peakVoiceDay, topVoicePeople e voiceByHour (24 posições, sempre) - o histograma agrupa por posição, não pelo mesmo expressão com binding repetido: dois placeholders com o mesmo valor não são iguais para o Postgres e quebravam com "must appear in the GROUP BY clause" - a view ganha a variante compacta (.slide-inner.is-dense) para tudo caber sem rolagem, e defaults para snapshots congelados antes destes campos
O deck montava o nome da view com str_replace inline e o Deck Builder precisava do mesmo caminho para dizer ao operador qual arquivo abrir. Dois lugares derivando a mesma convenção é convite para discordarem. SlideView passa a ser o único dono: kind() para o partial do slide, cover() e closing() para os fixos, e path() resolvendo o arquivo pelo finder do Blade — devolve null quando o partial não existe, em vez de um caminho que mente (snapshot congelado antes da view existir, ou view renomeada depois). InspectorViewPath traduz a seleção do inspector para esse caminho. Bloco de fonte devolve null de propósito: fonte não tem view, ela emite slides. Um teste percorre o catálogo de todas as fontes registradas e falha se algum kind ficar sem partial.
… e caminho da view O Deck Builder abandona o iframe: o deck renderiza no próprio DOM do painel pelo MESMO render path da página pública (DeckPresentation), dentro de um island do Livewire — os renders do inspector não morfam o deck e a navegação por dentro dele (retro-moved) move a seleção da estrutura junto. A tira de miniaturas (filmstrip) mostra o deck de verdade em escala no rodapé, com on/off e ordem por fonte. As ações da tira e do deck despacham eventos que borbulham para FORA dos islands antes de virar chamada Livewire: uma ação disparada de dentro vira chamada escopada ao island, o inspector não atualiza no mesmo roundtrip e cada clique pagaria o re-render da tira inteira — teste de regressão impede o wire:click de voltar para dentro dos islands. O cabeçalho do preview mostra o arquivo da view do slide selecionado (SlideView, dona única da convenção kind -> partial) com botão de copiar, encurtando o caminho entre "não gostei disso" e o editor aberto no arquivo certo. ADR-0002 emendado: o preview divide o render path com a página pública; a alternativa "renderizar no painel" passa a ser a aceita.
`.retro-thumb` declarava `--retro-thumb-width` em si mesma, então a miniatura era imune ao container e cada tamanho novo viraria uma classe nova. Agora a variável é lida com fallback (`var(--retro-thumb-width, 208)`) e quem monta a tira a define uma vez no container: todas as células acompanham, inclusive rótulos e o cartão de "sem dado". O padrão de 208px não muda.
O builder é uma ferramenta, não um documento: rolar a página para alcançar a tira ou o inspector era fricção paga a cada slide. Agora as três áreas dividem uma altura fixa e cada uma rola por dentro. O que devolveu altura: - subheading da página e as Sections de "Preview" e "Estrutura" saíram; no lugar do cabeçalho do preview entra uma barra de uma linha que diz onde o operador está (fonte / slide, posição no deck) e qual arquivo desenha aquilo; - Sections do inspector em `->compact()`; - miniaturas em 164px, herdando `--retro-thumb-width` da tira; - o deck troca `58vh` fixo por `h-full` e recebe o que sobra da linha do grid. Preview, barra e tira compartilham `--builder-max-width`, declarado uma vez no grid: as três terminam no mesmo x, e ajustar é mudar um número só. Dois defeitos corrigidos no caminho: - a tira era um item de grid sem `min-w-0` num nível intermediário, então não encolhia abaixo do conteúdo (~2600px de miniaturas), estourava a coluna e empurrava deck e inspector para fora do viewport; - `DeckFilmstrip` usava `blank($indices[$queue])`, que avalia o índice antes de testá-lo e disparava "Undefined array key" em todo slide desligado — o caso comum, não a exceção. Agora é `isset()`. O snapshot do deck deixa de ser `#[Computed]` e vira memo numa propriedade privada tipada: o cache do Computed só existe no acesso mágico, que a análise estática não enxerga (o retorno virava mixed) e que esconde a armadilha de chamar o método e pagar a coleta ao vivo de novo em silêncio.
O deck abria com a capa e caía direto em pull requests e mensagens. Quem chegava de fora via os números sem saber de quem eram; quem já era de dentro não via a comunidade em lugar nenhum. Três slides entre a capa e as fontes resolvem isso. Fica FORA do snapshot de propósito: quem a He4rt é não muda a cada recorte, e o snapshot existe para congelar o que muda. Nenhuma curadoria a desliga — ela é o contexto que faz o resto significar algo. AboutSection é a dona única da lista e da ordem, porque o Deck Builder conta por ela para saber em que índice cada slide caiu. A He4rt: a linha do tempo é desenhada como batimento e cada marco É um pico da onda. O path do SVG é gerado a partir dos marcos, não escrito à mão — um ano novo redesenha a onda sozinho. Os pontos são HTML por cima, porque a onda estica na horizontal e um <circle> viraria elipse. A timeline para no `until` da edição: uma retro de 2021 não anuncia o meetup de 2022. Iniciativas: o diagrama carrega dado. O raio da órbita é a raridade do encontro e a velocidade da volta é a frequência — o semanal gira por dentro e rápido, o que só acontece quando surge o tema orbita longe e devagar. A cor do ponto é a mesma do marcador do item, que é o que liga a figura à lista. Onde entrar: os canais saem de `he4rt.social_media`, a mesma fonte da página /redes — um link novo aparece no deck sem ninguém lembrar de vir editar aqui. O que este recorte mediu leva selo, que é o que amarra a apresentação aos slides seguintes em vez de deixar uma lista de links solta. Corrige de carona um defeito antigo das miniaturas: traço que só aparece ao ser desenhado ficava INVISÍVEL no filmstrip, onde nada anima. Valia para o ECG da capa também.
A seção sobre a He4rt empurra todo slide composto três casas para a direita. Sem isso o builder erra por três: clicar numa miniatura leva o preview para o vizinho, e o "4 / 12" da barra mente. O deslocamento passa a ter dono único (composedOffset), que pergunta a contagem ao portal — quem desenha a seção é quem sabe o tamanho dela. Os testes que fixavam o "+1" da capa na mão passaram a perguntar por ele: acrescentar um slide à seção não obriga mais a corrigir teste. InspectorMode ganha o caso que NÃO edita. Ele existe porque a tira precisa de um alvo para aqueles slides — sem modo próprio, clicar na miniatura mandaria o preview para a capa. O inspector só diz o que a seção é e por que não há campo ali, e o botão Salvar some: um botão que não escreve nada promete uma edição que não existe. O caminho do blade continua na barra do preview, que é o que o operador precisa para mexer na copy.
danielhe4rt
marked this pull request as ready for review
August 23, 2026 23:16
Os totais de voz saem de uma query base, e `first()` devolve stdClass: toda
propriedade dele é mixed, porque o driver não promete tipo nenhum — o Postgres
manda COUNT e SUM como string, e recorte sem linha manda null. `(int)` em cima
disso escondia a ausência de dado em vez de tratá-la.
Junto vinha uma armadilha: `(string) $row->day` num dia ilegível vira string
vazia, e `CarbonImmutable::parse('')` devolve AGORA. O painel anunciaria HOJE
como o dia mais movimentado do recorte. O dia agora precisa ser legível para
virar pico.
O teste novo mira o portão que tudo isso sustenta: só entra painel de voz no deck
se os agregados lerem participante de verdade. Lidos como qualquer outra coisa, o
slide entraria vazio, com um dia de pico que ninguém viveu.
gvieira18
approved these changes
Aug 23, 2026
Luisnadachi
approved these changes
Aug 23, 2026
|
lgtm |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
O que é isso
A retrospectiva é aquela página que conta o que a comunidade fez num período: quem participou, o que rolou, os destaques. É um "resumo do capítulo" da He4rt, feito para ser publicado e compartilhado.
Este é o PR guarda-chuva da feature: ele acompanha o trabalho inteiro. As entregas de verdade vêm em PRs menores, um de cada vez, apontando para esta branch.
O problema hoje
A retrospectiva atual só enxerga o GitHub: pull requests, código, quem contribuiu nos repositórios. Só que a comunidade é muito mais que isso. A vida acontece no Discord (conversas, tempo em call, reações, gente nova chegando), no WhatsApp e em outras plataformas, e nada disso aparece hoje.
O que esta feature entrega
Como vai ser entregue (em fases, uma por vez)
Para quem quiser o detalhe técnico
O desenho completo, com as decisões e os porquês, está documentado no módulo
community:app-modules/community/docs/specs/2026-07-19-retrospectiva-multi-fonte.mdapp-modules/community/docs/adr/0001-retrospectiva-multi-fonte-via-contrato-de-source.mdapp-modules/community/docs/adr/0002-retrospectiva-persistida-com-snapshot-ao-publicar.md