Skip to content

feat/codions tools platform - #1

Open
fabioassuncao wants to merge 61 commits into
mainfrom
feat/codions-tools-platform
Open

feat/codions tools platform#1
fabioassuncao wants to merge 61 commits into
mainfrom
feat/codions-tools-platform

Conversation

@fabioassuncao

Copy link
Copy Markdown
Member

No description provided.

The project had no tests, no lint and no type checking — the .vue files had
never been type-checked at all. Refactoring a CDN-distributed package on that
footing is guesswork, so this lands the safety net before any structural change.

- Vitest + jsdom, with three kinds of coverage:
  - golden snapshots of the generated snippet, which is the compatibility
    contract for every script tag already pasted into a third-party site;
  - runtime behaviour of the widget (render, presets, popup, destroy);
  - the CDN bundle contract: the exact dist path and both globals
    (FloatingContact and the legacy FloatingWhatsApp alias).
- tsconfig.base.json shared by both packages; vue-tsc for the SFCs. The old
  `*.vue` shim in env.d.ts was removed because it silently disabled component
  type checking.
- ESLint 9 flat config + Prettier; fixed every error it surfaced.
- CI workflow validating pull requests (there was none before).
- LICENSE, .nvmrc, engines, CHANGELOG; concurrently moved out of `npx` and
  into the lockfile so `npm run dev` is reproducible.

Two real bugs the snippet tests uncovered are pinned with `it.fails` and fixed
in the next commit: quotes in user text break out of the generated attribute,
and a `callback` action serializes its handler as a string.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
Formatting-only commit, kept separate so the structural changes that follow stay
readable. The two packages had drifted apart (the runtime used semicolons, the
site did not); this settles on one style before the directory move.

The snippet golden snapshots are unchanged, confirming no generated output moved.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
Four real defects the new tests exposed, fixed before any structural work so the
golden snapshots capture correct output from here on.

Snippet generator:
- No HTML escaping anywhere. A quote in user text — a label like "Bob's Chat", or
  an inline SVG icon full of double quotes — closed the data- attribute early and
  the pasted snippet was silently truncated in the customer's page.
- A `callback` action was serialized with its handler as a JSON *string*, which
  throws "handler is not a function" on first click. Those configs now generate the
  script + init() form, which can actually carry a function.
- The CDN URL was unpinned, so publishing a major would upgrade every site that had
  ever pasted the snippet. It now pins the minor (@0.1); patches still flow.

Runtime:
- Channel links went straight to window.open, so a `javascript:` URL in
  data-channels executed. data-channels is reachable by any CMS that interpolates
  user input, so URLs are now checked against a protocol allowlist; blocked ones
  warn instead of navigating. Also added noreferrer.

Playground preview:
- The message listener was never removed, messages were accepted from any window,
  and the iframe combined allow-scripts with allow-same-origin, which cancels the
  sandbox. The frame now has an opaque origin and is identified by event.source.
- A missing bundle used to render a blank frame with no signal; it now explains
  itself in an overlay.

Verified in a real browser: both snippet shapes render in a plain third-party page,
quotes survive, the javascript: channel is refused, and the playground preview still
works through the tightened sandbox.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
Pure rename, no logic changes. With a second tool coming, `packages/docs` was a
misleading name (it holds the whole playground) and `packages/lib` stops
identifying anything once there are several libraries.

  packages/docs -> apps/web
  packages/lib  -> packages/floating-contact-button

The rule this establishes: `packages/` is what ships to third-party sites,
`apps/` is what gets deployed. That makes "where does the next tool go?" answer
itself.

The npm package name is unchanged, so the jsDelivr URL and every pasted snippet
keep working. The Cloudflare worker name is also unchanged, so the deploy targets
the same worker.

Also removes `prepublishOnly: cp ../../README.md .` — the riskiest line in this
refactor. Once the root README describes the platform, that hook would have
published platform docs to npm in place of the tool's own docs. The package now
carries a versioned README (and LICENSE), and CI checks it stays that way.

Verified: clean `npm ci` from a regenerated lockfile, `npm pack --dry-run` lists
the same files with no runtime dependencies, `wrangler deploy --dry-run` resolves
the new assets directory, and build/typecheck/lint/tests all pass.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
Every element used to spell out both themes — `bg-slate-800 light:bg-slate-100`,
249 such pairs — because there were no design tokens. Adding a second tool to that
would have doubled it.

Tokens (apps/web/src/styles/theme.css) use Tailwind v4's `@theme inline`, so the
generated utilities reference the raw custom properties rather than snapshotting
them. One redefinition under `.light` now re-themes the whole app: `bg-card`,
`text-fg-muted`, `border-line`, `bg-accent`.

That also killed `bg-slate-850` and `bg-slate-750`, used in three places. Neither
exists in Tailwind, so they had been generating no CSS at all.

Primitives extracted only where the duplication was measured (3+ real uses):
UiInput (17), UiField (11), UiCodeBlock (12), UiSegmented (5), UiTabs (3),
UiColorInput (3), UiCard, UiButton, UiRange. Two exceptions worth naming:
- UiToggle has one use today, but it ships `role="switch"` + `aria-checked` that
  the inline markup lacked, and the consent categories will need several.
- No UiSelect: still a single `<select>` in the whole app.

useHighlighter() makes Shiki a page-level singleton. It used to be constructed per
component, so a docs page with a dozen code blocks meant a dozen Oniguruma WASM
engines.

GlobalOptions.vue: 275 -> 163 lines, its template 256 -> 98. ActionEditor 167 -> 108,
ChannelEditor 106 -> 69. CodeOutput 119 -> 61, having handed highlighting to the
shared composable.

DocsPage.vue still carries its 134 `light:` pairs; it gets split into per-tool docs
in the next commit and is migrated there rather than twice.

Verified: build, typecheck, lint, 29 tests, plus screenshots of the playground in
both themes confirming it renders as before.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
The site presented itself as one product. This makes the platform the subject and
the Floating Contact Button the first tool in it, with the structure sized so the
third tool is local work rather than another refactor.

Tool contract (src/tools/types.ts): metadata + an optional runtime + an optional
playground. Registration is explicit in src/tools/index.ts — no import.meta.glob, no
filename conventions, so grep finds every reference. Playgrounds and docs are dynamic
imports, so the home page does not pull in every tool's editor.

Playground is now tool-agnostic. PreviewFrame takes a script URL, a global name and
options; the srcdoc moved out of the SFC into buildPreviewSrcdoc.ts, where it can be
tested. Every runtime honours one contract — `window[global].init(options)` returns
`{ destroy() }` — which is what lets one preview component serve all tools. The
generator returns CodeSnippet[] instead of three fixed tabs.

Config state is scoped to the tool route (providePlayground) rather than provided
app-wide. It used to exist on the docs page too, which had no use for it.

Routes: /, /tools, /tools/:slug, /tools/:slug/playground, plus a real 404 (any
unknown path used to render an empty shell). /playground and /docs redirect to the
Floating Contact Button so existing links keep working. These are client-side
redirects and the 404 answers HTTP 200 — Workers Assets serves the SPA shell for
every path, and changing that needs a Worker in front of the assets. Recorded in the
code rather than glossed over.

i18n is namespaced per tool by composition, so `pt-BR` stays annotated with the `en`
shape and a missing key is still a compile error. useToolI18n(slug) scopes lookups.

DocsPage (573 lines) split into the tool's own FcbDocs / FcbLiveDemo / reference.ts,
moved verbatim apart from tokens and UiCodeBlock, so the diff stays reviewable. Its
134 `light:` pairs are gone — the app now has none.

Three defects the new tests and browser run caught:
- App.vue called useDocumentMeta() which injected i18n from the component that
  provides it; inject only looks at ancestors, so every route threw.
- FcbDocs kept calling the root t() after its keys moved into the tool namespace,
  rendering raw key names. The i18n coverage test now picks the translator based on
  which one the file imports, instead of accepting either — checking both forms is
  exactly what let this through the first time.
- A bad tool slug redirected to / instead of 404, because the catch-all route needs
  its param passed explicitly.

Verified in a browser: every route, both redirects, the 404, and the preview loading
the real runtime bundle through the sandboxed frame.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
The twelve built-in channels existed three times: in the runtime, in the site's
constants, and in the docs table — each with the brand SVGs pasted in by hand. Any
new channel meant editing all three, and a colour could drift silently.

The runtime's presets gain a `label` and are re-exported alongside CHANNEL_ICONS, so
the configuration UI and the documentation table both derive from the code that
actually applies them. 95 lines and twelve duplicated SVGs removed from the site.

Also reorders CI: runtimes build before typecheck, because the web app type-checks
against their generated .d.ts. Adding the re-exports and then type-checking against
a stale dist is exactly how that ordering announced itself.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
With a second runtime coming, the pieces both need were extracted from a real
consumer rather than guessed at: injectStyleOnce, escapeHtml, isSafeUrl,
insertTrustedSVG, a typed emitter, and the data-* dataset reader.

The point is less code reuse than a single runtime contract —
`window[global].init(options)` returning `{ destroy() }` — which is what lets one
playground preview component serve every tool.

The package is private and source-only: runtimes take it as a devDependency and
bundle it (`noExternal`), so published tarballs still declare zero runtime
dependencies. Verified with `npm pack --dry-run`. The risk here was tsup's dts
rollup refusing to resolve a package whose exports point at .ts; it resolves fine,
so the fallback of giving core its own build was not needed.

Two fixes this surfaced, both affecting live sites:

- **The whatsapp() shorthand broke the button size.** It forwards every option it
  accepts whether the caller set one or not, and spreading `size: undefined` over the
  defaults won — so `--fc-size` was set to the string "undefined" and the button fell
  back to its intrinsic width (300px instead of 60px). Confirmed against the previous
  build before changing anything, so this is a pre-existing defect, not a regression.
  Undefined values are now dropped before merging.
