From 3b66d1f90e63c67cbd4b53e8f51ed1a5c6c8b575 Mon Sep 17 00:00:00 2001 From: agent-linusswartz Date: Fri, 31 Jul 2026 01:06:48 -0300 Subject: [PATCH] feat(analytics): add Pages-only usage statistics --- .github/workflows/pages.yml | 8 +- README.md | 7 +- docs/architecture.md | 5 +- docs/privacy.md | 12 +- docs/releasing.md | 6 +- docs/validation.md | 6 +- index.html | 18 ++ package.json | 4 +- .../help-feature-request.test.mjs | 5 +- .../contract-tests/pages-analytics.test.mjs | 112 +++++++++++++ scripts/prepare-pages-site.mjs | 155 ++++++++++++++++++ scripts/validate-approval-gates.mjs | 9 + scripts/validate-pages-analytics.mjs | 88 ++++++++++ security_posture.md | 3 +- src/capabilities/03a-localization-messages.js | 6 + src/index.template.html | 11 ++ src/styles/02-layout.css | 1 + 17 files changed, 441 insertions(+), 15 deletions(-) create mode 100644 scripts/contract-tests/pages-analytics.test.mjs create mode 100644 scripts/prepare-pages-site.mjs create mode 100644 scripts/validate-pages-analytics.mjs diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 1e74479..b768b18 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -33,11 +33,9 @@ jobs: npm run validate:full:ci - name: Prepare site - run: | - mkdir -p _site - cp index.html _site/index.html - cp CHANGELOG.md _site/CHANGELOG.md - cp .release-please-manifest.json _site/.release-please-manifest.json + env: + UMAMI_WEBSITE_ID: ${{ vars.UMAMI_WEBSITE_ID }} + run: npm run prepare:pages - name: Upload artifact uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 diff --git a/README.md b/README.md index d36b8fe..3b6640c 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ The product is distributed as one standalone `index.html` artifact and is offlin ## Privacy and trust boundary -hSQLite Editor has no application backend and does not intentionally upload databases, queries, history, or settings. Files and browser storage remain on the device unless the user explicitly exports or shares them. +hSQLite Editor has no application backend and does not intentionally upload databases, queries, history, or settings. The downloaded standalone HTML, `file://` use, and local development contain no usage tracker. Only the official GitHub Pages deployment conditionally loads Umami for minimized pageview statistics, including an editor-version tag; it excludes URL query parameters and fragments and honors DNT and GPC. -The browser, browser extensions, operating system, downloaded artifact, and files selected by the user remain outside the project's trust boundary. Browser storage is not an encrypted secrets vault. The complete localStorage, IndexedDB, recent-file metadata, clearing, and permission-revocation inventory is in [docs/privacy.md](docs/privacy.md). See [SECURITY.md](SECURITY.md) and [security_posture.md](security_posture.md). +The browser, browser extensions, operating system, downloaded artifact, files selected by the user, and hosted analytics provider remain outside the project's trust boundary. Browser storage is not an encrypted secrets vault. The complete hosted-statistics boundary, localStorage and IndexedDB inventory, recent-file metadata, clearing behavior, and permission-revocation guidance are in [docs/privacy.md](docs/privacy.md). See [SECURITY.md](SECURITY.md) and [security_posture.md](security_posture.md). ## Browser support @@ -66,6 +66,7 @@ npm run validate:source npm run validate:i18n npm run validate:accessibility npm run validate:privacy +npm run validate:pages npm run validate:artifact npm run validate:artifact:structure npm run validate:runtime @@ -81,6 +82,8 @@ npm run quality:security:docker Do not edit generated `index.html` or `dist/` artifacts manually. Change source under `src/`, then rebuild. +The GitHub Pages workflow creates a separate `_site/index.html` with `npm run prepare:pages`. That command requires the public `UMAMI_WEBSITE_ID` variable and injects the reviewed Pages-only analytics loader after all standalone gates pass. + ## Architecture The project is a modular browser monolith: diff --git a/docs/architecture.md b/docs/architecture.md index 12e586d..0cdbeb5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,7 +2,7 @@ ## Product boundary -hSQLite Editor is one browser application distributed as one offline-capable HTML file. SQLite execution, persistence, import/export, and UI behavior run locally in the browser. There is no application backend and no remote data service. +hSQLite Editor is one browser application distributed as one offline-capable HTML file. SQLite execution, persistence, import/export, and UI behavior run locally in the browser. There is no application backend or remote database service. The GitHub Pages deployment is a narrow hosting satellite that adds minimized Umami pageview analytics after the standalone artifact passes validation. ## Source ownership @@ -25,6 +25,7 @@ hSQLite Editor is one browser application distributed as one offline-capable HTM 5. `src/app.mjs` is the sole production entry point. Every cross-file dependency is an ESM import/export, and module cycles are blocking failures. 6. User-facing copy and locale formatting are owned by the localization capability. 7. Generated `index.html` and `dist/` files are outputs, not hand-maintained source. +8. Hosted analytics exists only in generated `_site/index.html`, runs only on the exact official Pages origin and path, and never enters tracked standalone or release artifacts. ## Build composition @@ -36,6 +37,8 @@ DOM registries and styles are split by owned surface. Numeric filenames define d The Linux filesystem stage is a packaging satellite over the same versioned HTML artifact. It adds no second runtime, service, persistence layer, file association, or protocol handler. Its launcher resolves the packaged HTML relative to the installed prefix and delegates that one local path to `xdg-open`; [linux-packaging.md](linux-packaging.md) defines the complete boundary. +The GitHub Pages stage is a second deployment satellite over the same validated `index.html`. `scripts/prepare-pages-site.mjs` copies that artifact to `_site`, injects one fixed-origin Umami loader, attaches the package version as a pageview tag, and validates the hosted result. Only the public website ID comes from a GitHub variable. The loader checks the exact Pages origin and project path and honors DNT and GPC before requesting the third-party script. `_site` is generated and never becomes a standalone or release source. + ## Architectural change policy Preserve the modular monolith and standalone distribution unless evidence shows they no longer serve the product. The CodeMirror 6 decision is recorded in `docs/adr/0001-codemirror-6-editor-runtime.md`; the ESM graph and standalone linker decision is recorded in `docs/adr/0002-esm-application-module-graph.md`. diff --git a/docs/privacy.md b/docs/privacy.md index e96e03f..0efe420 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -2,10 +2,20 @@ ## Data flow -hSQLite Editor runs in the browser and has no application backend, analytics service, or required telemetry endpoint. The standalone artifact does not intentionally upload database bytes, SQL text, history, favorites, settings, or file metadata. Data leaves the browser only when the user explicitly downloads, exports, copies, or shares it, or when browser/extension/operating-system behavior outside the project trust boundary does so. +hSQLite Editor runs in the browser and has no application backend. The downloaded standalone artifact, `file://` use, local development, and nonofficial hosts do not load an analytics service or intentionally upload database bytes, SQL text, history, favorites, settings, file names, or file metadata. Data leaves those surfaces only when the user explicitly downloads, exports, copies, or shares it, or when browser, extension, or operating-system behavior outside the project trust boundary does so. Browser storage is local to a browser profile and origin. It is not encrypted application storage and must not be treated as a secrets vault. Storage behavior for `file://` pages varies by browser; use the browser's site-data controls for the artifact origin when removing all data. +## GitHub Pages usage statistics + +Only the official deployment at `https://helbertm.github.io/hSQLite-Editor/` conditionally loads the reviewed Umami Cloud tracker. The loader checks the exact origin and project path before making a request. It does not run on `file://`, localhost, local IP addresses, copied HTML, or another domain. It also makes no analytics request when the browser enables Do Not Track or Global Privacy Control. + +The hosted deployment records automatic pageviews. Umami can use ordinary pageview and browser metadata to report the page path, referring site, browser, operating system, device category, language, screen size, country, date, and time. URL query parameters and fragments are excluded. The current editor version is attached as a tag such as `hsqlite-editor-v0.7.0`, which allows maintainers to compare aggregate use by version. + +The integration does not call custom tracking or identification functions and does not enable custom events, performance collection, session replay, or heatmaps. It does not intentionally send SQLite database contents, database bytes, SQL text, queries, file names, history, favorites, settings, recent-file metadata, exported data, or tab content. Umami Cloud is a third-party hosted service; its network processing, retention, and country derivation are governed by the service configuration and terms rather than by the standalone editor. + +The deployment fails closed when the public Umami website ID is absent or malformed. The tracker URL and minimization settings are fixed in reviewed repository code rather than mutable GitHub variables. Analytics being blocked or unavailable does not prevent the editor from starting or working. + ## localStorage inventory The application can store these entries: diff --git a/docs/releasing.md b/docs/releasing.md index 72c7964..cb177c1 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -7,11 +7,11 @@ 5. Inspect both readable `index.html` and minified `dist/hSQLite-Editor-v.html` artifacts. 6. Run the browser locale/accessibility matrix on the generated artifact. 7. Run `npm run validate:native:chromium`, then open the final artifact directly with `file://` in current Safari and complete changed assistive-technology spot checks. -8. Confirm no runtime network request is required and no machine-local path is present. +8. Confirm the standalone and release artifacts make no runtime network request and contain no machine-local path or hosted analytics loader. 9. Run `npm run validate:linux`; when publishing a distribution-specific Linux package, complete the host-native installation and desktop-session check defined in [linux-packaging.md](linux-packaging.md). 10. Merge the release PR only after all required GitHub checks pass. Release Please must create the exact tag and a draft release, never a public incomplete release. 11. The publication job rebuilds the created tag, validates the release, creates both attestations, uploads the exact HTML, `sbom.spdx.json`, and `SHA256SUMS` without overwrite semantics, then publishes the draft release last. Any earlier failure must leave the release unpublished as a draft. -12. Verify the GitHub release attachment and Pages deployment match the `hsqlite-editor-v` tag. +12. Verify the GitHub release attachment and Pages deployment match the `hsqlite-editor-v` tag. Confirm the Pages deployment contains exactly one Umami loader tagged with that version, while the release attachment contains none. 13. Run `GH_TOKEN= npm run validate:github-controls -- --confirm-pages-admin-bypass-disabled` and require exit code `0`. 14. Verify both the provenance and SPDX SBOM attestations cryptographically for the exact HTML artifact. @@ -19,6 +19,8 @@ The exact order and ownership of static, contract, artifact/runtime, browser, se `index.html` and `sbom.spdx.json` are tracked, reproducible release evidence. Versioned `dist/` artifacts, `SHA256SUMS`, and the temporary Pages `_site/` tree are generated outputs and must not be committed as release history. +The Pages workflow requires the public repository variable `UMAMI_WEBSITE_ID`. `scripts/prepare-pages-site.mjs` rejects a missing or malformed UUID, fixes the reviewed Umami Cloud script URL in source, and validates `_site/index.html` after injection. Do not place executable HTML, a script URL, or tracking configuration in a mutable GitHub variable. + Generate and verify the portable checksum file with `npm run generate:release-checksums` and `npm run validate:release-assets`. Published checksum entries use release asset basenames, so users can download the HTML, SBOM, and `SHA256SUMS` into one directory and run `shasum -a 256 -c SHA256SUMS`. After publication, verify provenance with `gh attestation verify dist/hSQLite-Editor-v.html --repo /`. Verify the associated SPDX predicate by adding `--predicate-type https://spdx.dev/Document/v2.3`. Immutable release publication is atomic: create the exact tag and draft, build from that tag, validate, attest, upload the complete asset bundle, then publish. The workflow forbids `--clobber`. If upload is partial or an asset-name conflict occurs, delete and recreate the unpublished draft after diagnosing the cause; never repair a release by overwriting an asset. diff --git a/docs/validation.md b/docs/validation.md index b6f9714..5263173 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -4,7 +4,9 @@ hSQLite Editor separates checks by failure ownership. A passing broader layer ne ## Static policy -Run `npm run validate:static` for source composition, localization ownership, accessibility contracts, privacy documentation, SPDX reproducibility, and workflow syntax. These checks inspect owned files without starting the application. +Run `npm run validate:static` for source composition, localization ownership, accessibility contracts, privacy documentation, GitHub Pages analytics isolation, SPDX reproducibility, and workflow syntax. These checks inspect owned files without starting the application. + +`npm run validate:pages` proves that tracked standalone and generated release artifacts contain no tracker, the Pages workflow owns deploy-time preparation, the vendor script URL is fixed in reviewed code, only the website UUID is configurable, the exact official origin and path are enforced, URL query/hash data is excluded, DNT/GPC is honored, and privacy documentation covers the hosted boundary. The Pages preparation command performs a second validation against the generated `_site/index.html`. ## Deterministic unit and contract tests @@ -44,6 +46,8 @@ CodeQL analyzes the owned `src/` and `scripts/` trees. Generated standalone arti This command is intentionally outside `validate:full`, `validate:full:ci`, and normal pull-request CI. Hosted state is networked, permission-sensitive, and non-deterministic. A missing token or manual confirmation is `UNVERIFIED`, never a pass; verified drift and transport failures use different exit codes so operators do not misclassify access failures as product defects. +After a Pages deployment, inspect browser network traffic on the exact public URL. With DNT and GPC disabled, expect one reviewed Umami script and one automatic pageview tagged `hsqlite-editor-v`; verify that query strings, URL fragments, and application content are absent. With DNT or GPC enabled, expect no Umami request. Repeat from localhost and `file://` and expect no Umami request. Vendor-account dashboards and retention settings remain hosted evidence and are not proven by repo-local tests. + ## Orchestration `npm run validate:full` runs static policy, deterministic contracts, readable structure, cross-surface runtime, feature-owned runtime, release structure/runtime, deterministic Linux staging, and approval gates in that order. `npm run validate:full:ci` adds clean-repository checks before and after the same sequence. Browser, security-update, host-native, and hosted GitHub checks remain separate because their environments and failure modes are independent. diff --git a/index.html b/index.html index 7d1b4c7..bb0e8a0 100644 --- a/index.html +++ b/index.html @@ -406,6 +406,7 @@ .app-footer { display: flex; + flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px; @@ -5895,6 +5896,8 @@

