Deck Builder da retrospectiva: 3 colunas, curadoria por contrato (Fase 3) - #474
Deck Builder da retrospectiva: 3 colunas, curadoria por contrato (Fase 3)#474Clintonrocha98 wants to merge 8 commits into
Conversation
…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.
Follow-up: 403 no preview + acabamento do builderTrês commits novos.
|
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.
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 aFase 1 e o
deck_configjá gravava os refs, mas nenhuma fonte chamava o método. Exclusion eracampo 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)) nocommunity, implementadapor
GithubSourceeDiscordSource. ORetrospectiveSourcenão muda — é o crescimento poradição de interface que o ADR-0001 já previa. O builder checa
instanceof: fonte que não curaaparece 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, aplicarLIMIT(30 no GitHub, 20 no Discord) ecachear 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ídosome 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:noDiscord).
DeckConfig::allExclusions()achata tudo numa lista só antes de virarSourceFilters,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,RetrospectiveFormeDeckConfigFormsaem; entra umaPagede resourceregistrada na chave
editcom rota/{record}/deck. A chave preserva o clique na tabela e ogetUrl('edit'); a rota deixa a URL honesta.ListeCreatecontinuam padrão — criar uma ediçãoé preencher título e período, não montar deck.
Duas telas editando o mesmo
deck_configseriam duas fontes de verdade de curadoria, com risco deuma sobrescrever a outra.
O inspector é contextual e cada modo escreve onde a Fase 2 já escrevia:
hidden_sources,exclusions,orderhidden_slidesclosing_textNenhuma coluna nova, nenhuma migration: o
DeckConfigda Fase 2 já tinhahidden_slidespersistindo 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, quepassa pelo mesmo
ComposeDeckda 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ó oupdated_atnão bastaria, porque dois salvamentosno mesmo segundo dariam o mesmo token.
Decisões de escopo (adiadas por escolha, não esquecidas)
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.
github.reposrende 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.
ComposeDecka 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 osalvamento 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 segregadaRetrospective/DTOs/{SlideDescriptor,ExclusionCandidate}.php,Enums/ExclusionKind.phpRetrospective/DTOs/DeckConfig.php—with*imutáveis (fonte, slide, ordem, exclusions por fonte)Retrospective/DTOs/Period.php—cacheKey()para cachear as varreduras por recorteFontes:
integration-github/.../GithubSource.phpeactivity/.../DiscordSource.php—implementam
CuratableSourcee aplicam exclusions dentro docollect().Builder (panel-admin):
Pages/BuildDeck.php+resources/views/retrospective/build-deck.blade.phpSupport/DeckStructure.php— timeline pelo mesmoposition()que oComposeDeckusaSupport/{InspectorMode,InspectorSelection}.php— tipam a seleção que viaja pela wireSupport/ExclusionPicker.php— agrupa candidatos por kind, isola órfãosEditRetrospective,RetrospectiveForm,DeckConfigFormDocs: ADR-0002 do
panel-admin(novo), ADR-0001 e spec docommunityatualizados(
CuratableSourcesaiu de "futuro" para "implementado"), glossário doCONTEXT.mddopanel-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 200abre com a timeline das fontes e o iframe do previewdesliga e religa uma fonte pelo inspector— ida e volta, incluindo o estado pré-preenchidodesliga um kind de slide sem tocar os outros kinds da fontesobe e desce um bloco na ordem editorialreordenar não mexe em on/off nem em exclusions— a garantia de que os eixos são independenteso picker oferece os candidatos que a fonte varreu no recortesalva no deck_config as exclusions escolhidas no pickerpreserva refs já excluídos que ficaram fora do teto do picker— o caso dos órfãosavisa que exclusion exige republicar, e só quando ela muda— cobre os dois ladosa fonte que não cura entra na timeline com on/off, mas sem picker— via duplo emtests/Supportsalva capa e período nas colunas da edição,a capa exige título,salva a mensagem de fechopublicar pelo builder marca publicando e enfileira o job,apagar pelo builder volta para a listao preview fura cache com a versão do registroDeckStructureTest(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): cadawith*imutável, incluindo não duplicar o já escondido, remover a chaveda fonte quando a lista esvazia e normalizar refs (sem vazios nem repetidos).
GithubSourceTest/DiscordSourceTest(+): catálogo estático, candidatos escopados pelo período eexclusion derrubando item e número.
Verificação
rector --dry-run,pint --testephpstanlimpos. 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:00esperado vs-03:00obtido) em asserções deoccurred_at. Confirmado rodando o arquivocom as mudanças deste PR guardadas: mesmas 3 falhas, mesmas linhas.