- **data-phone was coerced to a Number**, dropping leading zeros. The shared dataset
  reader leaves digit strings alone and converts only the options that are genuinely
  numeric.

The CSS is now minified before being inlined: `minify: true` only ever compressed
JavaScript, so the stylesheet shipped as source text with its comments to every
visitor. 36.3 kB -> 33.2 kB raw, 11.3 kB -> 10.7 kB gzipped.

Auto-init moved from index.ts to cdn.ts. Importing the package used to register a
DOMContentLoaded listener as a side effect, which is pointless in a module —
document.currentScript is null there.

scripts/size-check.mjs enforces a gzip budget per bundle in CI, using node:zlib.

Verified in a browser: the legacy FloatingWhatsApp global, a phone number with
leading zeros, numeric data attributes, and the default button size.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
The platform's second tool, and the first real test of whether the structure holds.
It does: registering it touched three files outside its own directory — the registry,
the message catalogue and the runtime bundle list — one line each.

## The runtime

Layered so the boundaries are checkable rather than aspirational: nothing in `core/`
or `storage/` imports from `ui/`, and a test asserts it. The engine is exercised
end-to-end by 27 tests with a stub host and no DOM at all, which is the same property
that makes a headless engine usable for SSR or a custom banner.

Decisions worth recording:

- **Shadow DOM, not class prefixes.** For a contact button, leaking styles is ugly.
  Here it is a functional risk: a host page that hides or deforms "Reject all" costs
  the site its lawful basis. Verified against a page that sets
  `button { display: none !important }` and `* { color: red !important }`.
- **Cookie primary, localStorage mirror.** Only a cookie is readable server-side and
  shareable across subdomains, but Safari caps script-written cookies at seven days —
  every Safari visitor would be re-prompted weekly. The mirror restores the cookie
  instead.
- **Expiry is absolute and never renewed.** Rolling it forward because someone came
  back would be consent by inertia.
- **URLs go in `data-consent-src`, not `src`.** `type="text/plain"` stops execution
  but not the browser's preload scanner, so a plain `src` still calls the third party
  before consent.
- **No innerHTML anywhere**, enforced by a test. Every string in this widget comes
  from configuration; parsing any of it as HTML would turn the configurator into an
  XSS delivery mechanism. Descriptions support `[text](url)` built with createElement.

Two semantics the tests corrected while writing them:

- **Expired consent granted nothing** only after the state was split: the form
  pre-selects what the visitor chose before, but `hasConsent()` stays false until they
  confirm. The first draft conflated the two, which would have kept analytics running
  on a lapsed record.
- **Re-prompting is not a first consent.** `firstConsent` now fires once per stored
  record, not once per pending state.

## Fixes this surfaced elsewhere

- The preview could not take over a runtime that boots itself: its `init()` was
  refused as a duplicate, so the first config change was ignored. The frame now adopts
  an existing instance before replacing it.
- The consent bundle's auto-init overrode an inline `init({...})` that ran during
  parsing — breaking the two-tag form its own README documents.
- `build:runtimes` and `typecheck` only ever covered one package.
- Inherited properties cross a shadow boundary: `color` and `font` were being taken
  from the host page. The root now pins its own.

15.4 kB gzipped against a 17 kB ceiling enforced in CI.

Verified in a browser on a hostile third-party page: tags stay blocked until their
category is accepted, a `javascript:` src is refused, Consent Mode v2 defaults land
before any decision, the decision survives a reload, revocation clears the declared
cookies, and a revision bump asks again.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
The README described a single product, a `build:lib` script that no longer exists, and
Cloudflare Pages — which is not the product this repo deploys to.

- README is now the platform's: what it is, the tools with links to their own docs,
  the three layers and why the config UI is never the runtime, and a measured
  checklist for adding a tool.
- CONTRIBUTING carries the architecture rules with their rationale, so the next person
  can tell which constraints are load-bearing.
- AGENTS.md is new: the same rules as instructions, plus the traps this refactor
  actually hit — Prettier breaking multi-statement Vue handlers, provide/inject in the
  same component, typecheck depending on built runtimes, catch-all routes needing an
  explicit param.

Workflows: `build.yml` became `release.yml` (publish on a GitHub Release, now running
the tests first) and a separate `deploy.yml` publishes the site. Deploy had no
automation at all before.

Also adds `published` to the runtime contract. The consent manager's package is not on
npm yet, so the snippet its playground generates points at a URL that resolves to
nothing; the code panel now says so rather than handing out something that silently
fails. Truthful beats tidy.

Static meta tags describe the platform, with a comment recording why they are shared:
per-route tags are set at runtime, which link-preview crawlers do not execute.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
Comparing the rendered widget from this branch against the previous build, on both
snippet shapes: the multi-channel path is identical, and the data-phone shorthand
differs in exactly one way — the button is now 60x60 instead of the 300x183 that the
`size: undefined` bug produced.

Worth stating plainly in the changelog, because existing sites will see it.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
Internal notes, deliberately not linked from the README or the site: what was studied
before writing the consent manager, what was adopted, what was rejected and why, plus
a baseline (versions, stars, last push) so the alternatives can be tracked over time.

Also records the decisions that came from nowhere in particular — mirrored storage,
never renewing expiry, a pending state granting nothing, the URL living in
data-consent-src — since those are the ones that will look arbitrary on review.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
Deployment was spread across three documents and none of it explained the parts that
are easy to get wrong. This is the complete procedure: Cloudflare and npm, automated
and by hand, with what to verify before and after.

Worth calling out, because they are not obvious:

- The generated snippet pins the CDN minor, so publishing 0.1.4 reaches every existing
  install while 0.2.0 reaches none. A fix that must reach live sites has to ship as a
  patch on the current minor.
- "Breaking" for these packages means anything that breaks a script tag someone pasted
  and forgot about: the bundle path, the globals, the data- attributes. The bundle
  contract tests and the snippet snapshots are the mechanical check for that.
- The trusted publisher on npm still points at build.yml, which no longer exists. The
  next release fails to authenticate until that is changed to release.yml. Flagged as
  an action item rather than buried.

Corrects both documents on a point I verified rather than assumed: `npm version` in a
workspace only edits package.json and the lockfile — it does not commit and does not
tag, so `git push --follow-tags` did nothing.

Also explains why this is Workers Assets and not Pages, since the old README claimed
Pages and the two are configured completely differently.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
The repository is being renamed on GitHub and the old Worker and subdomain were
deleted, so the deploy starts from scratch.

- Repository URLs point at codions/codions-tools. The npm package name
  (@codions/floating-contact-button) is deliberately unchanged: renaming it would
  break every install and every pasted snippet.
- wrangler.json targets a new Worker, `codions-tools`, and declares
  tools.codions.dev as a custom domain so the hostname is version-controlled rather
  than clicked into the dashboard.

ROADMAP.md is the working document: what this was, what was done, what it is becoming,
and what happens next — plus a ready-to-use prompt for the Nuxt migration.

That migration exists to take documentation out of the code. Today FcbDocs.vue is ~600
lines of Vue markup mixing prose, examples and components, so editing a paragraph means
editing a component. Nuxt Content moves it to markdown while MDC components keep the
interactivity. SSR comes along with it, which happens to fix the three SPA limitations
already recorded in DEPLOYMENT.md: real 301s, a real 404, and per-route Open Graph tags
that link-preview crawlers can actually read.

The Cloudflare backend (Workers, D1, KV, Email; email-only magic-link auth) is recorded
as a later phase with a note that the moment accounts and metrics exist, this project
starts processing personal data — and a platform that ships a consent manager should
not be careless about its own.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
npm matches a trusted publisher on repository name as well as workflow filename, so
the rename made both fields stale — not just the one already flagged. Left as a single
action item with the exact values to enter.

Claude-Session: https://claude.ai/code/session_01Jkki2TKQLeojVbYLZL9UUy
…entation

This commit transitions the project to Nuxt 4, restructuring the application to use the `app` directory instead of `src`. Key changes include:

- Updated `.gitignore` to exclude Nuxt generated files.
- Revised `AGENTS.md`, `CONTRIBUTING.md`, and `DEPLOYMENT.md` to reflect the new architecture and deployment process.
- Introduced `content.config.ts` for Nuxt Content integration, allowing Markdown documentation management.
- Updated `nuxt.config.ts` for proper asset handling and routing.
- Enhanced ESLint configuration to accommodate new globals and ignore patterns.
- Removed legacy `index.html` in favor of server-rendered content.
- Adjusted paths in various files to align with the new structure.

This migration improves the platform's maintainability and prepares it for future enhancements.
@fabioassuncao

Copy link
Copy Markdown
Member Author

Code review

Found 2 issues:

  1. </script> breakout in generated CDN snippets — unescaped user config values are embedded inside a literal <script>…</script> block, so a value containing </script> closes the tag early in the customer's page and the remainder becomes live HTML markup (e.g. an injected <img onerror=…>). formatValue/formatString only escape for JS-string-literal safety (\, ', \n, \r), not for HTML; the sibling htmlAttr() in the same files does escape </>/&, showing the escaping gap is an oversight rather than intentional. Reachable via the hasCallback branch of the floating-contact-button cdn snippet, and the else branch of the consent-manager cdn snippet (when dataAttributes() returns empty).

function formatValue(val: any): string {
if (typeof val === 'string') {
const escaped = val
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
return `'${escaped}'`
}
if (typeof val === 'boolean' || typeof val === 'number') return String(val)
return String(val)
}

