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()