Resultados

GitHub MIT + + Privacidade @@ -6316,6 +6319,7 @@

Ajuda Geral do hSQLite Editor

Mapa SQL Sessão e atualizações Backup e modo offline + Privacidade e estatísticas Suporte e sugestões
@@ -6432,6 +6436,14 @@

Backup e modo offline

O HTML standalone oficial funciona em file://. Dependendo do navegador, mudar o nome ou o caminho do HTML pode criar outro armazenamento local; exporte o backup antes de substituir o arquivo.

+
+

Privacidade e estatísticas

+

O HTML standalone baixado, o uso por file:// e a execução local não enviam estatísticas de uso.

+

Somente o site oficial no GitHub Pages usa Umami para medir visualizações, origem de referência, navegador, sistema, dispositivo, idioma, tamanho da tela, país e versão do editor. Parâmetros e fragmentos da URL são excluídos, e DNT e GPC são respeitados.

+

Nenhum banco, SQL, nome de arquivo, consulta, histórico, favorita, configuração ou conteúdo de aba é enviado para essas estatísticas.

+ Ler o aviso de privacidade completo +
+

Suporte e sugestões

Use Reportar erro para defeitos reproduzíveis. Sugerir melhoria abre o formulário específico Feature request do GitHub.

@@ -30783,6 +30795,11 @@