function formatString(value: string): string {
const escaped = value
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
return `'${escaped}'`
}

  1. Production deploy is not gated on CI. deploy.yml and ci.yml both trigger independently on push: branches: [main], and deploy.yml has no needs/workflow_run dependency on ci.yml's verify job — it runs npx wrangler deploy straight after npm run build, so a push to main can deploy even if lint/typecheck/tests fail. It also has no concurrency group (unlike ci.yml), so two rapid pushes can race and an older build can overwrite a newer deploy.

https://github.com/codions/codions-tools/blob/c37a453bc94ac0b63c1574c469a93ba0b912d4ee/.github/workflows/deploy.yml#L1-L30

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

This commit modifies the GitHub Actions deployment workflow to trigger on successful completion of the CI workflow, ensuring that only verified commits are deployed. It also introduces concurrency control to prevent overlapping deployments.

Additionally, the consent manager code has been updated to improve security by escaping certain characters in string literals to prevent potential XSS vulnerabilities. The code now includes tests to verify that user input does not break out of script tags, enhancing the overall robustness of the consent manager.

Minor adjustments were made to the UI components and translations, including the removal of the read-only toggle in the consent manager panel, streamlining the user interface.
This commit introduces the detailed planning and documentation for Fase C, transitioning the codions-tools project to a single pixel platform. Key additions include:

- New architecture documentation in `00-arquitetura.md`, outlining the shift to a single script per registered site, configurable via a panel.
- Implementation steps for Fase 1 and Fase 2, including user registration, site management, and metrics collection.
- Security and privacy considerations documented in `04-seguranca-e-privacidade.md`, ensuring compliance with data protection standards.
- Testing guidelines in `05-testes-em-ambiente-publico.md` to validate functionality in a public environment.

The roadmap now provides a comprehensive guide for executing the transition, ensuring clarity and structure for future development.

roadmap/STATUS.md: updated to reflect the new phase and its components.
This commit introduces a new documentation file, CLAUDE.md, which provides essential guidance for working with the codions-tools repository. Key additions include:

- Overview of the repository structure, detailing the npm workspaces monorepo setup.
- Comprehensive command list for development, building, testing, and deployment processes.
- Clear architecture guidelines emphasizing the separation between the configuration interface and runtime.
- Hard rules for tool registration and deployment to ensure consistency and security.

