From 7cb68860893c440f50fad7cf772971119952708a Mon Sep 17 00:00:00 2001 From: gnacho Date: Sat, 22 Aug 2026 22:48:44 +0200 Subject: [PATCH] feat(demo): optional GoatCounter tracker for public demo builds Refs #112 --- app/vite.config.ts | 24 ++++++++++++++++++++++-- server/src/index.js | 5 ++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/app/vite.config.ts b/app/vite.config.ts index 86347be..292e0ff 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -1,12 +1,32 @@ import path from "path" import react from "@vitejs/plugin-react" import tailwindcss from "@tailwindcss/vite" -import { defineConfig } from "vite" +import { defineConfig, type Plugin } from "vite" + +// Tracker GoatCounter SOLO en el build de la demo pública (issue #112): +// VITE_GC_COUNT=https://stats.helios.cloudless.club npm run build +// Los builds normales NO lo llevan: una instalación self-hosted nunca debe +// llamar a casa. Los hits se registran con prefijo /demo en el mismo site +// que la landing ("/" = landing, "/demo/..." = demo). +const gcCount = process.env.VITE_GC_COUNT?.replace(/\/$/, "") + +function goatcounterPlugin(): Plugin { + return { + name: "helios-goatcounter", + transformIndexHtml(html) { + if (!gcCount) return html + const snippet = + ` \n` + + ` \n ` + return html.replace("", snippet) + }, + } +} // https://vite.dev/config/ export default defineConfig({ base: './', - plugins: [react(), tailwindcss()], + plugins: [react(), tailwindcss(), goatcounterPlugin()], server: { port: 3000, }, diff --git a/server/src/index.js b/server/src/index.js index a1af149..6f90e87 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -227,13 +227,16 @@ app.post('/api/update/apply', async (c) => { // Security headers middleware +// GC_ORIGIN (solo demo pública, issue #112): origen extra permitido en CSP +// para el tracker GoatCounter. Las instalaciones normales no lo definen. +const gcOrigin = process.env.GC_ORIGIN ? ` ${process.env.GC_ORIGIN}` : '' app.use('*', async (c, next) => { c.header('X-Content-Type-Options', 'nosniff') c.header('X-Frame-Options', 'DENY') c.header('Referrer-Policy', 'strict-origin-when-cross-origin') c.header('Permissions-Policy', 'geolocation=(), microphone=(), camera=()') c.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains') - c.header('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://api.github.com") + c.header('Content-Security-Policy', `default-src 'self'; script-src 'self' 'unsafe-inline'${gcOrigin}; style-src 'self' 'unsafe-inline'; connect-src 'self' https://api.github.com${gcOrigin}`) await next() }) const sseClients = new Set()