commit 52000c0f5ae1315e9a1c4a6c3c65260d32d3e40b Author: Jeff Emmett Date: Wed Jan 21 11:50:14 2026 +0100 Initial commit: WORLDPLAY event website - Express server with health check - Single-page HTML site - Docker + docker-compose setup - Traefik integration for worldplay.jeffemmett.com Co-Authored-By: Claude Opus 4.5 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a7f774b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +npm-debug.log +.git +.gitignore +*.md +.env +.env.* +data/ +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d7edbb9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm install --omit=dev + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +# Copy built node_modules from builder +COPY --from=builder /app/node_modules ./node_modules + +# Copy application files +COPY package*.json ./ +COPY server.js ./ +COPY index.html ./ + +# Create data directory with proper permissions +RUN mkdir -p /app/data && chown -R nodejs:nodejs /app + +# Switch to non-root user +USER nodejs + +# Expose port +EXPOSE 3000 + +# Environment variables +ENV NODE_ENV=production +ENV PORT=3000 +ENV DATA_DIR=/app/data + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 + +# Start the server +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..01b479e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +version: '3.8' + +services: + worldplay: + build: . + container_name: worldplay-website + restart: unless-stopped + environment: + - NODE_ENV=production + - PORT=3000 + - DATA_DIR=/app/data + - ADMIN_TOKEN=${ADMIN_TOKEN:-worldplay-admin-2026} + volumes: + - worldplay-data:/app/data + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik-public" + # Main domain + - "traefik.http.routers.worldplay.rule=Host(`worldplay.jeffemmett.com`)" + - "traefik.http.routers.worldplay.entrypoints=web" + - "traefik.http.routers.worldplay.service=worldplay" + - "traefik.http.services.worldplay.loadbalancer.server.port=3000" + # Optional: Add www subdomain redirect + - "traefik.http.routers.worldplay-www.rule=Host(`www.worldplay.jeffemmett.com`)" + - "traefik.http.routers.worldplay-www.entrypoints=web" + - "traefik.http.routers.worldplay-www.middlewares=worldplay-www-redirect" + - "traefik.http.middlewares.worldplay-www-redirect.redirectregex.regex=^https://www\\.worldplay\\.jeffemmett\\.com/(.*)" + - "traefik.http.middlewares.worldplay-www-redirect.redirectregex.replacement=https://worldplay.jeffemmett.com/$${1}" + networks: + - traefik-public + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 10s + +volumes: + worldplay-data: + driver: local + +networks: + traefik-public: + external: true diff --git a/index.html b/index.html new file mode 100644 index 0000000..f3e6733 --- /dev/null +++ b/index.html @@ -0,0 +1,1868 @@ + + + + + + WORLDPLAY: To be Defined | June 7โ€“13, 2026 + + + + + + + + + + + +
+ WORLDPLAY +
+ + + + + +
+
+
+
+ + + +
+
+

WORLDPLAY

+
+

: To be Defined

+ + + +
+ +
+
+ +
+
+
+
+ +

Avant-garde revolutionaries LARPing as commons economists

+ +

WORLDPLAY is a pop-up physical hub for prefiguring postcapitalist futures through fiction, performance and playโ€”the opening move of a new event series and emerging network.

+ +

A beckoning call to sci-fi writers, game-makers, artivists, LARPers, weird economists and eutopian dreamers to collectively scribble futures, materialise speculative artefacts, and devise ways of hijacking public spaces, cyberspace and eventually reality itself.

+
+ +
+
+ ๐ŸŽญ + Reality as
design space +
+
+ โœ’๏ธ + Speculative
fictions +
+
+ ๐ŸŽฒ + Games as
social orgs +
+
+
+
+
+ +
+
+
+ +

Co-Created Programme

+

Partly curated, partly self-organised unconference styleโ€”emphasising lasting peer collaboration.

+
+ +
+
+ ๐ŸŽค +

Shape the Programme

+

Pitch sessions and co-labs in advance and on-site

+
+
+ ๐Ÿ“š +

Digital Publishing

+

Partner with orgs like Institute of Network Cultures

+
+
+ ๐ŸŽฎ +

Game Prototyping

+

Selected games resourced and shared as open designs

+
+
+ ๐Ÿ”— +

Distributed Network

+

Stay connected through shared project threads

+
+
+ ๐Ÿ’ฐ +

Sustainable Models

+

Co-ops, art DAOs, fiction-fueled crowdfunds

+
+
+ ๐ŸŒ +

Open Source Worlds

+

Co-author worlds through games and experiments

+
+
+
+
+ +
+
+
+ +

Interwoven Threads

+

Plus reality-bending games, unfinished stories, LARPs, and experiments you bring.

