From 358a37503323f2557ef841ccdefe8d2ec8b414e3 Mon Sep 17 00:00:00 2001 From: Aliasocracy Date: Sun, 23 Nov 2025 18:21:20 -0600 Subject: [PATCH 1/4] feat: Add AI Council Proxy UI and Docker configuration - Introduced a new UI service in docker-compose.yml with environment variables for database and Redis configuration. - Added Dockerfile.ui for building the UI with multi-stage setup, including health checks. - Implemented the UI server entry point in src/ui-server.ts, establishing connections to PostgreSQL and Redis. - Enhanced UserInterface class to use a longer default API key for testing purposes. - Created a new API keys table in the database schema for authentication management. This update sets up a complete UI service for the AI Council Proxy, improving user interaction and system integration. --- Dockerfile.ui | 52 ++++++++++++++++++++++ database/schema.sql | 19 +++++++- docker-compose.yml | 46 ++++++++++++++++++++ src/ui-server.ts | 104 ++++++++++++++++++++++++++++++++++++++++++++ src/ui/interface.ts | 7 +-- 5 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 Dockerfile.ui create mode 100644 src/ui-server.ts diff --git a/Dockerfile.ui b/Dockerfile.ui new file mode 100644 index 0000000..655db67 --- /dev/null +++ b/Dockerfile.ui @@ -0,0 +1,52 @@ +# Build stage +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY tsconfig.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY src ./src + +# Build TypeScript +RUN npm run build + +# Production stage +FROM node:18-alpine + +WORKDIR /app + +# Install curl for healthcheck +RUN apk add --no-cache curl + +# Copy package files +COPY package*.json ./ + +# Install production dependencies only +RUN npm ci --only=production + +# Copy built application from builder +COPY --from=builder /app/dist ./dist + +# Copy database schema for reference +COPY database ./database + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +USER nodejs + +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:8080/ || exit 1 + +CMD ["node", "dist/ui-server.js"] + diff --git a/database/schema.sql b/database/schema.sql index 94cc129..77f7616 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -239,4 +239,21 @@ CREATE TABLE devils_advocate_logs ( ); CREATE INDEX idx_devils_advocate_logs_request_id ON devils_advocate_logs(request_id); -CREATE INDEX idx_devils_advocate_logs_created_at ON devils_advocate_logs(created_at); \ No newline at end of file +CREATE INDEX idx_devils_advocate_logs_created_at ON devils_advocate_logs(created_at); + +-- ============================================================================ +-- API Keys table (for API authentication) +-- ============================================================================ +CREATE TABLE api_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id VARCHAR(255) NOT NULL, + key_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 hash of the API key + active BOOLEAN NOT NULL DEFAULT TRUE, + expires_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_used_at TIMESTAMP +); + +CREATE INDEX idx_api_keys_key_hash ON api_keys(key_hash); +CREATE INDEX idx_api_keys_user_id ON api_keys(user_id); +CREATE INDEX idx_api_keys_active ON api_keys(active); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9ac850e..a6036fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -99,6 +99,52 @@ services: retries: 3 start_period: 40s + # AI Council Proxy UI + ui: + build: + context: . + dockerfile: Dockerfile.ui + container_name: council-ui + environment: + NODE_ENV: ${NODE_ENV:-production} + + # Database + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: ${DATABASE_NAME:-ai_council_proxy} + DATABASE_USER: ${DATABASE_USER:-postgres} + DATABASE_PASSWORD: ${DATABASE_PASSWORD:-postgres} + DATABASE_URL: postgresql://${DATABASE_USER:-postgres}:${DATABASE_PASSWORD:-postgres}@postgres:5432/${DATABASE_NAME:-ai_council_proxy} + + # Redis + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_URL: redis://redis:6379 + + # UI Configuration + UI_PORT: ${UI_PORT:-8080} + # API_BASE_URL defaults to http://localhost:3000 in code (for browser access) + # Override if needed for different deployment scenarios + API_BASE_URL: ${API_BASE_URL} + ports: + - "${UI_PORT:-8080}:8080" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + api: + condition: service_healthy + restart: unless-stopped + networks: + - council-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + volumes: postgres_data: driver: local diff --git a/src/ui-server.ts b/src/ui-server.ts new file mode 100644 index 0000000..5750242 --- /dev/null +++ b/src/ui-server.ts @@ -0,0 +1,104 @@ +/** + * UI Server Entry Point + * Starts the User Interface web server + */ + +import { Pool } from 'pg'; +import { createClient } from 'redis'; +import { UserInterface } from './ui/interface'; +import { ConfigurationManager } from './config/manager'; + +async function startUIServer() { + console.log('Starting AI Council Proxy UI...'); + + // Initialize database connection + const pool = new Pool({ + host: process.env.DATABASE_HOST || 'localhost', + port: parseInt(process.env.DATABASE_PORT || '5432'), + database: process.env.DATABASE_NAME || 'ai_council_proxy', + user: process.env.DATABASE_USER || 'postgres', + password: process.env.DATABASE_PASSWORD || 'postgres' + }); + + console.log('Connecting to PostgreSQL...'); + try { + await pool.query('SELECT 1'); + console.log('✓ PostgreSQL connected'); + } catch (error) { + console.error('Failed to connect to PostgreSQL:', error); + process.exit(1); + } + + // Initialize Redis client + const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; + console.log('Connecting to Redis...'); + const redis = createClient({ url: redisUrl }); + + redis.on('error', (err) => { + console.error('Redis error:', err); + }); + + try { + await redis.connect(); + console.log('✓ Redis connected'); + } catch (error) { + console.error('Failed to connect to Redis:', error); + process.exit(1); + } + + // Initialize ConfigurationManager + const configManager = new ConfigurationManager(pool, redis as any); + + // Get API base URL from environment or default to localhost (for browser access) + // Note: In Docker, this should be set via API_BASE_URL env var to http://localhost:3000 + // so the browser (running on host) can access the API + const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3000'; + const uiPort = parseInt(process.env.UI_PORT || '8080'); + + // Start User Interface + const ui = new UserInterface(configManager, apiBaseUrl); + + console.log(`Starting UI server on port ${uiPort}...`); + await ui.start(uiPort); + console.log(`✓ UI server running on http://0.0.0.0:${uiPort}`); + console.log(`✓ API Gateway URL: ${apiBaseUrl}`); + + // Handle graceful shutdown + const shutdown = async (signal: string) => { + console.log(`\n${signal} received. Shutting down gracefully...`); + try { + await ui.stop(); + console.log('✓ UI server stopped'); + await redis.disconnect(); + console.log('✓ Redis disconnected'); + await pool.end(); + console.log('✓ PostgreSQL disconnected'); + console.log('Shutdown complete'); + process.exit(0); + } catch (error) { + console.error('Error during shutdown:', error); + process.exit(1); + } + }; + + process.on('SIGTERM', () => void shutdown('SIGTERM')); + process.on('SIGINT', () => void shutdown('SIGINT')); + + // Handle uncaught errors + process.on('uncaughtException', (error) => { + console.error('Uncaught exception:', error); + void shutdown('UNCAUGHT_EXCEPTION'); + }); + + process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled rejection at:', promise, 'reason:', reason); + void shutdown('UNHANDLED_REJECTION'); + }); +} + +// Start the UI server +startUIServer().catch((error) => { + console.error('Failed to start UI server:', error); + process.exit(1); +}); + diff --git a/src/ui/interface.ts b/src/ui/interface.ts index c07bfb6..c67c2d4 100644 --- a/src/ui/interface.ts +++ b/src/ui/interface.ts @@ -448,7 +448,8 @@ export class UserInterface { try { // Get API key from localStorage (in production, use proper auth) - const apiKey = localStorage.getItem('apiKey') || 'demo-api-key'; + // Default key must be at least 32 characters for production mode + const apiKey = localStorage.getItem('apiKey') || 'demo-api-key-for-testing-purposes-only-12345678901234567890'; // Submit request const response = await fetch(\`\${config.apiBaseUrl}/api/v1/requests\`, { @@ -486,7 +487,7 @@ export class UserInterface { // Start streaming response function startStreaming(requestId) { - const apiKey = localStorage.getItem('apiKey') || 'demo-api-key'; + const apiKey = localStorage.getItem('apiKey') || 'demo-api-key-for-testing-purposes-only-12345678901234567890'; eventSource = new EventSource( \`\${config.apiBaseUrl}/api/v1/requests/\${requestId}/stream?apiKey=\${apiKey}\` @@ -524,7 +525,7 @@ export class UserInterface { // Poll for response (fallback) async function pollForResponse(requestId) { - const apiKey = localStorage.getItem('apiKey') || 'demo-api-key'; + const apiKey = localStorage.getItem('apiKey') || 'demo-api-key-for-testing-purposes-only-12345678901234567890'; const maxAttempts = 60; // 60 seconds let attempts = 0; From f7085ab5396f29c3379d218f67a4474c1c17cece Mon Sep 17 00:00:00 2001 From: Aliasocracy Date: Sun, 23 Nov 2025 18:27:28 -0600 Subject: [PATCH 2/4] feat: Enable pgcrypto extension for UUID generation in database schema - Added the pgcrypto extension to support the gen_random_uuid() function for generating UUIDs. - Updated the API keys table definition to utilize the new UUID generation method for the primary key. This enhancement improves the database schema by ensuring unique identifiers for API keys. --- database/schema.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/database/schema.sql b/database/schema.sql index 77f7616..1e76d9e 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -244,6 +244,9 @@ CREATE INDEX idx_devils_advocate_logs_created_at ON devils_advocate_logs(created -- ============================================================================ -- API Keys table (for API authentication) -- ============================================================================ +-- Enable pgcrypto extension for gen_random_uuid() function +CREATE EXTENSION IF NOT EXISTS pgcrypto; + CREATE TABLE api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id VARCHAR(255) NOT NULL, From d9155fff11b805057c02a02cf3a6827bebe23772 Mon Sep 17 00:00:00 2001 From: Aliasocracy Date: Sun, 23 Nov 2025 18:31:59 -0600 Subject: [PATCH 3/4] feat: Add demo API key seeding to database schema - Introduced a seed entry for a demo API key in the database schema to facilitate development and testing. - Added an index on the 'active' column in the api_keys table for improved query performance. This enhancement supports easier testing and development workflows by providing a pre-configured API key. --- database/schema.sql | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/database/schema.sql b/database/schema.sql index 1e76d9e..6ed2260 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -259,4 +259,11 @@ CREATE TABLE api_keys ( CREATE INDEX idx_api_keys_key_hash ON api_keys(key_hash); CREATE INDEX idx_api_keys_user_id ON api_keys(user_id); -CREATE INDEX idx_api_keys_active ON api_keys(active); \ No newline at end of file +CREATE INDEX idx_api_keys_active ON api_keys(active); + +-- Seed demo API key for UI (development/testing only) +-- Key: demo-api-key-for-testing-purposes-only-12345678901234567890 +-- Hash: 52346957575b04c715942a324887efde06f034ca62893fa6a76064d7f65f8e43 +INSERT INTO api_keys (user_id, key_hash, active) +VALUES ('demo-user', '52346957575b04c715942a324887efde06f034ca62893fa6a76064d7f65f8e43', true) +ON CONFLICT (key_hash) DO NOTHING; \ No newline at end of file From 6fbf08e6f658a557c62eb0bbd03ea4294a417a14 Mon Sep 17 00:00:00 2001 From: Aliasocracy Date: Sun, 23 Nov 2025 18:39:10 -0600 Subject: [PATCH 4/4] feat: Enhance graceful shutdown handling in server and UI server - Updated shutdown functions in both src/server.ts and src/ui-server.ts to differentiate between normal and error-induced shutdowns. - Added an optional parameter to the shutdown function to log appropriate messages and exit codes based on the shutdown reason. - Ensured that uncaught exceptions and unhandled rejections trigger the error shutdown process. This improvement enhances the robustness of the application by providing clearer logging and proper exit codes during shutdown scenarios. --- src/server.ts | 18 +++++++++++------- src/ui-server.ts | 20 ++++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/server.ts b/src/server.ts index 7f7d771..b1d70c5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -82,8 +82,12 @@ async function startServer() { console.log(`\nHealth check: http://localhost:${port}/health`); // Handle graceful shutdown - const shutdown = async (signal: string) => { - console.log(`\n${signal} received. Shutting down gracefully...`); + const shutdown = async (signal: string, isError: boolean = false) => { + if (isError) { + console.error(`\n${signal} received. Shutting down due to error...`); + } else { + console.log(`\n${signal} received. Shutting down gracefully...`); + } try { await apiGateway.stop(); console.log('✓ API Gateway stopped'); @@ -92,25 +96,25 @@ async function startServer() { await pool.end(); console.log('✓ PostgreSQL disconnected'); console.log('Shutdown complete'); - process.exit(0); + process.exit(isError ? 1 : 0); } catch (error) { console.error('Error during shutdown:', error); process.exit(1); } }; - process.on('SIGTERM', () => void shutdown('SIGTERM')); - process.on('SIGINT', () => void shutdown('SIGINT')); + process.on('SIGTERM', () => void shutdown('SIGTERM', false)); + process.on('SIGINT', () => void shutdown('SIGINT', false)); // Handle uncaught errors process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); - void shutdown('UNCAUGHT_EXCEPTION'); + void shutdown('UNCAUGHT_EXCEPTION', true); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); - void shutdown('UNHANDLED_REJECTION'); + void shutdown('UNHANDLED_REJECTION', true); }); } diff --git a/src/ui-server.ts b/src/ui-server.ts index 5750242..698ef8a 100644 --- a/src/ui-server.ts +++ b/src/ui-server.ts @@ -57,15 +57,19 @@ async function startUIServer() { // Start User Interface const ui = new UserInterface(configManager, apiBaseUrl); - + console.log(`Starting UI server on port ${uiPort}...`); await ui.start(uiPort); console.log(`✓ UI server running on http://0.0.0.0:${uiPort}`); console.log(`✓ API Gateway URL: ${apiBaseUrl}`); // Handle graceful shutdown - const shutdown = async (signal: string) => { - console.log(`\n${signal} received. Shutting down gracefully...`); + const shutdown = async (signal: string, isError: boolean = false) => { + if (isError) { + console.error(`\n${signal} received. Shutting down due to error...`); + } else { + console.log(`\n${signal} received. Shutting down gracefully...`); + } try { await ui.stop(); console.log('✓ UI server stopped'); @@ -74,25 +78,25 @@ async function startUIServer() { await pool.end(); console.log('✓ PostgreSQL disconnected'); console.log('Shutdown complete'); - process.exit(0); + process.exit(isError ? 1 : 0); } catch (error) { console.error('Error during shutdown:', error); process.exit(1); } }; - process.on('SIGTERM', () => void shutdown('SIGTERM')); - process.on('SIGINT', () => void shutdown('SIGINT')); + process.on('SIGTERM', () => void shutdown('SIGTERM', false)); + process.on('SIGINT', () => void shutdown('SIGINT', false)); // Handle uncaught errors process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); - void shutdown('UNCAUGHT_EXCEPTION'); + void shutdown('UNCAUGHT_EXCEPTION', true); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); - void shutdown('UNHANDLED_REJECTION'); + void shutdown('UNHANDLED_REJECTION', true); }); }