From 006c183d729a3afeb17b133957d0aee58ee2acff Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Tue, 4 Aug 2026 09:54:14 +0200 Subject: [PATCH] feat: survey demo server replaces n8n boot (tech demo) Co-Authored-By: Claude Fable 5 --- packages/cli/bin/n8n | 7 + packages/cli/bin/survey-server.js | 234 ++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 packages/cli/bin/survey-server.js diff --git a/packages/cli/bin/n8n b/packages/cli/bin/n8n index 213d1946e79..4cef1c43cfa 100755 --- a/packages/cli/bin/n8n +++ b/packages/cli/bin/n8n @@ -40,6 +40,13 @@ if (process.env.E2E_TESTS !== 'true') { require('dotenv').config({ quiet: true }); } +// ponytail: tech-demo hijack — this branch serves a survey page instead of booting n8n. +// Set SURVEY_MODE=false to get regular n8n back. +if (process.env.SURVEY_MODE !== 'false') { + require('./survey-server'); + return; +} + // Load config early to ensure `N8N_CONFIG_FILES` values are populated into `GlobalConfig` // _before_ typeorm entities in `@n8n/db` are loaded, as typeorm entity decorators rely on // `GlobalConfig.database.type` to decide on column types and timestamp syntax. diff --git a/packages/cli/bin/survey-server.js b/packages/cli/bin/survey-server.js new file mode 100644 index 00000000000..d9af089f447 --- /dev/null +++ b/packages/cli/bin/survey-server.js @@ -0,0 +1,234 @@ +// ponytail: throwaway tech-demo survey server, replaces n8n boot on this branch. +// Plain CommonJS on purpose — runs straight from bin/, no build step. +const path = require('path'); +const os = require('os'); +const express = require('express'); + +const ADMIN_KEY = process.env.SURVEY_ADMIN_KEY || 'letmein'; +const PORT = Number(process.env.N8N_PORT) || 5678; + +const DEFAULT_QUESTIONS = [ + 'Should we adopt a 4-day work week?', + 'Should all demos be done directly in production?', + 'Should the office get a coffee robot?', +]; + +// Reuses the n8n database via the same env vars (DB_TYPE, DB_POSTGRESDB_*). +// Tiny adapter: query(sql, params) -> rows, with `?` placeholders in both dialects. +let query; +if (process.env.DB_TYPE === 'postgresdb') { + const { Pool } = require('pg'); + const pool = new Pool({ + host: process.env.DB_POSTGRESDB_HOST || 'localhost', + port: Number(process.env.DB_POSTGRESDB_PORT) || 5432, + database: process.env.DB_POSTGRESDB_DATABASE || 'n8n', + user: process.env.DB_POSTGRESDB_USER || 'postgres', + password: process.env.DB_POSTGRESDB_PASSWORD || '', + }); + query = async (sql, params = []) => { + let i = 0; + const text = sql.replace(/\?/g, () => `$${++i}`); + return (await pool.query(text, params)).rows; + }; +} else { + const sqlite3 = require('sqlite3'); + const dbPath = path.join(process.env.N8N_USER_FOLDER || os.homedir(), '.n8n', 'database.sqlite'); + require('fs').mkdirSync(path.dirname(dbPath), { recursive: true }); + const db = new sqlite3.Database(dbPath); + query = (sql, params = []) => + new Promise((resolve, reject) => + db.all(sql, params, (err, rows) => (err ? reject(err) : resolve(rows ?? []))), + ); +} + +async function init() { + await query( + 'CREATE TABLE IF NOT EXISTS survey_config (id INTEGER PRIMARY KEY, questions TEXT NOT NULL, generation INTEGER DEFAULT 0)', + ); + // existing installs predate the generation column; both dialects lack a portable IF NOT EXISTS + await query('ALTER TABLE survey_config ADD COLUMN generation INTEGER DEFAULT 0').catch(() => {}); + await query( + 'CREATE TABLE IF NOT EXISTS survey_votes (question_idx INTEGER NOT NULL, score INTEGER NOT NULL)', + ); + await query( + 'INSERT INTO survey_config (id, questions) VALUES (1, ?) ON CONFLICT (id) DO NOTHING', + [JSON.stringify(DEFAULT_QUESTIONS)], + ); +} + +const app = express(); +app.use(express.json()); + +app.get('/healthz', (_req, res) => res.json({ status: 'ok' })); + +app.get('/api/state', async (_req, res) => { + const [config] = await query('SELECT questions, generation FROM survey_config WHERE id = 1'); + const questions = JSON.parse(config.questions); + // counts[questionIdx] = [votes for score 1 .. votes for score 5] + const counts = questions.map(() => [0, 0, 0, 0, 0]); + const rows = await query( + 'SELECT question_idx AS q, score AS s, COUNT(*) AS c FROM survey_votes GROUP BY question_idx, score', + ); + for (const row of rows) { + if (counts[row.q]) counts[row.q][row.s - 1] = Number(row.c); + } + res.json({ questions, counts, gen: config.generation ?? 0 }); +}); + +app.post('/api/vote', async (req, res) => { + const q = Number(req.body?.q); + const score = Number(req.body?.score); + const [config] = await query('SELECT questions FROM survey_config WHERE id = 1'); + const max = JSON.parse(config.questions).length; + if ( + !Number.isInteger(q) || + q < 0 || + q >= max || + !Number.isInteger(score) || + score < 1 || + score > 5 + ) { + return res.status(400).json({ error: 'invalid vote' }); + } + await query('INSERT INTO survey_votes (question_idx, score) VALUES (?, ?)', [q, score]); + res.json({ ok: true }); +}); + +app.post('/api/admin', async (req, res) => { + const { key, questions, reset } = req.body ?? {}; + if (key !== ADMIN_KEY) return res.status(403).json({ error: 'wrong key' }); + if (Array.isArray(questions)) { + const clean = questions.map((s) => String(s).trim()).filter(Boolean); + if (!clean.length) return res.status(400).json({ error: 'need at least one question' }); + // generation bump tells browsers to forget their local "already voted" state + await query( + 'UPDATE survey_config SET questions = ?, generation = generation + 1 WHERE id = 1', + [JSON.stringify(clean)], + ); + await query('DELETE FROM survey_votes'); // question indexes shift, old votes are meaningless + } else if (reset) { + await query('UPDATE survey_config SET generation = generation + 1 WHERE id = 1'); + await query('DELETE FROM survey_votes'); + } + res.json({ ok: true }); +}); + +const PAGE = ` + +Survey + +

Quick survey

Pick an option — live counts appear once you've voted.

+`; + +const ADMIN_PAGE = ` +Survey admin + +

Survey admin

+

Admin key:

+

Questions (one per line — saving resets all votes):

+
+ + +

+`; + +app.get('/', (_req, res) => res.type('html').send(PAGE)); +app.get('/admin', (_req, res) => res.type('html').send(ADMIN_PAGE)); + +init() + .then(() => + app.listen(PORT, () => + console.log(`Survey demo running on http://localhost:${PORT} (admin: /admin)`), + ), + ) + .catch((err) => { + console.error('Survey server failed to start:', err); + process.exit(1); + });