+
+ +
+
+ ๐ŸŽญ +

Playing with Reality

+
+
+
+

Culture and social conventions as reprogrammable design spaces. Interventions and reality hacks blurring performance and politics.

+
+
+

Inspirations

+ The Yes Men + Bureau of Inverse Technology + Billboard Liberation Front + Center for Political Beauty + Situationist International +
+
+
+ +
+
+ โœ’๏ธ +

Socio-Economic Science Fictions

+
+
+
+
    +
  • Hyperstitional fictions grounded in prefigurative politics
  • +
  • Peer-supported speculative writing workshops
  • +
  • Co-authoring worlds through experimental formats
  • +
+
+
+

Inspirations

+ Walkaway + Ministry for the Future + Multispecies Cities + Everything for Everyone +
+
+
+ +
+
+ ๐Ÿ›  +

Guerrilla Futuring

+
+
+
+
    +
  • Artefacts from parallel/future realities
  • +
  • Soft LARPs and worldbuilding exercises
  • +
  • Street interventions and commonist propaganda
  • +
+
+
+

Inspirations

+ Futurematic + Treaty of Finsbury Park + Queer Embassy of Possible Futures + NOVA: Future Thoughts +
+
+
+ +
+
+ ๐ŸŽฒ +

Tabletop & Game Commons

+
+
+
+
    +
  • Radical analogue and digital/hybrid games
  • +
  • Prototyping collectively-owned game artefacts and mechanics
  • +
+
+
+

Inspirations

+ Half-Earth Socialism + Post-Growth Toolkit + Social Strike Game + The Transition Year +
+
+
+ +
+
+ ๐ŸŒฑ +

Infrastructures for Imagination

+
+
+
+
    +
  • Open-source world-making platforms
  • +
  • Anticipatory fiction archives
  • +
  • Connecting distributed nodes of practice
  • +
+
+
+

Inspirations

+ Witnesspedia + POCAS + Nordic Larp Wiki +
+
+
+
+
+ +
+
+
+ +

When & Where

+
+ +
+
+
+
+ ๐Ÿ“… +
+ June 7โ€“13, 2026 + 7 days of worldbuilding +
+
+
+ ๐Ÿ“ +
+ Hirschwang an der Rax, Austria + Austrian Alps, ~1.5 hours from Vienna by train +
+
+
+ ๐Ÿ  +
+ Commons Hub + Co-working/co-living venue for artists and decentralized communities +
+
+
+
+ +
+ ๐Ÿ”๏ธ +
+ ๐Ÿ‡ฆ๐Ÿ‡น Austria ยท Alps ยท Commons Hub +
+
+
+
+
+ +
+
+
+ +

Register Interest

+

First edition. Limited spaces.

+
+ +
+
+

Early registration gets you

+ +
    +
  • + โœ“ + Priority booking when tickets open +
  • +
  • + โœ“ + Pitch sessions and co-labs +
  • +
  • + โœ“ + Pre-event online gatherings +
  • +
  • + โœ“ + Programme development updates +
  • +
+ +

Who should attend?

+

Sci-fi writers, game-makers, artivists, LARPers, weird economists, futurists, performers, eutopian dreamers.

+
+ +
+
+

Express Interest

+

We'll be in touch with next steps

+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + + + + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+
+
+
+ +
+
+

Ready to Play?

+

Join fellow dreamers, makers, and reality-benders in prefiguring postcapitalist futures.