Modo offline indisponível

["help.backupTitle", "Backup and offline mode", "Backup e modo offline", "Copia de seguridad y modo sin conexi\xF3n"], ["help.backupHtml", "Periodically export a JSON backup from Settings. It may contain SQL, history, favorites, preferences, and tabs, but does not include the SQLite database; save the database separately as .db. Clearing site data, using a private window, or changing browser, profile, or device can make local data unavailable.", "Exporte periodicamente um backup JSON em Configura\xE7\xF5es. Ele pode conter SQL, hist\xF3rico, favoritas, prefer\xEAncias e abas, mas n\xE3o inclui o banco SQLite; salve o banco separadamente em .db. Limpar dados do site, usar janela privativa ou trocar de navegador, perfil ou dispositivo pode tornar os dados locais indispon\xEDveis.", "Exporte peri\xF3dicamente una copia JSON desde Configuraci\xF3n. Puede contener SQL, historial, favoritas, preferencias y pesta\xF1as, pero no incluye la base de datos SQLite; guarde la base por separado como .db. Borrar los datos del sitio, usar una ventana privada o cambiar de navegador, perfil o dispositivo puede hacer que los datos locales no est\xE9n disponibles."], ["help.offlineHtml", "The official standalone HTML works from file://. Depending on the browser, changing the HTML name or path may create separate local storage; export a backup before replacing the file.", "O HTML standalone oficial funciona em file://. Dependendo do navegador, mudar o nome ou o caminho do HTML pode criar outro armazenamento local; exporte o backup antes de substituir o arquivo.", "El HTML aut\xF3nomo oficial funciona desde file://. Seg\xFAn el navegador, cambiar el nombre o la ruta del HTML puede crear otro almacenamiento local; exporte la copia antes de sustituir el archivo."], + ["help.privacyTitle", "Privacy and usage statistics", "Privacidade e estat\xEDsticas de uso", "Privacidad y estad\xEDsticas de uso"], + ["help.privacyStandalone", "The downloaded standalone HTML, file:// use, and local development do not send usage statistics.", "O HTML standalone baixado, o uso por file:// e a execu\xE7\xE3o local n\xE3o enviam estat\xEDsticas de uso.", "El HTML aut\xF3nomo descargado, el uso mediante file:// y la ejecuci\xF3n local no env\xEDan estad\xEDsticas de uso."], + ["help.privacyPages", "Only the official GitHub Pages site uses Umami to measure pageviews, referrers, browser, operating system, device, language, screen size, country, and editor version. URL query parameters and fragments are excluded, and DNT and GPC are respected.", "Somente o site oficial no GitHub Pages usa Umami para medir visualiza\xE7\xF5es, origem de refer\xEAncia, navegador, sistema operacional, dispositivo, idioma, tamanho da tela, pa\xEDs e vers\xE3o do editor. Par\xE2metros e fragmentos da URL s\xE3o exclu\xEDdos, e DNT e GPC s\xE3o respeitados.", "Solo el sitio oficial de GitHub Pages usa Umami para medir visitas, referencias, navegador, sistema operativo, dispositivo, idioma, tama\xF1o de pantalla, pa\xEDs y versi\xF3n del editor. Se excluyen los par\xE1metros y fragmentos de la URL, y se respetan DNT y GPC."], + ["help.privacyNoContent", "No database, SQL text, file name, query, history entry, favorite, setting, or tab content is sent for these statistics.", "Nenhum banco, texto SQL, nome de arquivo, consulta, item do hist\xF3rico, favorita, configura\xE7\xE3o ou conte\xFAdo de aba \xE9 enviado para essas estat\xEDsticas.", "No se env\xEDa ninguna base de datos, texto SQL, nombre de archivo, consulta, elemento del historial, favorito, configuraci\xF3n ni contenido de pesta\xF1a para estas estad\xEDsticas."], + ["help.privacyNotice", "Read the complete privacy notice", "Ler o aviso de privacidade completo", "Leer el aviso de privacidad completo"], ["help.supportTitle", "Support and suggestions", "Suporte e sugest\xF5es", "Soporte y sugerencias"], ["help.supportBody", "Use Report a bug for reproducible defects. Suggest improvement opens the scoped GitHub Feature request form.", "Use Reportar erro para defeitos reproduz\xEDveis. Sugerir melhoria abre o formul\xE1rio espec\xEDfico Feature request do GitHub.", "Use Informar de un error para defectos reproducibles. Sugerir una mejora abre el formulario espec\xEDfico Feature request de GitHub."], ["offline.description", "This artifact is expected to work entirely offline. This screen means the file is incomplete or corrupted.", "Este artefato deve funcionar totalmente offline. Esta tela indica que o arquivo est\xE1 incompleto ou corrompido.", "Este artefacto debe funcionar totalmente sin conexi\xF3n. Esta pantalla indica que el archivo est\xE1 incompleto o da\xF1ado."], @@ -31082,6 +31099,7 @@