This documentation aims to streamline onboarding and enhance understanding of the project's operational framework.
Table separators in roadmap/00-arquitetura.md, roadmap/README.md and
roadmap/02-ferramentas/*.md had drifted from the project's Prettier config
(column widths not padded to content). No content change — verified with
`git diff -b --ignore-all-space`. Fixing this now because Fase C's execution
cycle requires npm run format:check to pass cleanly before every etapa commit,
and this predates any Fase C code.
…e_tool_configs)

Implementa a etapa 1.1 do roadmap/STATUS.md. Cria o banco codions-tools-app
(binding APP_DB, separado do DB do Nuxt Content), com migração inicial via
`wrangler d1 migrations` (arquivos em apps/web/server/database/migrations/,
aplicada local e remotamente). Liga observability no wrangler.json (rota do
pixel em 1.6 será a de maior tráfego do projeto). Regenera o tipo Env via
`wrangler types` em apps/web/server/worker-configuration.d.ts e adiciona a
tipagem de event.context.cloudflare.env em apps/web/server/types.d.ts, para
que handlers futuros (1.4 em diante) acessem os bindings com segurança de
tipos. Exclui o .d.ts gerado do lint/format, como já se faz com dist/.nuxt.

roadmap/01-fundacao.md: registrado o mecanismo de migração escolhido
(wrangler d1 migrations, não SQL à mão) e a confirmação de que o D1 local
segue o mesmo padrão já usado pelo binding DB, sem configuração extra.

roadmap/STATUS.md: etapa 1.1 marcada concluído.
Implementa a etapa 1.2 do roadmap/STATUS.md. Binding SESSIONS adicionado a
wrangler.json (namespace codions-tools-sessions), tipo Env regenerado via
wrangler types. Duas formas de chave a serem usadas em 1.4:
magic:<token> -> { email } (TTL 900s, uso único) e session:<id> -> { userId }
(TTL 2592000s) — ambas com expirationTtl nativo do KV, sem tabela própria no
D1. Sem critério de validação manual nesta etapa (nenhuma rota usa o binding
ainda); a validação pública do fluxo de sessão acontece em 1.4 e 1.11.

roadmap/STATUS.md: etapa 1.2 marcada concluído.
npx wrangler email sending enable tools.codions.dev falha com Authentication
error [code: 10000] — o token OAuth local não tem o escopo
email_sending:write (confirmado via wrangler whoami). Não é contornável por
outro comando: precisa de wrangler login interativo para emitir um token com
o escopo certo. Etapa 1.3 marcada bloqueado em vez de pulada, seguindo a
convenção de status já definida no topo do arquivo.
…odions.dev

Implementa a etapa 1.3 do roadmap/STATUS.md. Bloqueio anterior (token OAuth
sem escopo email_sending:write) resolvido com um novo `wrangler login`.
`wrangler email sending enable tools.codions.dev` concluído; settings e dns
get confirmam Enabled: true e os registros MX/SPF/DKIM/DMARC já criados na
zona automaticamente (mesma conta Cloudflare do domínio). Binding
send_email/EMAIL adicionado a wrangler.json, tipo Env regenerado.
Remetente será algo como login@tools.codions.dev, sem caixa de entrada real
necessária — só o domínio autorizado, como o próprio roadmap já previa.

roadmap/STATUS.md: etapa 1.3 marcada concluído, bloqueio anterior documentado
como resolvido.
…/verify)

Implementa a etapa 1.4 do roadmap/STATUS.md. Três peças em apps/web/server/:
request.post.ts (rate limit por e-mail em KV — Camada 2 de
04-seguranca-e-privacidade.md §2 — gera o token, envia o e-mail, sempre
{ok:true}), routes/auth/verify.get.ts (token de uso único, cria/resolve
usuário em APP_DB, seta o cookie de sessão, redireciona para /painel) e
middleware/session.ts (resolve event.context.user para /painel/** e
/api/sites/**, 401/redirect sem sessão). Tela /entrar simples nos dois
locales. Helpers testados em apps/web/test/server/auth.spec.ts.

apps/web/server/utils/{auth,users}.ts importam KVNamespace/D1Database de
@cloudflare/workers-types (nova devDependency de apps/web, pinada na versão
que o wrangler já declara) em vez dos globais ambientes de
worker-configuration.d.ts — o $fetch tipado do Nitro puxa esses arquivos para
o projeto TS do app, que nunca inclui esse .d.ts; ver AGENTS.md.

Corrigido também um bug de i18n descoberto ao testar /entrar: '@' num
placeholder quebra o compilador de mensagens do vue-i18n (sintaxe de "linked
message"), mesmo entre aspas — trocado por um texto sem '@'. Registrado como
armadilha em AGENTS.md.

Validado com npx wrangler dev --tunnel (EMAIL com remote:true temporário,
revertido depois): e-mail real recebido em fabio23gt@gmail.com, cookie de
sessão confirmado com HttpOnly; Secure; SameSite=Lax via HTTPS real, token
confirmado como uso único. Detalhes e uma armadilha de teste (origin do link
sob rota simulada do wrangler dev) documentados em roadmap/01-fundacao.md
§1.4.

roadmap/STATUS.md: etapa 1.4 marcada concluído.
Um hook de revisão de segurança automática, rodado depois do commit da etapa
1.4, encontrou que o contador de rate limit por e-mail (Camada 2 de
04-seguranca-e-privacidade.md §2) não normalizava o endereço — "Test@x.com" e
"test@x.com" contavam como alvos diferentes, um bypass trivial da cota de 3
pedidos/10min. Corrigido com normalizeEmail() (trim + minúsculas), aplicado
antes de validar, contar e gravar o token magic:<token>.

As outras duas observações da revisão (GET em /auth/verify sem proteção
CSRF própria, corrida no incremento do contador em KV) foram avaliadas e
reconhecidas como características aceitas do desenho já documentado — não
corrigidas. Ver roadmap/STATUS.md, "Revisão de segurança automática do
commit da etapa 1.4", para o raciocínio completo de cada uma.

roadmap/STATUS.md: 4.2 atualizada para refletir que a Camada 2 (contador por
e-mail) já está implementada; a Camada 1 (Rate Limiting Rules por IP, no
dashboard da Cloudflare) segue pendente e continua sendo pré-requisito antes
de abrir a plataforma além do próprio usuário.
Implementa a etapa 1.5 do roadmap/STATUS.md. TOOL_CATALOG com as duas
ferramentas já existentes (floating-contact-button -> FloatingContact,
consent-manager -> CodionsConsent), conforme roadmap/00-arquitetura.md §5.
defaultConfig() de cada uma importa DEFAULTS/createDefaultState() de
apps/web/app/tools/<slug>/defaults.ts em vez de reinventar os valores, como
pedido pela etapa. configVersion reservado, sem comportamento ainda, igual
já registrado em 00-arquitetura.md.

Testado em apps/web/test/server/tool-catalog.spec.ts — inclui uma checagem
de que os dois pacotes (que fazem import.meta.env.VITE_*_VERSION no nível do
módulo) importam e avaliam sem lançar no contexto do servidor, não só no
build do app.

roadmap/01-fundacao.md: registrada a leitura adotada para uma ambiguidade
de 00-arquitetura.md §5 — defaultConfig() está na forma do estado do editor
(DEFAULTS), não na forma das opções de runtime; nenhuma rota desta fase
depende disso para chamar init().

roadmap/STATUS.md: etapa 1.5 marcada concluído.
Implementa a etapa 1.6 do roadmap/STATUS.md. apps/web/server/routes/p/[siteKey].get.ts
resolve site+ferramentas habilitadas via D1 Sessions API
(env.APP_DB.withSession('first-primary')), busca cada bundle já buildado via
self-fetch no binding ASSETS (Workers não têm filesystem em runtime), e
concatena em ordem de catálogo com um bootstrap try/catch por ferramenta —
uma falhar não derruba as outras. Cache-Control: public, max-age=60,
stale-while-revalidate=300; cache.enabled:true no wrangler.json (Workers
Caching, não Cache API). siteKey desconhecido, site sem nada habilitado, ou
tool_slug removido do catálogo: resposta vazia inofensiva, nunca erro.

Lógica de montagem extraída para apps/web/server/utils/pixel.ts, livre de
D1/ASSETS, testada em apps/web/test/server/pixel.spec.ts (equivalente ao
bundle.spec.ts de cada pacote: monta dois tools, confirma as duas chamadas
de init() com a config esperada, ordem de catálogo, tool_slug desconhecido e
config_json corrompido não derrubam a montagem).

Corrigido um bug real de build descoberto ao implementar: tool-catalog.ts
importa DEFAULTS/createDefaultState() de apps/web/app/tools/<slug>/defaults.ts
(etapa 1.5), e esses arquivos computavam CDN_URL usando
import.meta.env.VITE_*_VERSION — um `define` que só existe no build
client/SSR do Vite, não no bundle do Nitro. Isso derrubava o prerender do
`nuxt build` assim que a rota do pixel passou a importar o catálogo de
verdade. Extraído para cdn.ts em cada ferramenta, mantendo defaults.ts livre
de import.meta.env; os 4 consumidores de CDN_URL atualizados.

Validado com npx wrangler dev --tunnel (10 ago 2026): site de teste inserido
direto no D1 local, pixel carregado de uma página HTML servida por um
segundo túnel, fora deste projeto, aberta num navegador real (Playwright).
Confirmado por screenshot e JS: os dois widgets inicializam de verdade a
partir do pixel único. cf-cache-status ficou DYNAMIC nas requisições
repetidas — cache HIT de borda de verdade não é observável via wrangler dev
tunelado, só um deploy real, como já avisado em
05-testes-em-ambiente-publico.md §1; o header Cache-Control em si foi
conferido correto via curl.

roadmap/01-fundacao.md: registrada a sintaxe de rota que de fato funciona
([siteKey].get.ts, sem .js no nome do arquivo) e o resultado da validação.
roadmap/STATUS.md: etapa 1.6 marcada concluído.
AGENTS.md: nova entrada em "Known traps" sobre import.meta.env em arquivos
compartilhados entre app e server.
Implementa a etapa 1.7 do roadmap/STATUS.md. /painel lista os sites do
usuário logado (nome, domínio, contagem de ferramentas ativas), com estado
vazio próprio para quem ainda não tem nenhum. Botão "Adicionar site" abre um
formulário inline (nome + domínio informativo); ao salvar, POST /api/sites
cria a linha em `sites` com site_key via crypto.getRandomValues() (8 bytes,
prefixo ck_, apps/web/server/utils/sites.ts) e redireciona para
/painel/sites/:id (404 esperado — página da etapa 1.8, ainda não existe).

GET/POST /api/sites protegidos pelo middleware de sessão da etapa 1.4
(prefixo /api/sites já coberto); apps/web/server/utils/session.ts adiciona
requireUser(event), único ponto que lê event.context.user com type narrowing
em vez de cada handler checar undefined.

Corrigido um bug real descoberto ao testar pelo navegador: useAsyncData
chamando $fetch('/api/sites') durante o SSR falhava em silêncio — $fetch
puro não repassa o cookie da requisição original nessa chamada
same-origin-mas-servidor-para-si-mesmo, o middleware via 401, a página caía
no estado vazio sem erro visível. Trocado por useRequestFetch(). Registrado
em AGENTS.md — vale para qualquer página SSR chamando /api/sites/** daqui
pra frente.

Testado: apps/web/test/server/sites.spec.ts (formato do site_key,
unicidade, validação de nome/domínio). Validado com wrangler dev local (sem
necessidade de túnel público — etapa não toca cookie novo, cache ou e-mail,
já cobertos em 1.4): sessão de teste no KV local, fluxo completo pelo
navegador real — lista com contagem correta, criação de site e
redirecionamento funcionando.

roadmap/STATUS.md: etapa 1.7 marcada concluído.
Implementa/confirma a etapa 1.10 do roadmap/STATUS.md. Nenhum código novo
foi necessário, como o documento previa: grep por floating-contact-button/
consent-manager em apps/web/server/ e apps/web/app/playground/ só encontra
tool-catalog.ts (o registro em si) e um comentário de exemplo pré-existente
em PreviewFrame.vue — nenhuma rota, composable ou lógica de montagem do
pixel tem acoplamento a uma ferramenta específica.

Verificação consolidada com wrangler dev local: GET /p/ck_pixeltestkey01.js
monta as duas chamadas de init() com exatamente a config que o painel
salvou de verdade na etapa 1.9, fechando o círculo completo catálogo (1.5)
-> painel lista/toggle (1.7/1.8) -> config panel via toOptions() (1.9) ->
pixel monta e serve (1.6), para as duas ferramentas.

roadmap/STATUS.md: etapa 1.10 marcada concluído.
Implementa a etapa 1.11 do roadmap/STATUS.md, a última da Fase 1. Rodada
inteira em npx wrangler dev --tunnel (Worker) + um segundo túnel cloudflared
(página HTML de teste, fora deste projeto) + o usuário real deste projeto:

1. Login real: e-mail recebido de verdade, Set-Cookie confirmado com
   HttpOnly; Secure; SameSite=Lax via HTTPS real.
2. Site criado do zero pelo fluxo real da UI.
3. Pixel colado numa página HTML externa, servida por um processo Python
   http.server exposto por um segundo túnel.
4. Floating Contact Button ativado, canal WhatsApp configurado e salvo.
5. Página de teste recarregada: widget renderizado de verdade.
6. Consent Manager ativado da mesma forma: banner renderizado de verdade.
7. Consent Manager desativado pelo painel: página recarregada (com
   cache-busting), CodionsConsent virou undefined, FloatingContact e o
   widget continuaram presentes.
8. Content-Type e Cache-Control conferidos via curl -D -, exatamente como
   especificado. Cache HIT de borda não confirmável neste ambiente
   (cf-cache-status ficou DYNAMIC) — mesmo limite já registrado na etapa
   1.6, só observável com um deploy real.

Suíte automatizada completa rodada limpa após a validação manual.

roadmap/STATUS.md: etapa 1.11 marcada concluído — Fase 1 (Fundação da
plataforma) completa, todas as 11 etapas.
Implementa a etapa 2.1 do roadmap/STATUS.md. Migração 0002_tool_metrics.sql
(site_id, tool_slug, event, day, count), chave primária composta, upsert
via ON CONFLICT DO UPDATE SET count = count + 1 (a implementar no endpoint
de beacon da etapa 2.2). Aplicada local e remota via wrangler d1 migrations.

Sem identificador de visitante em nenhuma coluna, por desenho — cumpre o
piso obrigatório de roadmap/04-seguranca-e-privacidade.md §3 ("sempre
agregada, nunca por visitante") desde a etapa mais básica da Fase 2.

roadmap/STATUS.md: etapa 2.1 marcada concluído.
Implementa a etapa 2.2 do roadmap/STATUS.md. Corpo lido via readRawBody +
JSON.parse manual, não readBody() — navigator.sendBeacon(url, JSON.stringify(...))
envia Content-Type: text/plain, que o parsing automático do h3 não converte para
objeto, então a validação sempre falharia com o cliente real. Escrita do contador
via context.waitUntil(), nunca bloqueando a resposta 204. Nunca lança exceção não
tratada, mesmo com corpo malformado.

Validado com wrangler dev --tunnel + página HTML servida por um segundo túnel em
domínio diferente, navigator.sendBeacon disparando o payload real: 204 cross-origin
sem erro de CORS/mixed content, contador incrementando no D1 em chamadas repetidas,
e os quatro casos de borda (siteKey desconhecido, tool fora do catálogo, corpo
não-JSON, corpo vazio) todos retornando 204 sem escrita.

roadmap/STATUS.md: etapa 2.2 marcada concluído, com o bug do Content-Type
documentado. roadmap/03-metricas.md: nota de implementação sobre
context.waitUntil() confirmada e resolvida.

Claude-Session: https://claude.ai/code/session_01KV2pi4u3mmBPY2fkEAZUvf
Implementa a etapa 2.3 do roadmap/STATUS.md. reportEvent(siteKey, tool, event)
em packages/core/src/telemetry.ts, exportado só por um subpath próprio
(@codions/tools-core/telemetry) — nunca pelo barrel index.ts — para que uma
ferramenta que nunca reporta eventos nunca inclua o módulo no seu bundle.
navigator.sendBeacon envolto em try/catch silencioso, mesmo princípio de
"fail quietly" já usado no resto dos runtimes.

npm run size confirma zero variação nos dois bundles existentes: nenhuma
ferramenta atual consome o módulo ainda — essa decisão é de cada spec em
02-ferramentas/, não desta etapa. Sem fluxo de navegador para validar
publicamente por não haver consumidor real ainda; cobertura é
packages/core/test/telemetry.spec.ts.

roadmap/STATUS.md: etapa 2.3 marcada concluído.

Claude-Session: https://claude.ai/code/session_01KV2pi4u3mmBPY2fkEAZUvf
Implementa a etapa 2.4 do roadmap/STATUS.md — a última de Fase 2 (Métricas),
agora concluída. GET /api/sites/:siteId/metrics + página
/painel/sites/:siteId/metricas, seguindo o padrão de página já usado pela
config por ferramenta. Uma consulta só no D1 com somas condicionais calcula
total de 30 dias, contagem de hoje e a média dos 7 dias anteriores por
(ferramenta, evento) — sem pré-agregação, como já decidido em 03-metricas.md.
Só ferramentas ativas aparecem. O card "Métricas" na página do site trocou o
selo "Em breve" por um link real.

Validado com wrangler dev --tunnel e o fluxo de login por link mágico
completo (dois usuários de teste), cobrindo os três estados possíveis:
nenhuma ferramenta ativa, ferramenta ativa sem eventos, e ferramenta ativa
com dados reais (os gravados pela validação da etapa 2.2). Zero erros de
console nas três páginas.

roadmap/STATUS.md: etapa 2.4 marcada concluído; Fase 2 (Métricas) completa.

Claude-Session: https://claude.ai/code/session_01KV2pi4u3mmBPY2fkEAZUvf
Implementa a etapa 3.1 do roadmap/STATUS.md. @codions/announcement-bar,
seguindo o padrão de floating-contact-button (classe simples, static
init()/destroy()). Faixa fixa top/bottom, mensagem só via textContent, link
opcional checado por isSafeUrl, dispensa persistida em localStorage
namespaced por siteKey + ferramenta. Reporta impression/click/dismiss via
@codions/tools-core/telemetry quando inicializada com um siteKey real.

Decisão de arquitetura desta etapa (roadmap/00-arquitetura.md §8): siteKey
chega ao runtime como segundo argumento de init() — init(config, { siteKey })
— não embutido em config. apps/web/server/utils/pixel.ts e a rota
p/[siteKey].get.ts atualizados; sem mudança no contrato público WidgetFactory.

Corrige três lacunas reais do touch list de "adding a tool" em AGENTS.md/
README.md, descobertas ao registrar a terceira ferramenta:
content.config.ts (enum Zod do schema de conteúdo), OptionsTable.vue
(despacho por ferramenta hard-coded) e vitest.config.ts (define do
VITE_*_VERSION que faltava no projeto "web").

Validado com wrangler dev --tunnel, login real por link mágico e uma segunda
página HTML servida por um túnel separado, simulando um cliente colando o
pixel: barra renderizando ao lado do Floating Contact Button do mesmo pixel
sem colisão de globals, dispensa persistindo entre reloads, e o beacon de
telemetria disparando para a URL e o payload certos nos eventos impression/
dismiss (escrita no D1 já validada separadamente na etapa 2.2 — a URL de
telemetria é fixa para o domínio de produção, não observável via túnel local).

roadmap/STATUS.md: etapa 3.1 marcada concluído.

Claude-Session: https://claude.ai/code/session_01KV2pi4u3mmBPY2fkEAZUvf
Implementa a etapa 3.2 do roadmap/STATUS.md. @codions/recent-activity: rotação
de itens client-side via setTimeout encadeado (nunca setInterval, para não
sobrepor quando a aba fica em background), displayDurationMs controla quanto
tempo cada item fica visível, intervalMs o tempo entre o início de um item e
o início do próximo. maxPerSession limita popups por visitante via
sessionStorage namespaced por siteKey. label/message via textContent,
imageUrl/linkUrl checados por isSafeUrl. Reporta impression/click via
@codions/tools-core/telemetry.

Editor de itens no painel replica o padrão de ChannelList/ChannelEditor do
Floating Contact Button (arrastar para reordenar, expandir/colapsar).
Generalizada a exceção de vue/no-mutating-props no eslint.config.mjs de
nomes de arquivo explícitos para o padrão **/*Editor.vue, já que a mesma
extensão teve que ser repetida pela segunda vez.

