amostra 03/em produção
ChecklistQA
Uma extensão que precisou sair de dentro do Google Meet para funcionar em ambientes corporativos.
- MV3
- JS
- Panel
01
Contexto
Em uma call de vendas é fácil esquecer um passo do roteiro, e anotar em outro lugar desvia a atenção de quem está falando. O ChecklistQA mantém o roteiro e as anotações ao lado da reunião.
A regra desde o início foi não coletar nada: o conteúdo da reunião nunca é lido, e nada sai do navegador.
02
Ponto de partida
A primeira versão injetava o checklist direto na página do Google Meet.
10
itens no roteiro original
3
etapas da call: início, meio e fim
0
dados coletados ou enviados
03
Linha do tempo
12 commits no histórico do projeto. Estes são os marcos.
29 de ago. de 2026
Primeira versão
Extensão Manifest V3 que exibia o checklist dentro do Meet, isolado em Shadow DOM, com política de privacidade e página da loja.
31 de ago. de 2026
Barrada no ambiente corporativo
Em máquinas com política de segurança corporativa, a CSP estrita e o Trusted Types impediam o painel de ser montado dentro do Meet. A extensão foi rearquitetada para rodar numa página própria, no painel lateral do Chrome.
07 de set. de 2026
Tópicos da chamada
Versão 1.2.0 com um bloco de pauta no topo do checklist, que cresce conforme o texto.
04
Mapa de decisões
Reconstruído a partir do histórico de commits, do README e da documentação do projeto.
01
Zero coleta como requisito
Nada é gravado nem enviado. O estado vive só na memória do painel e some ao recarregar, o que simplifica a privacidade e a aprovação na loja.
02
Sair de dentro do Meet
Injetar no Meet funcionava em casa e falhava em máquinas corporativas. Em vez de contornar a política de segurança, o painel passou a rodar numa página da própria extensão, que tem a sua própria CSP.
03
Detectar não é abrir
O Chrome só abre o painel lateral com um gesto do usuário. Por isso o detector apenas acende um aviso no ícone, e o painel abre com um clique ou com Alt+Shift+C.
04
Detector só de leitura
O script que roda no Meet só olha o endereço da aba. Não toca na página, não lê a reunião e não faz nenhuma requisição.
05
Texto do usuário nunca vira HTML
A lista é montada a partir de itens fixos do próprio código. O que a pessoa digita é tratado só como texto, então uma anotação não tem como executar nada.
06
Um painel que fica aberto
Uma vez aberto, o painel lateral acompanha a troca de abas e de calls. Basta abri-lo uma vez no começo do dia.
05
Arquitetura
As mesmas cinco camadas desta página, aplicadas ao projeto.
000 m/interface
- Painel lateral nativo do Chrome
- Checklist em três etapas e pauta
- Anotações com cópia
030 m/rota
- Clique no ícone abre o painel
- Atalho Alt+Shift+C
060 m/serviço
- Service worker
- Aviso por aba ao entrar numa call
- Detector só de leitura no Meet
120 m/persistência
- Nenhuma: o estado vive só na memória
06
Código
Trechos reais do repositório público.
Um detector que só lê o endereço
Esse é todo o código que roda dentro do Meet. Ele compara o endereço da aba com o formato de uma sala de call e avisa o service worker. Nenhum elemento da página é lido ou alterado.
(function () {
'use strict';
// Código de call do Meet: 3-4-3 letras (ex.: abc-defg-hij)
const CALL_CODE_RE = /^\/[a-z]{3}-[a-z]{4}-[a-z]{3}$/i;
let lastHref = null;
let lastInCall = null;
function report() {
const inCall = CALL_CODE_RE.test(location.pathname);
if (inCall === lastInCall) return;
lastInCall = inCall;
// sendMessage pode falhar se o service worker estiver dormindo; ignoramos.
try {
chrome.runtime.sendMessage({ type: 'meet-call-state', inCall });
} catch (_) { /* no-op */ }
}
report();
// O Meet troca de URL sem recarregar (SPA); poll leve pega entrada/saída da call.
setInterval(() => {
if (location.href !== lastHref) {
lastHref = location.href;
report();
}
}, 1000);
})();Abrir com gesto, avisar sem gesto
Uma tecla conta como gesto do usuário, então o atalho pode abrir o painel. A entrada numa call não conta, então ela só acende o aviso no ícone daquela aba.
// (2) Atalho de teclado abre o painel na janela em foco.
chrome.commands.onCommand.addListener(async (command) => {
if (command !== 'open-checklist') return;
try {
const win = await chrome.windows.getLastFocused();
await chrome.sidePanel.open({ windowId: win.id });
} catch (err) {
console.error('Falha ao abrir painel pelo atalho:', err);
}
});
// (3) Badge de "entrou na call", por aba.
chrome.runtime.onMessage.addListener((msg, sender) => {
if (!msg || msg.type !== 'meet-call-state') return;
const tabId = sender.tab && sender.tab.id;
if (tabId == null) return;
if (msg.inCall) {
chrome.action.setBadgeBackgroundColor({ tabId, color: BADGE_COLOR });
chrome.action.setBadgeText({ tabId, text: BADGE_TEXT });
} else {
chrome.action.setBadgeText({ tabId, text: '' });
}
});Permissões mínimas
Trecho do manifesto: uma permissão de API, a do painel lateral, e um script restrito às páginas do Meet. Sem acesso a abas, histórico, armazenamento ou rede.
{
"manifest_version": 3,
"permissions": ["sidePanel"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://meet.google.com/*"],
"js": ["meet-detect.js"],
"run_at": "document_idle"
}
],
"side_panel": {
"default_path": "panel.html"
}
}07
Resultado
1
permissão de API: a do painel lateral
0
dados coletados ou enviados
0
dependências externas
12
itens no roteiro atual
Estado atual
Publicada na Chrome Web Store.