Modo offline indisponível

["results.csvFilename", "sqlite_result.csv", "resultado_sqlite.csv", "resultado_sqlite.csv"], ["results.jsonFilename", "sqlite_result.json", "resultado_sqlite.json", "resultado_sqlite.json"], ["sqlMap.pngFilenamePrefix", "sql-map-er", "mapa-sql-der", "mapa-sql-der"], + ["footer.privacy", "Privacy", "Privacidade", "Privacidad"], ["database.filePickerDescription", "SQLite database", "Banco de dados SQLite", "Base de datos SQLite"] ]); var I18N_ADDITIONAL_MESSAGES = Object.freeze(Object.fromEntries( diff --git a/package.json b/package.json index 578ac87..f75b7a2 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "bump:patch": "node scripts/bump-version.mjs", "build": "node scripts/build.mjs", "build:release": "node scripts/build-release.mjs", + "prepare:pages": "node scripts/prepare-pages-site.mjs", "generate:release-checksums": "node scripts/release-assets.mjs", "generate:linux-metadata": "node scripts/sync-linux-release-metadata.mjs", "generate:sbom": "node scripts/generate-sbom.mjs", @@ -31,7 +32,7 @@ "validate:full": "node scripts/validate-full.mjs", "validate:full:ci": "node scripts/validate-full.mjs --expect-clean-index", "validate:github-controls": "node scripts/validate-github-controls.mjs", - "validate:static": "npm run validate:modules && npm run validate:source && npm run validate:i18n && npm run validate:accessibility && npm run validate:privacy && npm run validate:runtime-components && npm run validate:sbom && npm run validate:workflows", + "validate:static": "npm run validate:modules && npm run validate:source && npm run validate:i18n && npm run validate:accessibility && npm run validate:privacy && npm run validate:pages && npm run validate:runtime-components && npm run validate:sbom && npm run validate:workflows", "validate:linux": "node scripts/validate-linux-package.mjs", "validate:linux-metadata": "node scripts/sync-linux-release-metadata.mjs --check", "validate:linux:system": "bash scripts/validate-linux-system-tools.sh", @@ -41,6 +42,7 @@ "validate:accessibility": "node scripts/validate-accessibility.mjs", "validate:dependencies": "node scripts/validate-dependencies.mjs", "validate:privacy": "node scripts/validate-privacy-docs.mjs", + "validate:pages": "node scripts/validate-pages-analytics.mjs", "validate:runtime-components": "node scripts/generate-runtime-components.mjs --check", "validate:sbom": "node scripts/generate-sbom.mjs --check", "validate:workflows": "node scripts/validate-workflows.mjs", diff --git a/scripts/contract-tests/help-feature-request.test.mjs b/scripts/contract-tests/help-feature-request.test.mjs index 052e890..92c4bdc 100644 --- a/scripts/contract-tests/help-feature-request.test.mjs +++ b/scripts/contract-tests/help-feature-request.test.mjs @@ -54,7 +54,7 @@ test("Help uses a semantic document layout for session continuity", () => { assert.match(helpDialog, /aria-describedby="helpIntro"/); assert.match(helpDocument, /
@@ -735,6 +737,7 @@

Ajuda Geral do hSQLite Editor

Mapa SQL Sessão e atualizações Backup e modo offline + Privacidade e estatísticas Suporte e sugestões
@@ -851,6 +854,14 @@

Backup e modo offline

O HTML standalone oficial funciona em file://. Dependendo do navegador, mudar o nome ou o caminho do HTML pode criar outro armazenamento local; exporte o backup antes de substituir o arquivo.

+
+

Privacidade e estatísticas

+

O HTML standalone baixado, o uso por file:// e a execução local não enviam estatísticas de uso.

+

Somente o site oficial no GitHub Pages usa Umami para medir visualizações, origem de referência, navegador, sistema, dispositivo, idioma, tamanho da tela, país e versão do editor. Parâmetros e fragmentos da URL são excluídos, e DNT e GPC são respeitados.

+

Nenhum banco, SQL, nome de arquivo, consulta, histórico, favorita, configuração ou conteúdo de aba é enviado para essas estatísticas.

+ Ler o aviso de privacidade completo +
+

Suporte e sugestões

Use Reportar erro para defeitos reproduzíveis. Sugerir melhoria abre o formulário específico Feature request do GitHub.

diff --git a/src/styles/02-layout.css b/src/styles/02-layout.css index ab8e724..a7b49af 100644 --- a/src/styles/02-layout.css +++ b/src/styles/02-layout.css @@ -99,6 +99,7 @@ font-size: 18px; .app-footer { display: flex; + flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px;