Corrigido durante a validação: tool-catalog.ts usava exampleItems() (com
_uid gerado por contador + Date.now()) como defaultConfig(), quebrando o
teste que exige saída determinística — trocado para items: [], mesma
convenção que floating-contact-button já usa (DEFAULTS.channels: [] vazio;
só createState() no playground pré-preenche conteúdo de exemplo).

Validado com wrangler dev --tunnel, login real e uma página HTML externa:
playground rotacionando de verdade, painel salvando um item real, pixel
montado com CodionsRecentActivity.init(config, {siteKey}), beacon de
impression confirmado por contagem de requisições de rede entre duas
cargas, e maxPerSession:1 corretamente bloqueando uma segunda impressão
no reload.

Critério de "Fase C pronta para uso real" (KICKOFF.md) atingido: Fase 1 e
Fase 2 completas, mais duas ferramentas novas (Anúncio + Atividade Recente)
construídas e validadas ponta a ponta.

roadmap/STATUS.md: etapa 3.2 marcada concluído.

Claude-Session: https://claude.ai/code/session_01KV2pi4u3mmBPY2fkEAZUvf
Implementa a etapa 3.3 do roadmap/STATUS.md. @codions/live-counter trata os
dois modos do spec como a mesma config, nunca duas ferramentas: 'simulated'
(padrão) varia um número em torno de baseCount, 100% client-side, sem rede;
'real' lê uma contagem genuína de tool_metrics via um novo endpoint público.

Infraestrutura de servidor nova, diferente das duas ferramentas anteriores:

- GET /api/live-count — público, sem auth, keyed por siteKey (não siteId),
  Cache-Control: public, max-age=15, stale-while-revalidate=30 (mesmo padrão
  da rota do pixel). Nunca lança exceção; degrada para { count: 0 } em
  qualquer entrada inválida. isValidLiveCountQuery + getTodayCount em
  apps/web/server/utils/metrics.ts.
- PIXEL_API_ORIGIN extraído para packages/core/src/api.ts, compartilhado
  entre o beacon de telemetria (packages/core/src/telemetry.ts, antes com a
  URL embutida) e a leitura do contador real — as duas únicas chamadas de
  rede que um runtime pode fazer.
- roadmap/04-seguranca-e-privacidade.md §2: /api/live-count adicionado à
  lista de candidatos a Rate Limiting Rules nativas (Camada 1), com
  prioridade menor por já ser GET cacheado.

Validado com curl direto contra o túnel ({"count":5} para dado real,
{"count":0} para siteKey desconhecido/tool fora do catálogo/sem dado
hoje/query incompleta), playground nos dois modos e três posições, e o
pixel real numa página externa confirmando via captura de rede que o
fetch do modo 'real' monta a URL, siteKey, sourceTool e sourceEvent
corretos — mesma limitação já registrada nas etapas 3.1/3.2 para
observar uma resposta 200 de verdade (PIXEL_API_ORIGIN fixo para produção,
só alcançável depois do deploy).

roadmap/STATUS.md: etapa 3.3 marcada concluído.

Claude-Session: https://claude.ai/code/session_01KV2pi4u3mmBPY2fkEAZUvf
…Fase 3

Implementa a etapa 3.4 do roadmap/STATUS.md. @codions/reviews: carrossel
(setas + pontos sempre funcionais, inclusive por teclado) ou grade (até 6
cartões, "Show more" para o resto), autor/cargo/foto/nota opcional,
telemetria impression/interaction via @codions/tools-core/telemetry.
Reaproveita o padrão de lista de itens de recent-activity (editor com
drag-to-reorder, toOptions/fromOptions, defaultConfig determinístico no
catálogo do pixel).

Validado com wrangler dev local + página HTML externa, reutilizando o site
de teste das etapas anteriores (D1 local, sem depender de login por
e-mail): pixel real montou CodionsReviews.init() com a config correta ao
lado das outras três ferramentas; navegação do carrossel confirmada por
clique real; POST /api/t de "interaction" disparado exatamente uma vez
via captura de rede real, confirmando o guard de uma interação por
instância. Achado registrado, não corrigido (fora de escopo): o playground
de recent-activity já produzia um aviso de hydration mismatch antes desta
etapa, herdado por reviews via o mesmo padrão de genUid()/Date.now().

roadmap/STATUS.md: etapa 3.4 marcada concluído.

Claude-Session: https://claude.ai/code/session_01KV2pi4u3mmBPY2fkEAZUvf
…uinta ferramenta da Fase 3

Implementa a etapa 3.5 do roadmap/STATUS.md. @codions/coupon reaproveita a
base de posicionamento/estilo de announcement-bar (copiada e adaptada, nunca
importada) e adiciona um código copiável (Clipboard API com fallback para
execCommand) e uma contagem regressiva opcional, recalculada a cada segundo
no relógio do próprio visitante a partir de expiresAt. Ao expirar com a
página aberta, o widget se esconde em vez de congelar em "0s restantes".
Posição inline nova, além de top/bottom herdadas do anúncio.

Bug real encontrado e corrigido durante a validação com Chrome de verdade:
contra uma aba controlada por automação, navigator.clipboard.writeText()
ficava com a promise presa (nem resolve nem rejeita) quando a permissão de
clipboard-write não é concedida de imediato — o fallback de execCommand
nunca era acionado e o botão "Copiar" não dava feedback nenhum. Corrigido
correndo a chamada contra um timeout de 1s via Promise.race antes de cair no
fallback; teste de regressão adicionado simulando uma promise que nunca
resolve.

Validado com wrangler dev local (D1 real) + página HTML externa + Chrome
real: pixel montado ao lado das outras quatro ferramentas, cópia do código
confirmada por clique real (onde o bug acima foi encontrado e corrigido),
contagem regressiva decrementando em tempo real e o widget desaparecendo
exatamente no instante do prazo zerar, link real quando linkUrl configurado,
telemetria disparando para o domínio fixo de produção (mesma limitação já
registrada nas etapas 3.1–3.4), e playground testado ponta a ponta incluindo
a conversão do campo datetime-local para ISO 8601 contra o fuso horário real
do navegador. Suíte completa (237 testes) verde numa rodada limpa.

roadmap/STATUS.md: etapa 3.5 marcada concluído.

