diff --git a/arbetsgrupp-live/Containerfile b/arbetsgrupp-live/Containerfile
new file mode 100644
index 0000000..5a14720
--- /dev/null
+++ b/arbetsgrupp-live/Containerfile
@@ -0,0 +1,28 @@
+FROM node:current
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+#RUN npm ci --omit=dev
+RUN npm install -g pnpm
+RUN pnpm pkg set pnpm.onlyBuiltDependencies[0]=better-sqlite3
+RUN pnpm install
+
+# Copy application code
+COPY . .
+
+# Create data directory for SQLite
+RUN mkdir -p /app/data
+
+# Expose port
+EXPOSE 3000
+
+# Set environment variables
+ENV NODE_ENV=production
+ENV PORT=3000
+
+# Start the application
+CMD ["pnpm", "start"]
\ No newline at end of file
diff --git a/arbetsgrupp-live/README.md b/arbetsgrupp-live/README.md
new file mode 100644
index 0000000..a9da83f
--- /dev/null
+++ b/arbetsgrupp-live/README.md
@@ -0,0 +1,60 @@
+# Live Audience Response System
+
+Ett webbbaserat system för att samla in svar från en live-publik och visa resultaten i realtid. Systemet är byggt med Node.js, Socket.IO och SQLite, och kan köras i en Podman-container.
+
+## Funktioner
+
+- **Live-formulär**: Deltagare kan välja mellan förutbestämda alternativ eller lägga till egna
+- **Flerval**: Användare kan välja flera alternativ
+- **Dynamiska alternativ**: Nya alternativ som läggs till av användare blir automatiskt tillgängliga för andra
+- **Live-resultat**: Admin-panel med realtidsuppdateringar av resultat
+- **QR-kod**: Genereras automatiskt för enkel delning av formuläret
+- **Responsiv design**: Fungerar på både desktop och mobil
+- **Persistent data**: SQLite-databas för säker datalagring
+
+## Teknisk stack
+
+### Backend
+- **Node.js** med Express.js
+- **Socket.IO** för realtidskommunikation
+- **SQLite** för datalagring
+- **QR Code generator** för QR-kodgenerering
+
+### Frontend
+- **Vanilla JavaScript** med Socket.IO client
+- **Chart.js** för datavisualisering
+- **Responsiv CSS** med modern design
+
+### Container
+- **Podman** (eller Docker) för containerisering
+- **Multi-stage build** för optimerad bildstorlek
+
+
+# Kör applikationen
+
+```bash
+podman run -d --restart=on-failure --name aeroklubben-arbetsgrupp-live -p 7000:3000 -v ./data:/app/data:Z aeroklubben-arbetsgrupp-live
+```
+
+## Användning
+
+### För deltagare
+1. Besök huvudsidan: `https://aeroklubben.hostux.fr`
+2. Välj ett eller flera alternativ
+3. Lägg till egna alternativ om önskat
+4. Skicka svaret
+
+### För administratör
+1. Besök admin-panelen: `https://aeroklubben.hostux.fr/admin`
+2. Visa QR-koden för deltagare
+3. Följ live-resultat i realtid
+4. Se statistik och diagram
+
+## API-endpoints
+
+- `GET /` - Huvudformulär
+- `GET /admin` - Admin-panel
+- `GET /api/options` - Hämta tillgängliga alternativ
+- `GET /api/results` - Hämta resultat
+- `POST /api/submit` - Skicka svar
+- `GET /api/qr` - Hämta QR-kod
\ No newline at end of file
diff --git a/arbetsgrupp-live/build.sh b/arbetsgrupp-live/build.sh
new file mode 100755
index 0000000..9b97a85
--- /dev/null
+++ b/arbetsgrupp-live/build.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# On error, exit the script
+set -e
+
+# Build script for the Live Audience Response System
+
+echo "🚀 Building Live Audience Response System..."
+
+# Build the container image
+echo "📦 Building Podman container..."
+podman build -t aeroklubben-arbetsgrupp-live .
+
+# Create data directory on host (for persistence)
+echo "📁 Creating data directory..."
+mkdir -p ./data
+
+echo "✅ Build complete!"
+echo ""
+echo "To run the application:"
+echo " podman run -d --restart=on-failure --name aeroklubben-arbetsgrupp-live -p 7000:3000 -v ./data:/app/data:Z aeroklubben-arbetsgrupp-live"
+echo ""
+echo "To view logs:"
+echo " podman logs -f aeroklubben-arbetsgrupp-live"
+echo ""
+echo "To stop:"
+echo " podman stop aeroklubben-arbetsgrupp-live"
+echo ""
+echo "To remove:"
+echo " podman rm aeroklubben-arbetsgrupp-live"
+echo ""
+echo "Access URLs:"
+echo " Form: https://aeroklubben.hostux.fr"
+echo " Admin: https://aeroklubben.hostux.fr/admin"
\ No newline at end of file
diff --git a/arbetsgrupp-live/data/responses.db b/arbetsgrupp-live/data/responses.db
new file mode 100644
index 0000000..4b95fc9
Binary files /dev/null and b/arbetsgrupp-live/data/responses.db differ
diff --git a/arbetsgrupp-live/data/responses.db-shm b/arbetsgrupp-live/data/responses.db-shm
new file mode 100644
index 0000000..742955b
Binary files /dev/null and b/arbetsgrupp-live/data/responses.db-shm differ
diff --git a/arbetsgrupp-live/data/responses.db-wal b/arbetsgrupp-live/data/responses.db-wal
new file mode 100644
index 0000000..a12f004
Binary files /dev/null and b/arbetsgrupp-live/data/responses.db-wal differ
diff --git a/arbetsgrupp-live/package.json b/arbetsgrupp-live/package.json
new file mode 100644
index 0000000..9e009ed
--- /dev/null
+++ b/arbetsgrupp-live/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "aeroklubben-arbetsgrupp-live",
+ "version": "1.0.0",
+ "description": "Live audience response collection system",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "nodemon server.js"
+ },
+ "dependencies": {
+ "better-sqlite3": "^12.2.0",
+ "cors": "^2.8.5",
+ "express": "^5.1.0",
+ "qrcode": "^1.5.3",
+ "socket.io": "^4.7.2"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.1"
+ },
+ "keywords": [
+ "audience",
+ "response",
+ "live",
+ "polling"
+ ],
+ "author": "",
+ "license": "MIT",
+ "pnpm": {
+ "onlyBuiltDependencies": [
+ "better-sqlite3"
+ ]
+ }
+}
diff --git a/arbetsgrupp-live/public/admin.html b/arbetsgrupp-live/public/admin.html
new file mode 100644
index 0000000..dd4b930
--- /dev/null
+++ b/arbetsgrupp-live/public/admin.html
@@ -0,0 +1,704 @@
+
+
+
+
+
+ Live Resultat - Arbetsgrupper
+
+
+
+
+
+
+
+
+
Scanna för att svara
+
+
+
+ Laddar URL...
+
+
+
+
+
+
+
+
+ Live Resultat
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/arbetsgrupp-live/public/aeroklubben.png b/arbetsgrupp-live/public/aeroklubben.png
new file mode 100644
index 0000000..6db8eb9
Binary files /dev/null and b/arbetsgrupp-live/public/aeroklubben.png differ
diff --git a/arbetsgrupp-live/public/index.html b/arbetsgrupp-live/public/index.html
new file mode 100644
index 0000000..066f858
--- /dev/null
+++ b/arbetsgrupp-live/public/index.html
@@ -0,0 +1,397 @@
+
+
+
+
+
+ Arbetsgrupp - Deltagarformulär
+
+
+
+
+
+

