codetac 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codetac",
3
- "version": "0.1.0",
4
- "description": "codeTAC — perceber o que o código da sua app Node faz, ação a ação: do clique no browser às funções do servidor, base de dados e serviços externos.",
3
+ "version": "0.2.0",
4
+ "description": "codeTAC — perceber o que o código da sua app Node ou Python faz, ação a ação: do clique no browser às funções do servidor, base de dados e serviços externos.",
5
5
  "keywords": [
6
6
  "debug",
7
7
  "trace",
@@ -10,6 +10,9 @@
10
10
  "nextjs",
11
11
  "vite",
12
12
  "express",
13
+ "python",
14
+ "fastapi",
15
+ "flask",
13
16
  "ai-generated",
14
17
  "vibe-coding",
15
18
  "dossier"
@@ -29,7 +32,9 @@
29
32
  "codetac": "src/cli.mjs"
30
33
  },
31
34
  "files": [
32
- "src/"
35
+ "src/",
36
+ "!**/__pycache__/",
37
+ "!**/*.pyc"
33
38
  ],
34
39
  "engines": {
35
40
  "node": ">=24.0.0"
@@ -37,10 +42,10 @@
37
42
  "scripts": {
38
43
  "test": "node --test test/*.test.mjs",
39
44
  "test:stacks": "node scripts/validate-stacks.mjs",
40
- "check": "node --check src/register.mjs && node --check src/transform.mjs && node --check src/runtime.mjs && node --check src/boundaries.mjs && node --check src/store.mjs && node --check src/panel.mjs && node --check src/page.mjs && node --check src/origins.mjs && node --check src/action-view.mjs && node --check src/browser/bar.js && node --check src/digest.mjs && node --check src/sentences.mjs && node --check src/ai.mjs && node --check src/cli.mjs && node --check src/detect.mjs && node --check src/recording.mjs && node --check src/diagnose.mjs",
45
+ "check": "node --check src/register.mjs && node --check src/transform.mjs && node --check src/runtime.mjs && node --check src/boundaries.mjs && node --check src/store.mjs && node --check src/panel.mjs && node --check src/page.mjs && node --check src/origins.mjs && node --check src/action-view.mjs && node --check src/browser/bar.js && node --check src/digest.mjs && node --check src/sentences.mjs && node --check src/ai.mjs && node --check src/cli.mjs && node --check src/detect.mjs && node --check src/detect-python.mjs && node --check src/recording.mjs && node --check src/diagnose.mjs",
41
46
  "test:performance": "node scripts/benchmark-functions.mjs",
42
47
  "panel": "node src/panel.mjs",
43
- "test:browser": "node scripts/accept-browser.mjs docs/aceitacao-browser-exemplo.json",
48
+ "test:browser": "node scripts/accept-browser.mjs scripts/ensaios/browser-exemplo.json",
44
49
  "codetac": "node src/cli.mjs"
45
50
  },
46
51
  "dependencies": {
package/readme.md CHANGED
@@ -2,9 +2,15 @@
2
2
 
3
3
  **Perceber o que o código da sua app faz, ação a ação.**
4
4
 
5
- *See what your app's code does, one click at a time: a local tool for Node.js apps, including those generated by AI tools (Lovable, Bolt, v0, Cursor, Replit, Claude Code…). Explanations are in Portuguese by default (`CODETAC_LANG=en` for English).*
5
+ *See what your app's code does, one click at a time: a local tool for Node.js and Python (FastAPI, Flask) apps, including those generated by AI tools (Lovable, Bolt, v0, Cursor, Replit, Claude Code…). Explanations are in Portuguese by default (`CODETAC_LANG=en` for English).*
6
6
 
7
- Clica num botão da sua app e o codeTAC mostra um **dossiê** dessa ação:
7
+ ![Dossier de um clique no botão «Sign in»: as funções do servidor que correram, com o ficheiro e a linha, e as leituras da base de dados](https://raw.githubusercontent.com/deltaxmodules/codetacvibe/main/.github/imagens/dossier.png)
8
+
9
+ ## Para quê
10
+
11
+ A sua app foi feita por uma IA, ou por outra pessoa, e não sabe ao certo o que acontece quando carrega em «Guardar»? Que funções correm, que tabela é alterada, se sai um email, se o pagamento está em modo de teste ou de produção?
12
+
13
+ Carregue num botão da sua app e o codeTAC mostra um **dossier** dessa ação:
8
14
  - **no browser:** o que foi clicado, o componente e onde está no código;
9
15
  - **no servidor:** as funções do seu projeto que correram, pela ordem, com o ficheiro e a linha;
10
16
  - **as fronteiras:** base de dados, serviços externos, email, pagamentos, ficheiros, IA;
@@ -12,8 +18,53 @@ Clica num botão da sua app e o codeTAC mostra um **dossiê** dessa ação:
12
18
  - **os efeitos permanentes:** linhas gravadas, emails enviados, cookies…;
13
19
  - uma frase simples em cada passo a dizer para que serve.
14
20
 
21
+ Em qualquer passo, pode abrir o código dessa função e fazer uma pergunta sobre ele.
22
+
15
23
  Tudo corre no seu computador. O código da app não é alterado.
16
24
 
25
+ ## Para que apps serve
26
+
27
+ **Apps web em Node.js (JavaScript ou TypeScript) ou em Python (FastAPI ou Flask), a correr no seu computador.** Incluindo as feitas com Lovable, Bolt, v0, Cursor, Replit ou Claude Code.
28
+
29
+ **Node.js — ensaiado com:**
30
+ - Next.js (incluindo a versão 16);
31
+ - Vite + React;
32
+ - Express;
33
+ - TanStack Start;
34
+ - projetos com frontend e backend separados, e monorepos (npm, pnpm, yarn).
35
+
36
+ Outras apps Node devem funcionar (Nuxt, SvelteKit, Astro, Remix, NestJS, Fastify…), mas ainda não foram ensaiadas. Se algo falhar, veja a [secção 6](#6-quando-algo-não-funciona).
37
+
38
+ **Python — ensaiado com:**
39
+ - FastAPI (funções `async def` e `def`), com o uvicorn, o `fastapi dev` ou o gunicorn;
40
+ - Flask, com o `flask run`, o `app.run()`, o gunicorn ou o waitress, incluindo os templates Jinja;
41
+ - um frontend Node (por exemplo Vite + React) com uma API Python na mesma pasta ou numa subpasta (`backend/`, `api/`…): arrancam juntos, e o dossier segue o clique do browser até às funções Python;
42
+ - projetos gerados pelo Claude Code, Replit Agent, Lovable, Codex e Cursor.
43
+
44
+ Para seguir as funções da app, precisa do **Python 3.12 ou mais recente**. Com o Python 3.8 a 3.11, o codeTAC funciona em **modo mínimo**: mostra os pedidos, as fronteiras e o browser, mas não as funções. Com o hypercorn ou o granian, a app corre, mas as ações não ficam com dossier (o comando avisa e sugere o uvicorn).
45
+
46
+ **Mostra à parte quando a app usa:**
47
+
48
+ | | |
49
+ | --- | --- |
50
+ | Base de dados | Postgres (`pg`), MySQL (`mysql2`), SQLite (`better-sqlite3`, `node:sqlite`), Supabase (com a tabela e a operação). Em Python: `sqlite3`, `psycopg2`, `psycopg`, `asyncpg`, `pymysql` e o SQLAlchemy |
51
+ | IA | OpenAI, Anthropic, Google, Mistral, Groq, OpenRouter, DeepSeek, Cohere, Together, modelos locais |
52
+ | Email e mensagens | nodemailer, Resend, SendGrid, Postmark, Mailgun, Brevo, Twilio |
53
+ | Pagamentos | Stripe, com a indicação de modo de teste ou de produção |
54
+ | Autenticação | NextAuth, Supabase Auth, Clerk |
55
+ | Ficheiros | disco do computador, S3, Supabase Storage |
56
+ | Outros | qualquer pedido do servidor a um serviço externo, cookies |
57
+
58
+ Os serviços chamados diretamente do browser (por exemplo o Supabase, em muitas apps do Lovable e do Bolt) aparecem como pedidos do browser, sem o passo a passo do servidor.
59
+
60
+ **Ainda não serve para:**
61
+ - Django, nem apps em PHP, Go ou Ruby (Laravel, Rails…);
62
+ - sites só com HTML e JavaScript, sem um servidor Node ou Python;
63
+ - partes que correm em Bun, Deno, Cloudflare Workers ou no middleware do Next.js;
64
+ - apps em produção ou que só existem na nuvem.
65
+
66
+ Precisa do Node.js 24 ou mais recente, também nas apps Python (o codeTAC é instalado pelo npm). Ensaiado em macOS; o Windows ainda não foi ensaiado.
67
+
17
68
  ---
18
69
 
19
70
  ## 1. Antes de começar
@@ -25,6 +76,7 @@ Precisa de três coisas:
25
76
  - no GitHub, **Code → Download ZIP**, e descompacte o ficheiro;
26
77
  - ou `git clone <endereço>`, se usa o Git.
27
78
  3. **Um browser:** Chrome, Edge, Firefox ou Safari.
79
+ 4. **Só nas apps Python: o Python 3.12 ou mais recente.** Para verificar, escreva `python3 --version`. Se não tiver, instale-o em [python.org](https://www.python.org/downloads/) (no macOS, também `brew install python`). Se tiver o [uv](https://docs.astral.sh/uv/), o codeTAC usa-o, e ele descarrega o Python que faltar.
28
80
 
29
81
  **Como abrir o Terminal:**
30
82
  - **macOS:** Cmd+Espaço, escreva «Terminal», Enter;
@@ -42,7 +94,7 @@ Confirme com `codetac --version`, que mostra o número da versão.
42
94
 
43
95
  Se aparecer um erro de permissões (`EACCES`), use `npx codetac` em vez de `codetac` nos passos abaixo. Funciona sem instalar.
44
96
 
45
- ## 3. O primeiro dossiê
97
+ ## 3. O primeiro dossier
46
98
 
47
99
  1. No Terminal, vá para a pasta do projeto: escreva `cd ` (com um espaço), arraste a pasta do projeto para a janela do Terminal e carregue em Enter.
48
100
  2. Escreva:
@@ -53,17 +105,19 @@ Se aparecer um erro de permissões (`EACCES`), use `npx codetac` em vez de `code
53
105
 
54
106
  3. O codeTAC diz o que vai arrancar, por exemplo `Arranque: Vite · npm run dev`.
55
107
  - Se faltarem as dependências da app, pergunta se as instala. Carregue em **Enter**, que quer dizer sim.
108
+ - Numa app Python sem ambiente, pergunta se cria o `.venv` na pasta do projeto e instala as dependências (`requirements.txt`, `pyproject.toml` ou `uv.lock`). **Enter** quer dizer sim.
109
+ - Se o projeto tiver um `.env.example` e não um `.env`, avisa: muitas apps precisam dessa configuração (copie o ficheiro para `.env` e preencha-o).
56
110
  - Se o projeto tiver várias partes, pergunta quais arranca. **Enter** arranca todas.
57
111
  4. Quando aparecer **«✓ App pronta em http://localhost:…»**, o browser abre com a sua app.
58
112
  5. **Use a app:** clique num botão, numa ligação, envie um formulário.
59
113
  - No canto inferior direito da página aparece uma pequena barra com «Gravada: …».
60
- - No Terminal aparece «✓ Primeiro dossiê …» com um link.
61
- 6. Carregue na barra, ou abra o link, para ver o dossiê.
114
+ - No Terminal aparece «✓ Primeiro dossier …» com um link.
115
+ 6. Carregue na barra, ou abra o link, para ver o dossier.
62
116
  7. Para terminar, volte ao Terminal e carregue em **Ctrl+C**.
63
117
 
64
- Numa app que já conhece, o primeiro dossiê chega normalmente em menos de um minuto.
118
+ Numa app que já conhece, o primeiro dossier chega normalmente em menos de um minuto.
65
119
 
66
- ## 4. Ler o dossiê
120
+ ## 4. Ler o dossier
67
121
 
68
122
  - **A frase do topo** resume a ação, por exemplo: «Clique em botão «Add» → POST /api/items (estado 201) → altera o ecrã».
69
123
  - **browser → servidor:** cada pedido que a ação fez ao servidor. Por baixo estão as funções do seu projeto que correram.
@@ -105,10 +159,13 @@ Cada linha começa por **✓** (funciona), **!** (atenção) ou **✗** (problem
105
159
 
106
160
  | Mensagem | O que fazer |
107
161
  | --- | --- |
108
- | «Não encontrei um package.json…» | Está na pasta errada. Vá para a pasta que tem o ficheiro `package.json`, ou indique o arranque: `codetac -- node server.js` |
162
+ | «Não encontrei um package.json…» | Está na pasta errada. Vá para a pasta que tem o ficheiro `package.json` (ou, numa app Python, o `requirements.txt`, o `pyproject.toml` ou o `main.py`/`app.py`), ou indique o arranque: `codetac -- node server.js`, `codetac -- uvicorn main:app` |
109
163
  | «A porta 3000 já está ocupada por outro programa…» | Feche o outro programa, por exemplo outra app aberta noutra janela do Terminal. Numa app com uma só parte, o codeTAC muda de porta sozinho |
110
164
  | «A app falhou também em modo mínimo» | O problema é da própria app. Muitas vezes faltam as chaves no ficheiro `.env` (Supabase, Firebase…). Veja o `.env.example` ou as instruções do projeto |
111
- | «A página inicial respondeu com erro 500» | A app arrancou, mas falha. O dossiê desse carregamento mostra onde |
165
+ | «A página inicial respondeu com erro 500» | A app arrancou, mas falha. O dossier desse carregamento mostra onde |
166
+ | «…ocupada pelo Recetor AirPlay do macOS» | A porta 5000 do Flask é do AirPlay no macOS. O codeTAC arranca a app noutra porta; para usar a 5000, desligue o «Recetor AirPlay» em Definições do Sistema › Geral › AirDrop e Handoff |
167
+ | «Modo mínimo: Python …» | A app corre num Python anterior ao 3.12. Crie o `.venv` com um Python mais recente (apague o `.venv` e corra `codetac` outra vez) |
168
+ | «O hypercorn (ou o granian) ainda não é reconhecido» | Arranque a app com o uvicorn, por exemplo `codetac -- uvicorn main:app` |
112
169
  | A barra não aparece na página | Recarregue a página. Veja se o endereço é o que o codeTAC indicou |
113
170
 
114
171
  **Comunicar um problema:**
@@ -140,20 +197,30 @@ Guarda o diagnóstico num ficheiro, sem código nem dados da app. Envie-o com um
140
197
  | Opção | Para quê |
141
198
  | --- | --- |
142
199
  | `--script <nome>` | usar outro script do `package.json` |
200
+ | `--parte <pasta>` | num projeto com várias partes, qual arrancar (pode repetir) |
143
201
  | `--porta <n>` | a porta da app, se não for descoberta sozinha |
144
202
  | `--painel <n>` | a porta do painel (por omissão 4000) |
145
203
  | `--minimo` | não seguir as funções do projeto |
146
204
  | `--sim` | responder «sim» às perguntas |
147
205
  | `--nao-abrir` | não abrir o browser |
148
- | `-- <comando>` | o comando de arranque, por exemplo `codetac -- node server.js` |
206
+ | `-- <comando>` | o comando de arranque, por exemplo `codetac -- node server.js` ou `codetac -- uvicorn main:app --reload` |
149
207
 
150
208
  ## 9. Limites
151
209
 
152
- - Só apps **Node.js** (JavaScript e TypeScript) a correr **no seu computador**, em desenvolvimento.
153
- - Não serve para apps em produção nem só na nuvem.
154
- - Partes da app que não correm no Node não são observadas por dentro: Bun, Deno, Cloudflare Workers, o middleware do Next.js. O comando avisa quando as reconhece.
210
+ - Os tipos de app que servem e os que não servem estão em [Para que apps serve](#para-que-apps-serve).
211
+ - É para usar em desenvolvimento, no seu computador. Não serve para apps em produção.
212
+ - As partes que não correm no Node nem no Python não são observadas por dentro. O comando avisa quando as reconhece.
155
213
  - No browser, mostra-se o elemento, o componente e o caminho até cada pedido, mas não cada função.
156
- - Windows: ainda não ensaiado.
214
+
215
+ **Nas apps Python:**
216
+ - As funções só são seguidas com o Python 3.12 ou mais recente. Abaixo, modo mínimo: pedidos, fronteiras e browser.
217
+ - A app é encontrada quando o `FastAPI(...)` ou o `Flask(...)` (ou uma fábrica `create_app`) está num ficheiro `.py` até dois níveis de pastas abaixo da pasta indicada. Noutros casos, indique o arranque com `codetac -- <comando>`.
218
+ - A configuração da app (`.env`, base de dados, Redis…) é sua: sem ela, o codeTAC mostra o erro da própria app.
219
+ - As threads criadas à mão (`threading.Thread`) não ficam ligadas ao pedido que as criou; as do FastAPI e do `asyncio` ficam.
220
+ - O Redis, o MongoDB e o Celery ainda não aparecem como fronteiras: o tempo passado neles fica dentro da função que os chamou.
221
+ - As páginas enviadas aos pedaços (stream) não recebem a barra; a ação continua a ser gravada.
222
+ - Com muitas consultas por página, o servidor fica mais lento: numa página com 52 consultas SQL, passou de 1,0 para 2,8 ms. Nas outras apps ensaiadas, o tempo ficou entre 1 e 1,3 vezes o normal.
223
+ - MySQL pelo `mysqlclient` e o Django ainda não foram ensaiados.
157
224
 
158
225
  ## Licença
159
226
 
package/src/ai.mjs CHANGED
@@ -295,7 +295,7 @@ export async function answerQuestion({ config, dossier, stepId, question, readCo
295
295
  let step = null;
296
296
  let parent = null;
297
297
  walk(dossier.digest.nodes, (node, holder) => { if (node.id === stepId) { step = node; parent = holder; } });
298
- if (!step) return { available: true, known: false, text: 'Este passo não foi encontrado no dossiê.' };
298
+ if (!step) return { available: true, known: false, text: 'Este passo não foi encontrado no dossier.' };
299
299
  const target = step.type === 'function' ? step : parent?.type === 'function' ? parent : null;
300
300
  const code = target ? readCode(dossier.request.run, target.file, target.line, target.endLine) : null;
301
301
  const facts = target ? subtree(target) : { boundaries: [step], functions: [] };
@@ -326,7 +326,9 @@ export async function answerQuestion({ config, dossier, stepId, question, readCo
326
326
  const allowedWords = wordsOf(code?.lines.join('\n'), question, ...lines);
327
327
  // Answers may talk about other cases of the code, so conditionals are allowed here.
328
328
  const rejected = validateSentence(text, { boundaries: facts.boundaries, allowedWords, knownTables, knownHosts, maxLength: 1500, conditionals: true });
329
- return { available: true, known: Boolean(result?.sabe), text, rejected, model: config.model, local: Boolean(config.local), valuesSent: Boolean(detail && withValues) };
329
+ // With a model outside this machine, the answer says the recorded values were not sent.
330
+ return { available: true, known: Boolean(result?.sabe), text, rejected, model: config.model, local: Boolean(config.local),
331
+ valuesSent: Boolean(detail && withValues), valuesWithheld: Boolean(detail && !withValues) };
330
332
  }
331
333
 
332
334
  export function createPurposes({ config, cache, readCode, redact = createRedactor(), limit = Number(process.env.CODETAC_AI_LIMIT || 40) }) {
@@ -89,6 +89,18 @@ export function classifyHttp({ method, url, headers, body, client }) {
89
89
  return { ...base, kind: 'ia', provider: AI[host] ?? (local ? 'modelo local' : `compatível (${host})`), operation: path,
90
90
  local: local || undefined, model: typeof json?.model === 'string' ? json.model : undefined };
91
91
  }
92
+ // S3-compatible storage: signed header, or a presigned URL (signature in
93
+ // the query: SigV4, or SigV2, which boto3 still makes by default). Before
94
+ // the local case: a local MinIO or LocalStack is S3.
95
+ if (/^AWS4-HMAC-SHA256/.test(String(headers.authorization ?? '')) || headers['x-amz-content-sha256']
96
+ || queryKeys.some(key => /^x-amz-(signature|algorithm)$/i.test(key))
97
+ || (queryKeys.includes('AWSAccessKeyId') && queryKeys.includes('Signature'))) {
98
+ const virtualHost = host.match(/^(.+?)\.s3[.-]/);
99
+ const bucket = virtualHost ? virtualHost[1] : parsed.pathname.split('/')[1];
100
+ const operation = { GET: 'leitura', HEAD: 'verificação', PUT: 'escrita', POST: 'escrita', DELETE: 'remoção' }[method] ?? method;
101
+ const size = Number(headers['content-length']);
102
+ return { ...base, kind: 'ficheiros', provider: 'S3', operation, bucket, bytes: Number.isFinite(size) ? size : undefined };
103
+ }
92
104
  if (local) return { ...base, local: true };
93
105
  if (host === 'api.stripe.com') {
94
106
  const auth = String(headers.authorization ?? '');
@@ -115,15 +127,6 @@ export function classifyHttp({ method, url, headers, body, client }) {
115
127
  if (area === 'storage') return { ...base, kind: 'ficheiros', provider: 'Supabase Storage', operation: method, bucket: second === undefined ? first : second };
116
128
  }
117
129
  if (host === 'api.clerk.com' || host.endsWith('.clerk.accounts.dev')) return { ...base, kind: 'autenticação', provider: 'Clerk' };
118
- // S3-compatible storage: signed header, or a presigned URL (signature in the query).
119
- if (/^AWS4-HMAC-SHA256/.test(String(headers.authorization ?? '')) || headers['x-amz-content-sha256']
120
- || queryKeys.some(key => /^x-amz-(signature|algorithm)$/i.test(key))) {
121
- const virtualHost = host.match(/^(.+?)\.s3[.-]/);
122
- const bucket = virtualHost ? virtualHost[1] : parsed.pathname.split('/')[1];
123
- const operation = { GET: 'leitura', HEAD: 'verificação', PUT: 'escrita', POST: 'escrita', DELETE: 'remoção' }[method] ?? method;
124
- const size = Number(headers['content-length']);
125
- return { ...base, kind: 'ficheiros', provider: 'S3', operation, bucket, bytes: Number.isFinite(size) ? size : undefined };
126
- }
127
130
  return base;
128
131
  }
129
132
 
@@ -218,7 +218,9 @@
218
218
  if (leaving && navigator.sendBeacon) navigator.sendBeacon(ENDPOINT, new Blob([body], { type: 'text/plain' }));
219
219
  else originalFetch.call(window, ENDPOINT, { method: 'POST', body, keepalive: body.length < 60000, headers: { 'content-type': 'text/plain' } }).catch(() => {});
220
220
  } catch {}
221
- if (action.trigger || action.requests.length) {
221
+ // A continuation on a new page counts too: in server-rendered apps (a form
222
+ // that loads the next page) it is how the bar shows what brought you here.
223
+ if (action.trigger || action.requests.length || action.segment > 1) {
222
224
  recorded.push({ id: action.id, label: labelOf(action), requests: action.requests.length, closedBy });
223
225
  if (recorded.length > 30) recorded.shift();
224
226
  updateBar(true);
@@ -521,7 +523,7 @@
521
523
  '.top{display:flex;gap:6px;align-items:center;padding:6px 8px;background:#18181b;color:#f4f4f5}.top b{margin-right:auto}' +
522
524
  '.top button,.top a{all:unset;cursor:pointer;padding:2px 8px;border-radius:5px;color:#e4e4e7}.top button:hover,.top a:hover{background:#3f3f46}' +
523
525
  'iframe{border:0;flex:1;width:100%}.msg{padding:16px;color:#27272a}</style>' +
524
- '<button class="pill" part="pill" title="CodeTAC: carregue para ver o dossiê da última ação"><span class="dot"></span><span class="label">CodeTAC</span></button>';
526
+ '<button class="pill" part="pill" title="CodeTAC: carregue para ver o dossier da última ação"><span class="dot"></span><span class="label">CodeTAC</span></button>';
525
527
  shadow.querySelector('.pill').addEventListener('click', () => toggleSheet());
526
528
  document.documentElement.appendChild(host);
527
529
  updateBar(false);
@@ -562,7 +564,7 @@
562
564
  }
563
565
  shown.innerHTML = '<div class="top"><b></b><button data-go="-1" title="Ação anterior">◀</button><button data-go="1" title="Ação seguinte">▶</button>' +
564
566
  '<a target="_blank" rel="noopener" title="Abrir no painel">painel ↗</a><button data-close title="Fechar">✕</button></div>' +
565
- '<iframe title="Dossiê da ação"></iframe>';
567
+ '<iframe title="Dossier da ação"></iframe>';
566
568
  shown.querySelector('b').textContent = (index + 1) + '/' + recorded.length + ' · ' + item.label;
567
569
  shown.querySelector('a').href = panel + '/?action=' + encodeURIComponent(item.id);
568
570
  shown.querySelector('iframe').src = url;