Claude-Session: https://claude.ai/code/session_01KV2pi4u3mmBPY2fkEAZUvf
…ma ferramenta da Fase 3

Implementa a etapa 3.6 do roadmap/STATUS.md. @codions/email-collector:
três layouts (inline/bar/modal), campo de nome opcional, honeypot
fora da tela descartado no servidor, e o único dos seis widgets com
armazenamento próprio.

Infraestrutura de servidor nova: migração 0003 (tabela
email_submissions), POST /api/email-collector/submissions (público,
resposta genérica em todo caminho que deve ficar em silêncio — só
e-mail malformado ou erro real de servidor respondem ok:false),
rate limit por IP em KV (mesmo padrão do login), webhook best-effort
lido do config salvo no servidor (nunca do payload do visitante,
para não virar proxy de SSRF), e duas rotas autenticadas
(listagem + export CSV com escaping RFC 4180 e neutralização de
CSV injection).

Slug em inglês (email-collector), não coletor-de-email como o
exemplo da spec — consistência com as cinco ferramentas anteriores.

Corrigido no mesmo commit, mesma causa raiz: GET /api/live-count
(etapa 3.3) nunca teve Access-Control-Allow-Origin, o que quebra
silenciosamente o modo real do live-counter entre origens diferentes
— só não tinha sido descoberto porque o domínio do pixel ainda não
está publicado.

Validado com wrangler dev local (D1/KV reais, migração aplicada) +
página HTML externa + Chrome real: honeypot confirmado fora da árvore
de acessibilidade, rate limit bloqueando a escrita sem revelar isso
na resposta, webhook disparando de verdade contra um servidor de eco
local e corretamente bloqueado com um protocolo fora do allowlist,
rotas autenticadas com checagem de posse confirmada (404 para
não-dono, 401 sem sessão), e o pixel real montando o widget ao lado
das outras cinco ferramentas já habilitadas no site de teste.

roadmap/STATUS.md: etapa 3.6 marcada concluído — Fase 3 completa.

Claude-Session: https://claude.ai/code/session_01KV2pi4u3mmBPY2fkEAZUvf
…iente

Tentativa de configurar Rate Limiting Rules nativas via API da Cloudflare
(MCP) em vez do dashboard falhou com Authentication error [code: 10000]
tanto na leitura quanto numa escrita de teste (dry_run: true, nada
persistido). Mesma causa raiz do bloqueio da etapa 1.3: o token OAuth
desta sessão não tem o escopo de API necessário para editar rulesets/WAF
da zona. Confirmado antes disso que a zona existe, está ativa, e que o
plano Free permite só 1 regra (suficiente para /api/auth/request, o
endpoint mais sensível dos três).

roadmap/STATUS.md: etapa 4.2 marcada bloqueado, com os parâmetros exatos
da regra documentados para configuração manual (API Token dedicado ou
dashboard) quando alguém com o escopo certo estiver disponível.
…o ativos

Implementa a etapa 4.3 do roadmap/STATUS.md — o reforço (b) de
04-seguranca-e-privacidade.md §3 em cima do piso (a) já existente desde a
2.3. packages/core/src/telemetry.ts consulta
globalThis.CodionsConsent?.hasConsent('analytics') antes de disparar o
beacon, via duck typing no global (nunca um import de
@codions/consent-manager — packages/core é bundlado em todo runtime que
usa telemetria, e uma dependência cruzada violaria "zero dependências" e
"nunca importar uma ferramenta de outra").

hasConsent() ainda não decidido (undefined) é tratado como bloqueado, não
liberado — a alternativa permissiva deixaria a telemetria própria mais
frouxa do que o próprio Consent Manager estaria numa checagem
equivalente antes da decisão do visitante.

Validado com os bundles reais via wrangler dev + Chrome (claude-in-chrome):
4 cenários (sem Consent Manager, sem decisão, aceito, recusado)
confirmados pela aba de rede, não só pelos testes unitários novos em
telemetry.spec.ts. npm run size sem regressão de budget nos 6 bundles
afetados.

roadmap/STATUS.md: etapa 4.3 marcada concluído.

Claude-Session: https://claude.ai/code/session_01KV2pi4u3mmBPY2fkEAZUvf
Implementa a etapa 4.1 do roadmap/STATUS.md. Nova coleção Nuxt Content
`legal` (sem o campo `tool` que `docs` exige) servindo /privacy e
/pt-BR/privacy via uma página dedicada, linkada do rodapé. Conteúdo
escrito a partir do inventário real do schema (contas, sites, config,
métricas agregadas, envios do coletor de e-mail) e dos TTLs reais de
login/sessão — não boilerplate genérico.

Distinção explícita controladora (dados de conta) vs. operadora (envios
do Coletor de e-mail, coletados a pedido do cliente que ativou a
ferramenta) — a política original (04-seguranca-e-privacidade.md §1) é
anterior à etapa 3.6 e não previa essa categoria. Lacunas reais
declaradas com honestidade: sem autoexclusão de conta/dados hoje, sem
expiração automática de métricas/envios — pedidos via contato manual em
privacidade@codions.com (endereço ainda não provisionado, ver nota em
STATUS.md).

Validado com npm run dev:web nas duas localidades (título, conteúdo,
link do rodapé) e Chrome real (captura de tela, console limpo).

roadmap/STATUS.md: etapa 4.1 marcada concluído. Com 4.2 já bloqueado
(escopo de API) e 4.3 concluído, nenhuma etapa numerada do roadmap
segue pendente — nota de fechamento adicionada no topo do arquivo.

Claude-Session: https://claude.ai/code/session_01KV2pi4u3mmBPY2fkEAZUvf
…-width

Todas as 16 páginas de documentação de ferramenta (8 ferramentas × 2
locales) usavam a mesma contagem de colons (::) em todo nível de
aninhamento: ::docs-body > ::feature-grid > ::feature-card. A sintaxe
MDC do Nuxt Content exige colons crescentes por nível de profundidade
(o oposto do que se poderia supor) — com contagem uniforme, o parser
fechava ::feature-grid e ::docs-body prematuramente logo após o
primeiro ::feature-card, deixando cada card seguinte (e tudo depois:
options-table, callout, playground-link) renderizar fora do container
`max-w-3xl mx-auto` do DocsBody, com largura total da tela, mais um
"::" literal órfão sobrando no meio do conteúdo.