+
Vi behöver DIG!
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/arbetsgrupp-live/server.js b/arbetsgrupp-live/server.js
new file mode 100644
index 0000000..fea4514
--- /dev/null
+++ b/arbetsgrupp-live/server.js
@@ -0,0 +1,258 @@
+const express = require('express');
+const http = require('http');
+const socketIo = require('socket.io');
+const Database = require('better-sqlite3');
+const QRCode = require('qrcode');
+const path = require('path');
+const cors = require('cors');
+
+const app = express();
+const server = http.createServer(app);
+const io = socketIo(server, {
+ cors: {
+ origin: "*",
+ methods: ["GET", "POST"]
+ }
+});
+
+const PORT = process.env.PORT || 3000;
+
+// Middleware
+app.use(cors());
+app.use(express.json());
+app.use(express.static('public'));
+
+// Initialize better-sqlite3 database
+const db = new Database('./data/responses.db');
+
+// Configure database for better performance
+db.pragma('journal_mode = WAL');
+db.pragma('synchronous = NORMAL');
+db.pragma('cache_size = 1000000');
+db.pragma('temp_store = memory');
+
+// Initialize database tables
+try {
+ // Create responses table
+ db.exec(`CREATE TABLE IF NOT EXISTS responses (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ participant_name TEXT NOT NULL,
+ response TEXT NOT NULL,
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
+ )`);
+
+ // Create options table for dynamic options
+ db.exec(`CREATE TABLE IF NOT EXISTS options (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ option_text TEXT UNIQUE NOT NULL,
+ is_predefined BOOLEAN DEFAULT FALSE,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )`);
+
+ // Insert predefined options
+ const predefinedOptions = ['Landvetter', 'Backamo', 'Borås', 'Trollhättan'];
+ const insertOption = db.prepare("INSERT OR IGNORE INTO options (option_text, is_predefined) VALUES (?, 1)");
+
+ predefinedOptions.forEach(option => {
+ insertOption.run(option);
+ });
+} catch (error) {
+ console.error('Database initialization error:', error);
+}
+
+// Prepared statements for better performance
+const getOptionsStmt = db.prepare("SELECT option_text FROM options ORDER BY is_predefined DESC, created_at ASC");
+const getResponseCountsStmt = db.prepare(`
+ SELECT response, COUNT(*) as count
+ FROM responses
+ GROUP BY response
+ ORDER BY response ASC
+`);
+const getAllResponsesStmt = db.prepare(`
+ SELECT participant_name, response, timestamp
+ FROM responses
+ ORDER BY timestamp DESC
+`);
+const insertResponseStmt = db.prepare("INSERT INTO responses (participant_name, response) VALUES (?, ?)");
+const insertOptionStmt = db.prepare("INSERT OR IGNORE INTO options (option_text) VALUES (?)");
+
+// Get all available options
+function getOptions() {
+ try {
+ return getOptionsStmt.all().map(row => row.option_text);
+ } catch (error) {
+ console.error('Error getting options:', error);
+ return [];
+ }
+}
+
+// Get response counts
+function getResponseCounts() {
+ try {
+ return getResponseCountsStmt.all();
+ } catch (error) {
+ console.error('Error getting response counts:', error);
+ return [];
+ }
+}
+
+// Get all responses with participant names
+function getAllResponses() {
+ try {
+ return getAllResponsesStmt.all();
+ } catch (error) {
+ console.error('Error getting all responses:', error);
+ return [];
+ }
+}
+
+// Routes
+app.get('/', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
+});
+
+app.get('/admin', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'admin.html'));
+});
+
+app.get('/api/options', (req, res) => {
+ try {
+ const options = getOptions();
+ res.json(options);
+ } catch (error) {
+ console.error('API error - options:', error);
+ res.status(500).json({ error: 'Failed to fetch options' });
+ }
+});
+
+app.get('/api/results', (req, res) => {
+ try {
+ const results = getResponseCounts();
+ res.json(results);
+ } catch (error) {
+ console.error('API error - results:', error);
+ res.status(500).json({ error: 'Failed to fetch results' });
+ }
+});
+
+app.get('/api/all-responses', (req, res) => {
+ try {
+ const responses = getAllResponses();
+ res.json(responses);
+ } catch (error) {
+ console.error('API error - all responses:', error);
+ res.status(500).json({ error: 'Failed to fetch responses' });
+ }
+});
+
+app.post('/api/submit', (req, res) => {
+ const { responses, participantName } = req.body;
+
+ if (!responses || !Array.isArray(responses) || responses.length === 0) {
+ return res.status(400).json({ error: 'Invalid responses' });
+ }
+
+ if (!participantName || typeof participantName !== 'string' || participantName.trim().length === 0) {
+ return res.status(400).json({ error: 'Participant name is required' });
+ }
+
+ const trimmedName = participantName.trim();
+
+ try {
+ // Use a transaction for better performance and data integrity
+ const insertTransaction = db.transaction((responses, participantName) => {
+ // Insert all responses
+ responses.forEach(response => {
+ insertResponseStmt.run(participantName, response);
+ // Add new custom options to options table
+ insertOptionStmt.run(response);
+ });
+ });
+
+ insertTransaction(responses, trimmedName);
+
+ // Emit updated data to all clients
+ const options = getOptions();
+ io.emit('optionsUpdated', options);
+
+ const results = getResponseCounts();
+ io.emit('resultsUpdated', results);
+
+ const allResponses = getAllResponses();
+ io.emit('allResponsesUpdated', allResponses);
+
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Error submitting response:', error);
+ res.status(500).json({ error: 'Failed to submit response' });
+ }
+});
+
+app.get('/api/qr', async (req, res) => {
+ try {
+ const url = `https://aeroklubben.hostux.fr`;
+ const qrCode = await QRCode.toDataURL(url);
+ res.json({ qrCode });
+ } catch (err) {
+ res.status(500).json({ error: 'Failed to generate QR code' });
+ }
+});
+
+// Socket.IO connection handling
+io.on('connection', (socket) => {
+ console.log('Client connected');
+
+ try {
+ // Send current options and results to new client
+ const options = getOptions();
+ socket.emit('optionsUpdated', options);
+
+ const results = getResponseCounts();
+ socket.emit('resultsUpdated', results);
+
+ const allResponses = getAllResponses();
+ socket.emit('allResponsesUpdated', allResponses);
+ } catch (error) {
+ console.error('Error sending initial data to client:', error);
+ }
+
+ socket.on('disconnect', () => {
+ console.log('Client disconnected');
+ });
+});
+
+// Start server
+server.listen(PORT, () => {
+ console.log(`Server running on port ${PORT}`);
+ console.log(`Form: https://aeroklubben.hostux.fr`);
+ console.log(`Admin: https://aeroklubben.hostux.fr/admin`);
+});
+
+// Graceful shutdown
+process.on('SIGINT', () => {
+ console.log('Shutting down gracefully...');
+ try {
+ db.close();
+ console.log('Database connection closed.');
+ } catch (error) {
+ console.error('Error closing database:', error);
+ }
+ server.close(() => {
+ console.log('Server closed.');
+ process.exit(0);
+ });
+});
+
+process.on('SIGTERM', () => {
+ console.log('Received SIGTERM, shutting down gracefully...');
+ try {
+ db.close();
+ console.log('Database connection closed.');
+ } catch (error) {
+ console.error('Error closing database:', error);
+ }
+ server.close(() => {
+ console.log('Server closed.');
+ process.exit(0);
+ });
+});
\ No newline at end of file