+ +
+
+ +
+
+ + +
+
+ + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..110ea25 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "worldplay-website", + "version": "1.0.0", + "description": "WORLDPLAY event website - prefiguring postcapitalist futures through fiction, performance and play", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js" + }, + "keywords": [ + "worldplay", + "event", + "commons", + "postcapitalism", + "futures" + ], + "author": "WORLDPLAY Collective", + "license": "MIT", + "dependencies": { + "express": "^4.18.2" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..a4d9ca2 --- /dev/null +++ b/server.js @@ -0,0 +1,208 @@ +const express = require('express'); +const fs = require('fs').promises; +const path = require('path'); + +const app = express(); +const PORT = process.env.PORT || 3000; +const DATA_DIR = process.env.DATA_DIR || './data'; +const REGISTRATIONS_FILE = path.join(DATA_DIR, 'registrations.json'); + +// Middleware +app.use(express.json()); +app.use(express.static('.')); + +// Ensure data directory exists +async function ensureDataDir() { + try { + await fs.mkdir(DATA_DIR, { recursive: true }); + try { + await fs.access(REGISTRATIONS_FILE); + } catch { + await fs.writeFile(REGISTRATIONS_FILE, '[]'); + } + } catch (error) { + console.error('Error creating data directory:', error); + } +} + +// Load registrations +async function loadRegistrations() { + try { + const data = await fs.readFile(REGISTRATIONS_FILE, 'utf8'); + return JSON.parse(data); + } catch { + return []; + } +} + +// Save registrations +async function saveRegistrations(registrations) { + await fs.writeFile(REGISTRATIONS_FILE, JSON.stringify(registrations, null, 2)); +} + +// Registration endpoint +app.post('/api/register', async (req, res) => { + try { + const { firstName, lastName, email, location, role, interests, contribute, message } = req.body; + + // Validation + if (!firstName || !lastName || !email) { + return res.status(400).json({ error: 'First name, last name, and email are required' }); + } + + if (!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { + return res.status(400).json({ error: 'Please provide a valid email address' }); + } + + // Load existing registrations + const registrations = await loadRegistrations(); + + // Check for duplicate email + if (registrations.some(r => r.email.toLowerCase() === email.toLowerCase())) { + return res.status(400).json({ error: 'This email is already registered' }); + } + + // Create new registration + const registration = { + id: Date.now().toString(36) + Math.random().toString(36).substr(2), + firstName: firstName.trim(), + lastName: lastName.trim(), + email: email.toLowerCase().trim(), + location: location?.trim() || '', + role: role || '', + interests: interests || [], + contribute: contribute || '', + message: message?.trim() || '', + registeredAt: new Date().toISOString(), + ipAddress: req.ip || req.connection.remoteAddress + }; + + // Save registration + registrations.push(registration); + await saveRegistrations(registrations); + + console.log(`New registration: ${registration.firstName} ${registration.lastName} <${registration.email}>`); + + res.json({ + success: true, + message: 'Registration successful', + id: registration.id + }); + + } catch (error) { + console.error('Registration error:', error); + res.status(500).json({ error: 'An error occurred. Please try again.' }); + } +}); + +// Admin endpoint to view registrations (protected by simple token) +app.get('/api/registrations', async (req, res) => { + const token = req.headers['x-admin-token'] || req.query.token; + const adminToken = process.env.ADMIN_TOKEN || 'worldplay-admin-2026'; + + if (token !== adminToken) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + try { + const registrations = await loadRegistrations(); + res.json({ + count: registrations.length, + registrations: registrations.map(r => ({ + ...r, + ipAddress: undefined // Don't expose IP in admin view + })) + }); + } catch (error) { + res.status(500).json({ error: 'Failed to load registrations' }); + } +}); + +// Export registrations as CSV +app.get('/api/registrations/export', async (req, res) => { + const token = req.headers['x-admin-token'] || req.query.token; + const adminToken = process.env.ADMIN_TOKEN || 'worldplay-admin-2026'; + + if (token !== adminToken) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + try { + const registrations = await loadRegistrations(); + + const headers = ['ID', 'First Name', 'Last Name', 'Email', 'Location', 'Role', 'Interests', 'Contribute', 'Message', 'Registered At']; + const rows = registrations.map(r => [ + r.id, + r.firstName, + r.lastName, + r.email, + r.location, + r.role, + Array.isArray(r.interests) ? r.interests.join('; ') : r.interests, + r.contribute, + r.message.replace(/"/g, '""'), + r.registeredAt + ]); + + const csv = [ + headers.join(','), + ...rows.map(row => row.map(cell => `"${cell}"`).join(',')) + ].join('\n'); + + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', 'attachment; filename=worldplay-registrations.csv'); + res.send(csv); + } catch (error) { + res.status(500).json({ error: 'Failed to export registrations' }); + } +}); + +// Health check +app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// Stats endpoint +app.get('/api/stats', async (req, res) => { + try { + const registrations = await loadRegistrations(); + + const stats = { + totalRegistrations: registrations.length, + byRole: {}, + byInterest: {}, + byContribute: {} + }; + + registrations.forEach(r => { + // Count by role + if (r.role) { + stats.byRole[r.role] = (stats.byRole[r.role] || 0) + 1; + } + + // Count by interest + if (Array.isArray(r.interests)) { + r.interests.forEach(interest => { + stats.byInterest[interest] = (stats.byInterest[interest] || 0) + 1; + }); + } + + // Count by contribute + if (r.contribute) { + stats.byContribute[r.contribute] = (stats.byContribute[r.contribute] || 0) + 1; + } + }); + + res.json(stats); + } catch (error) { + res.status(500).json({ error: 'Failed to calculate stats' }); + } +}); + +// Start server +ensureDataDir().then(() => { + app.listen(PORT, '0.0.0.0', () => { + console.log(`WORLDPLAY server running on port ${PORT}`); + console.log(`Admin token: ${process.env.ADMIN_TOKEN || 'worldplay-admin-2026'}`); + }); +});