Corrigido com um script determinístico (ignorando blocos de código
com ```` ``` ````) que recalcula a contagem de colons pela profundidade
real de aninhamento: nível 1 (docs-body, tool-hero) = ::, nível 2
(feature-grid, callout, options-table, playground-link) = :::, nível 3
(feature-card) = ::::. Validado renderizando as páginas de verdade
(sem "::" órfão em nenhuma, um único container de largura correta por
página, confirmado também visualmente via Chrome real). Não afeta
apps/web/content/*/legal/privacy.md, que não tem blocos aninhados.

Suíte completa (build:runtimes → typecheck → lint → format:check →
build:web → test → size) verde numa rodada limpa, 279 testes.
…ção do roadmap

Item 2 de PLANO-MESTRE.md §15: consolida na árvore de trabalho o resultado de
três rodadas de consolidação arquitetural que ainda não tinha commit próprio.

- ADR-0035: `ToolCatalogEntry.category: 'control' | 'functional'` — garante,
  por construção e testado, que `consent-manager` roda antes de qualquer
  módulo funcional no script montado do pixel.
- ADR-0041: Drizzle ORM (`drizzle-orm/d1`) para as 13 funções de acesso a
  dado fora da rota do pixel (`schema.ts`, `utils/db.ts`), com 41 testes
  novos contra migrations reais via `better-sqlite3`. A rota do pixel
  continua em D1 puro atrás de `PixelConfigSource` (única função presa à
  D1 Sessions API, sem suporte em nenhum ORM avaliado).
- `coupon` promovido de `beta` para `stable` — 8/8 widgets portados agora
  estáveis; `ToolStatus` perde o valor `beta`.
- Reorganização de `roadmap/` para a estrutura atual (`PLANO-MESTRE.md`,
  `KICKOFF.md`, `06-widgets/`, docs de detalhe `07`–`13`), substituindo os
  documentos da rodada anterior.
- `roadmap/_references/` (clone estático de terceiro para evidência de
  citação, ~19 MB, com `.git` aninhado) movido para `.gitignore` em vez de
  commitado — não é código-fonte nosso, e o `.git` aninhado o tornaria um
  gitlink de submódulo quebrado se adicionado como está.

Suíte completa (build:runtimes, typecheck, lint, format:check, build:web,
test, size) reverificada limpa nesta rodada — 304/304 testes.

PLANO-MESTRE.md: item 2 de §15 marcado resolvido.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
Implementa ADR-0004 / item 3 de PLANO-MESTRE.md §15. O default de
email-collector era 1000, sem justificativa registrada, destoando dos
outros 6 widgets de posição fixa (todos 100) e criando um empilhamento
implícito não intencional quando múltiplos widgets coexistem no mesmo
pixel. Alinhado em todos os espelhos: runtime, fallback CSS, README,
referência de docs e defaults do playground.

PLANO-MESTRE.md: item 3 de §15 marcado resolvido; §11 item 16 marcado
resolvido.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
…e evento legado

Implementa roadmap/06-widgets/_modelo-dados.md §3.3-§3.5 / item 4 de
PLANO-MESTRE.md §15. `tool_metrics` recriada com `label` na chave primária
(migração 0004, D1/SQLite não altera PRIMARY KEY com ALTER TABLE) — aplicada
e verificada contra D1 local real via `wrangler d1 migrations apply`, não só
a suíte de testes. Nenhuma linha histórica é reescrita além de ganhar
`label = ''` no default.

`apps/web/server/utils/telemetry-aliases.ts` (novo) traduz o vocabulário de
evento livre que os 6 runtimes já instrumentados ainda emitem (`click`,
`submit`, `copy`, `expire`...) para o par canônico `(event, label)` — usado
tanto na ingestão (`POST /api/t`) quanto na leitura (`GET /api/live-count`,
que resolve o `sourceEvent`/`sourceTool` legado configurado no painel do
live-counter), para que escrita e leitura do mesmo (tool, event) sempre
concordem. Um beacon já falando o vocabulário canônico (com `label`
explícito) nunca passa pelo alias — regra de desambiguação não prevista em
detalhe pela spec original, registrada como ADR-0043 (colisão real:
`reviews`'s evento legado `interaction` é também um dos 6 valores
canônicos).

O que este item NÃO faz (fora de escopo, item 5 de §15): instrumentar
`floating-contact-button`/`consent-manager` ou fazer os 6 widgets já
instrumentados emitirem `label` diretamente — eles continuam passando pelo
tradutor até essa extensão de `packages/core/src/telemetry.ts` acontecer.

PLANO-MESTRE.md: item 4 de §15 marcado resolvido; §10.6/§10.8/§10.15
atualizados; §11 item 18 marcado resolvido. 06-widgets/_decisoes.md: ADR-0043
registrado.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
…contact-button e consent-manager

Implementa _auditoria-paridade.md §3 item 1 / item 5 de PLANO-MESTRE.md §15.
`reportEvent(siteKey, tool, event, label?, options?)` ganha o único ponto de
extensão que a Fase 5 (ADR-0009) já tinha decidido — `label`, string curta e
fechada por widget — não os campos livres (url/notificationId/pageTitle) que
uma auditoria mais antiga (_auditoria-paridade.md §2.1) pedia; reconciliação
entre os dois documentos registrada em ADR-0044.

floating-contact-button (zero eventos antes): impression no render, conversion
com label link/callback/send nos canais e no envio do popup, interaction com
label toggle-bar/open-popup nas etapas intermediárias. consent-manager (zero
eventos antes): impression quando o banner é de fato exibido (engine 'ready'
com status pending), conversion com label accept-all/reject-all/save-preferences,
dismiss ao fechar sem decidir.

Achado durante a implementação: analyticsConsentGranted() (ADR-0016) bloquearia
estruturalmente a telemetria própria do consent-manager sobre reject-all e
sobre a própria impressão do banner, porque o estado que o gate consulta já
foi atualizado pela própria decisão antes do report acontecer — só accept-all
sobreviveria, uma taxa de aceite/rejeição inventada, não medida. Registrado em
PLANO-MESTRE.md §13, decidido com o usuário (AskUserQuestion): reportEvent
ganha options.bypassConsentGate, usado só no funil de decisão do próprio
consent-manager (ADR-0044) — nunca para medir o visitante em outro lugar da
página.

Validado manualmente nos dois playgrounds (clique nos canais e popup do
floating-contact-button, aceitar/rejeitar no consent-manager) — sem erro de
console, nenhum beacon disparado no playground (sem siteKey, como esperado).

PLANO-MESTRE.md: item 5 de §15 marcado resolvido; §10.19 e §11 Grupo A item 1
atualizados; pendência de §13 sobre o gate resolvida e removida.
06-widgets/_decisoes.md: ADR-0044 registrado.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
Implementa item 6 de PLANO-MESTRE.md §15 / roadmap/06-widgets/_pixel-unico.md
§8. Fecha a divergência entre documentação (00-arquitetura.md §7, invariante
6: "não existe um caminho de gerar um snippet por ferramenta e colar num
site, sem conta — ele não existe na plataforma") e código (os 8
packages/*/src/cdn.ts ainda liam data-* de qualquer <script> e se
auto-inicializavam, com ou sem site cadastrado).

Escolhida remoção completa, não gate: nenhum dos 8 runtimes faz round-trip
de rede para validar um data-site-key, então qualquer gate baseado em
atributo seria segurança de fachada (ADR-0045). Cada cdn.ts mantém só a
atribuição do global (window.<Nome> = <Classe>) — o que o bootstrap do
pixel (buildBootstrap, utils/pixel.ts) e o postMessage do playground já
usam, nunca dependeram do auto-init. packages/core/src/dataset.ts
(readScriptDataset/currentScriptDataset) removido junto — sem consumidor
restante depois da remoção.

Validado manualmente nos playgrounds de coupon e floating-contact-button:
renderização via init() programático segue funcionando, sem erro de
console.

Débito residual identificado durante a validação, registrado mas não
resolvido nesta rodada (fora do escopo deste item, UI maior): a aba
"CDN / Script" do playground continua gerando um snippet data-* que agora
não instala mais nada se colado num site real.

PLANO-MESTRE.md: item 6 de §15 marcado resolvido; §11 item 10 marcado
resolvido; novo item 25 registrado (débito da aba CDN/Script).
06-widgets/_decisoes.md: ADR-0045 registrado.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
Implementa ADR-0024 / item 7 de PLANO-MESTRE.md §15
(roadmap/06-widgets/_identidade-e-acesso.md §1.2/§1.6). Token de magic link
e sessionId eram usados como texto puro na própria chave KV — qualquer
leitor do namespace SESSIONS (dashboard, wrangler kv key list, token de API
com escopo de KV) via o segredo diretamente, sem reverter hash nenhum.

sha256hex novo em server/utils/auth.ts (crypto.subtle nativo do runtime de
Workers, sem dependência nova) — hash simples, sem sal, suficiente porque
token e sessionId são UUIDv4 de 122 bits de entropia (diferente do IP em
ADR-0015, que precisa de HMAC por ter espaço de busca pequeno). Aplicado às
3 escritas/leituras: magic: em request.post.ts e verify.get.ts, session: em
verify.get.ts e middleware/session.ts. O valor em texto puro continua
saindo só onde precisa sair — o link de e-mail e o cookie do navegador.

POST /api/auth/logout novo: apaga a sessão do request atual do KV, limpa o
cookie, redireciona para /entrar. Não exige sessão válida — encerrar uma
sessão já expirada ou inexistente é um no-op inofensivo.

Validado manualmente contra D1/KV local real (curl + wrangler kv key
list/put, não só a suíte de testes): chave gravada é o hash SHA-256 (64 hex,
sem hífen — nunca o UUID), /painel autentica com a sessão hasheada, logout
apaga a entrada e revoga o acesso imediatamente.

Fora do escopo deste item (resto de ADR-0024, não implementado):
invalidação de tokens pendentes ao consumir um, rotação de sessão,
logout-all.

PLANO-MESTRE.md: item 7 de §15 marcado resolvido; §10.10 atualizado; §11
item 11 e o risco de §14 marcados resolvidos.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
O item 7 de §15 implementou POST /api/auth/logout, mas §11 item 21 ainda
descrevia "nenhuma forma de logout, nem de uma sessão" como se nada
existisse. Achado ao compilar um status geral da sessão para o usuário.
Corrigido para refletir que só falta a UI (/painel/conta, botão de sair) e
logout-all — ambos já cobertos por §15 item 15.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
…luída

O try/catch na leitura do D1 da rota do pixel (ADR-0008) foi implementado
antes desta sessão (ADR-0041, PixelConfigSource) e já estava marcado
RESOLVIDO em §11 item 9 — mas §10.12 continuava descrevendo a lacuna como
se nada tivesse sido feito. Mesma categoria do fix anterior (§11 item 21):
achado ao compilar um status geral da plataforma para o usuário.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
Implementa item 8 de PLANO-MESTRE.md §15 (_auditoria-paridade.md §3 item 2 /
§2.6). packages/core/src/layout.ts exporta Layout (os seis valores que
ADR-0001 já definia na sua tabela de mapeamento — floating/bar/mini/
bar-mini/inline/modal, nunca implementados como tipo compartilhado) e
Anchor (os quatro valores já em uso hoje: top/bottom/bottom-left/
bottom-right — a grade completa de 9-12 posições continua fora de escopo,
trabalho futuro separado que depende deste eixo existir primeiro).

Só os tipos. Nenhum widget migrou seu Options público ainda — decisão
deliberada (ADR-0046) para não misturar 6 mudanças de contrato
independentes (announcement-bar, recent-activity, live-counter, coupon,
reviews, email-collector) num único item/commit. ADR-0046 registra o
mapeamento exato do campo atual de cada widget para (layout, anchor), para
a consolidação futura de cada um não precisar redescobrir isso — inclusive
um achado real: reviews já tem um campo público chamado layout para outra
coisa (arranjo carousel/grid), então sua consolidação futura vai exigir
renomear esse campo antes de adotar o eixo formal.

export type é apagado na compilação — confirmado que os 8 bundles ficam
byte-idênticos ao build anterior (npm run size).

PLANO-MESTRE.md: item 8 de §15 marcado resolvido; §11 Grupo A item 2
marcado resolvido ao nível de tipo. 06-widgets/_decisoes.md: ADR-0046
registrado.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
Implementa item 9 de PLANO-MESTRE.md §15 (_auditoria-paridade.md §2.2-§2.4/
§3 item 3). Nenhum dos 8 widgets portados tinha gatilho de exibição,
correspondência de URL ou frequência/fechar-reexibir — todos renderizam de
forma síncrona no construtor hoje.

packages/core/src/display-engine/ (novo), subpath dedicado
@codions/tools-core/display-engine (mesmo padrão de telemetry.ts — zero
custo de bundle para quem não importa):

- triggers.ts: os dez tipos da auditoria (delay, exit_intent, scroll,
  target_visible, inactivity, pageviews, time_on_site, click, hover,
  page_contains), cada um degradando para "nunca dispara" em vez de lançar
  quando o navegador não suporta algo ou o seletor não existe.
- url-rules.ts: exact/contains/starts_with/ends_with/regex, com
  include/exclude (exclude sempre vence).
- frequency.ts: all_time/once_per_session/once_per_browser, chave
  codions:<siteKey>:<toolSlug>:seen — deliberadamente idêntica à que
  12-campanhas-e-regras.md §5.2 já comprometeu no pseudocódigo do guard de
  campanhas (RULE_GUARD_PRELUDE), para as duas nunca divergirem quando o
  item 12 implementar aquele guard de verdade.
- page-session.ts: contadores de pageviews/time_on_site escopados por site,
  não por widget — dois widgets observando o mesmo gatilho concordam no
  mesmo número.
- engine.ts: orquestra URL → frequência → gatilho, com close() (persiste ou
  rearma depois de reopenAfterCloseMs) e destroy() (cancela sem persistir).

29 testes novos (jsdom): cada gatilho, matching de URL, frequência e a
engine isoladamente, incluindo os casos de degradação.

Nenhum widget migrado ainda — mesma disciplina de escopo do item 8
(ADR-0046): infraestrutura compartilhada é este item, consumi-la em cada
um dos 7 widgets candidatos é trabalho futuro por widget, não numerado em
§15 hoje. Bundle dos 8 pacotes confirmado byte-idêntico ao build anterior.

PLANO-MESTRE.md: item 9 de §15 marcado resolvido; §11 Grupo A item 3
marcado resolvido ao nível de infraestrutura. 06-widgets/_decisoes.md:
ADR-0047 registrado.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
Implementa ADR-0006/ADR-0048, item 10 de PLANO-MESTRE.md §15. Fecha o
maior risco de integridade de site registrado em §14: sem isso, quem
descobre um site_key alheio (público por desenho) podia servir a config
completa daquele site em qualquer domínio.

Migração 0005: sites ganha verification_token e domain_verified_at
(nullable, aditiva — nenhum comportamento muda até um site verificar de
fato). generateVerificationToken/ensureVerificationToken/
findVerificationMetaContent/verifySiteDomain em utils/sites.ts — fetch
server-side de https://<domain>/, procura
<meta name="codions-site-verification" content="..."> (parser próprio,
sem dependência de HTML), timeout de 5s, nunca lança. POST
/api/sites/:siteId/verify-domain novo. Painel do site ganha uma seção com
instruções, a tag para copiar e um botão "Verificar agora".

Enforcement, a parte que fecha o risco de fato: assemblePixelScript
(utils/pixel.ts) emite, uma única vez, um guard de location.hostname
(__codionsDomainAllows) para todo site com domain_verified_at preenchido,
envolvendo cada init() — não checagem de Referer/Origin no servidor, que
fragmentaria o cache compartilhado por siteKey (mesma restrição que já
descartou essa opção para o guard de campanhas, 12-campanhas-e-regras.md
§5.2). Um site não verificado continua com o bootstrap idêntico a antes
deste ADR. Limite explícito registrado em ADR-0048: é uma checagem
client-side, editável por quem inspeciona o bundle — barra o caso comum
(cópia sem exame do script), não um atacante deliberado; consistente com o
nível de risco já descrito em _pixel-unico.md §5.

Validado manualmente contra D1/KV local real: site criado via API real,
token exibido, POST verify-domain contra example.com (domínio real, sem a
tag) retornou false sem erro; com domain_verified_at setado, GET
/p/:siteKey.js passou a emitir o guard corretamente, com fail-open no
catch.

PLANO-MESTRE.md: item 10 de §15 marcado resolvido; §10.9 marcada
concluída; §11 item 15 e o risco de site_key em §14 marcados
resolvidos/mitigados. 06-widgets/_decisoes.md: ADR-0048 registrado.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
Implementa ADR-0026/ADR-0049, item 11 de PLANO-MESTRE.md §15. 8/8
handlers sob apps/web/server/api/sites/[siteId]/** já chamam getOwnedSite
antes de qualquer outra query — correto hoje por disciplina humana
consistente, não por algo que quebra a build se violado.

O design original de ADR-0026 (varrer handlers procurando nome de tabela
numa string SQL) ficou obsoleto pela migração Drizzle anterior a esta
sessão (ADR-0041): nenhum handler referencia mais tabela nenhuma
diretamente, todos chamam funções de utils/*.ts. Implementado como
especificado, o script nunca encontraria nada para checar — sempre
passaria, sem checar de verdade. Redesenhado (ADR-0049) para checar por
caminho de arquivo: todo .ts sob api/sites/[siteId]/** precisa importar e
chamar getOwnedSite — [siteId] no próprio caminho já é o sinal correto de
que a rota opera sobre um site existente por id, e o script não precisa
ser atualizado toda vez que uma função de utils/*.ts nova for adicionada.

scripts/authz-check.mjs (formato de scripts/size-check.mjs), npm run
authz-check novo, adicionado ao CI e à suíte completa documentada em
CLAUDE.md/KICKOFF.md. Testado manualmente nos dois sentidos: passa limpo
nos 8 handlers reais; com um handler fabricado sem getOwnedSite, falha
com exit 1 e aponta o arquivo exato.

PLANO-MESTRE.md: item 11 de §15 marcado resolvido; §11 item 14 marcado
resolvido. 06-widgets/_decisoes.md: ADR-0049 registrado.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
Implementa ADR-0036/ADR-0037/ADR-0038/ADR-0050, item 12 de PLANO-MESTRE.md
§15, seguindo o touch list exato de 12-campanhas-e-regras.md §7. ADR-0014
não é reaberto — agendamento e regras são três colunas novas na própria
site_tool_configs (starts_at, ends_at, rules_json), nunca uma tabela
campaigns separada.

Migração 0006 (próximo número livre real — 0004 já foi tomado por
tool_metrics_label numa etapa anterior desta sessão, 0005 pela verificação
de domínio). Aditiva: as linhas já existentes ficam com starts_at/ends_at
nulos e rules_json='{}', o mesmo comportamento "sempre visível" de hoje.

Agendamento avaliado no servidor, dentro do WHERE de
pixel-config-source.ts — uma ferramenta fora da janela nem transfere
bytes do bundle. device/frequency avaliados no cliente via
RULE_GUARD_PRELUDE real (utils/pixel.ts) — antes só pseudocódigo de
contrato. Dois guards: __codionsRuleAllows (leitura) e __codionsMarkSeen
(escrita, chamado só depois de init() suceder, dentro do mesmo bloco
protegido — mesmo cuidado que recent-activity.maxPerSession já tem hoje).
Combinado com o guard de domínio (ADR-0048, item posterior à
especificação original de campanhas) numa única condição, não dois blocos
aninhados. Chave de "visto" idêntica à que o display-engine já usa
(ADR-0047): codions:<siteKey>:<toolSlug>:seen.

saveSiteToolConfig migrado para um parâmetro único (SaveSiteToolConfigInput)
— a assinatura posicional de 5 argumentos já estava no limite antes dos 3
campos novos. Seção genérica nova no painel
(ToolScheduleAndRulesFields.vue, um componente, 8 usos reais no primeiro
dia) — datas de início/fim e selects de dispositivo/frequência, ao lado do
ConfigPanel de cada ferramenta, sem nenhum ConfigPanel.vue precisar saber
que campanhas existem.

Achado registrado em ADR-0050: nenhuma fonte nomeava o breakpoint exato de
device — 768px escolhido (breakpoint md do Tailwind, já implícito em toda
a UI da própria plataforma), não os breakpoints soltos e inconsistentes
dos 8 runtimes de widget.

url/visitorType continuam reservados no shape, sem avaliador — decisão
deliberada já registrada na especificação original, não implementados
nesta rodada.

13 testes novos (pixel.spec.ts, site-tools.spec.ts). Validado manualmente
ponta a ponta contra D1/painel locais reais: ciclo GET→PUT→GET
confirmando persistência; starts_at futuro e ends_at passado confirmados
excluindo a ferramenta do pixel montado; guard client-side gerado
corretamente com device/frequência reais; fluxo completo pela UI do
painel inspecionado via DevTools (PUT real, reload confirmando leitura de
volta).

PLANO-MESTRE.md: item 12 de §15 marcado resolvido; §10.13/§10.17 marcadas
concluídas (primeiro subconjunto). 06-widgets/_decisoes.md: ADR-0050
registrado.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
packages/request-collector/ (runtime completo: cartão flutuante, campo
phone/email/text, honeypot, dismiss via sessionStorage) +
apps/web/app/tools/request-collector/ (config panel, playground, codegen,
i18n en/pt-BR, docs) + tabela nova request_submissions (migração 0007) +
endpoint público de submissão + painel de envios/export CSV — segundo
widget de captura do catálogo, seguindo os mesmos padrões de
email-collector. Sem campo telemetry (um dos três widgets de telemetria
intrínseca, ADR-0052).

Achado e corrigido durante a implementação: bug de roteamento
pré-existente no Nitro fazia a própria tela de configuração/habilitação
de email-collector 404 em produção desde que foi portado (rota estática
homônima do toolSlug colidindo com tools/[toolSlug]) — nunca pego porque
nenhuma validação manual anterior exercitou a rota via HTTP real contra o
build. Resolvido movendo rotas de submissão para fora de tools/<slug>/
(ADR-0051), o que também corrige email-collector como efeito colateral.

Validado manualmente ponta a ponta contra D1/painel locais reais e contra
o build real (wrangler dev servindo .output/, não o servidor de dev do
Nuxt): migração aplicada, submissão válida persistida, honeypot/valor
inválido descartados sem persistir nada, pixel emitindo
CodionsRequestCollector, playground renderizando, ciclo GET→PUT→GET no
painel, Envios com a submissão real, CSV com neutralização de fórmula.

Claude-Session: https://claude.ai/code/session_013swvwnCAqRPDa63Kmq2UK5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant