diff --git a/docs/consultations/gemini-trinity-console-2-code-package-2026-04-05.md b/docs/consultations/gemini-trinity-console-2-code-package-2026-04-05.md new file mode 100644 index 0000000..dc7ebae --- /dev/null +++ b/docs/consultations/gemini-trinity-console-2-code-package-2026-04-05.md @@ -0,0 +1,848 @@ +# Gemini Response: Trinity Console 2.0 — Complete Code Package + +**Date:** April 5, 2026 +**From:** Gemini (Architectural Partner) +**To:** Michael (The Wizard) + Claude (Chronicler #61) +**Re:** Complete, production-ready code for Tasks #95, #96, #98 + +--- + +This is the master blueprint, Michael and Chronicler #61. Let's lay the foundation for Trinity Console 2.0. + +Here is the complete, production-ready code exactly as requested, formatted for easy copy-pasting into your Arbiter environment. + +--- + +## 1. Database Migrations (Core Foundation) + +### `migrations/001_rbac_tables.sql` + +```sql +-- 1. Roles Table +CREATE TABLE IF NOT EXISTS roles ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) UNIQUE NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 2. Permissions Table (Updated with is_active) +CREATE TABLE IF NOT EXISTS permissions ( + permission_key VARCHAR(100) PRIMARY KEY, + module_id VARCHAR(50) NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 3. Role Permissions Table +CREATE TABLE IF NOT EXISTS role_permissions ( + role_id INT REFERENCES roles(id) ON DELETE CASCADE, + permission_key VARCHAR(100) REFERENCES permissions(permission_key) ON DELETE CASCADE, + PRIMARY KEY (role_id, permission_key) +); + +-- 4. User Roles Table +CREATE TABLE IF NOT EXISTS user_roles ( + user_id VARCHAR(50) PRIMARY KEY, + role_id INT REFERENCES roles(id) ON DELETE CASCADE, + assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + assigned_by VARCHAR(50) +); + +-- 5. User Permission Overrides Table +CREATE TABLE IF NOT EXISTS user_permission_overrides ( + user_id VARCHAR(50) NOT NULL, + permission_key VARCHAR(100) REFERENCES permissions(permission_key) ON DELETE CASCADE, + is_granted BOOLEAN NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50), + PRIMARY KEY (user_id, permission_key) +); + +-- Core System Migrations Table +CREATE TABLE IF NOT EXISTS core_migrations ( + id SERIAL PRIMARY KEY, + module_id VARCHAR(50) NOT NULL, + migration_file VARCHAR(255) NOT NULL, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(module_id, migration_file) +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_user_overrides ON user_permission_overrides(user_id); +CREATE INDEX IF NOT EXISTS idx_role_permissions ON role_permissions(role_id); +CREATE INDEX IF NOT EXISTS idx_active_permissions ON permissions(is_active); + +-- Seed Data +INSERT INTO roles (name, description) VALUES +('Admin', 'Full system access (The Wizard)'), +('Community', 'Player management and support (The Emissary)'), +('Builder', 'Server and infrastructure management (The Catalyst)') +ON CONFLICT (name) DO NOTHING; +``` + +### `migrations/002_feature_flags.sql` + +```sql +CREATE TABLE IF NOT EXISTS feature_flags ( + key VARCHAR(100) PRIMARY KEY, + is_enabled BOOLEAN NOT NULL DEFAULT FALSE, + description TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_by VARCHAR(50) +); +``` + +--- + +## 2. Task #95: The Core Engine + +### `src/core/modules/registry.js` + +```javascript +const modules = new Map(); + +module.exports = { + register: (moduleConfig) => { + modules.set(moduleConfig.id, moduleConfig); + }, + getModule: (id) => modules.get(id), + getAllModules: () => Array.from(modules.values()), + isModuleEnabled: (id) => modules.has(id) // Can be expanded later if dynamic enable/disable is added +}; +``` + +### `src/core/modules/loader.js` + +```javascript +const fs = require('fs'); +const path = require('path'); + +function loadModules(modulesPath) { + if (!fs.existsSync(modulesPath)) return []; + + const folders = fs.readdirSync(modulesPath, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); + + const loadedModules = []; + + for (const folder of folders) { + const configPath = path.join(modulesPath, folder, 'module.json'); + if (fs.existsSync(configPath)) { + try { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + config.dirPath = path.join(modulesPath, folder); + loadedModules.push(config); + } catch (err) { + console.error(`[Core] Failed to parse module.json for ${folder}:`, err.message); + } + } + } + + return topologicalSort(loadedModules); +} + +function topologicalSort(modules) { + const sorted = []; + const visited = new Set(); + const visiting = new Set(); + const moduleMap = new Map(modules.map(m => [m.id, m])); + + function visit(moduleId) { + if (visiting.has(moduleId)) { + throw new Error(`[Core] Fatal: Circular dependency detected involving module '${moduleId}'`); + } + if (visited.has(moduleId)) return; + + visiting.add(moduleId); + const mod = moduleMap.get(moduleId); + + if (mod && mod.dependencies) { + for (const dep of Object.keys(mod.dependencies)) { + if (!moduleMap.has(dep)) { + console.warn(`[Core] Warning: Module '${moduleId}' depends on missing module '${dep}'`); + continue; + } + visit(dep); + } + } + + visiting.delete(moduleId); + visited.add(moduleId); + if (mod) sorted.push(mod); + } + + for (const mod of modules) { + visit(mod.id); + } + + return sorted; +} + +module.exports = { loadModules }; +``` + +### `src/core/database/migrations.js` + +```javascript +const fs = require('fs'); +const path = require('path'); + +async function runModuleMigrations(pool, moduleConfig) { + const migrationsDir = path.join(moduleConfig.dirPath, 'migrations'); + if (!fs.existsSync(migrationsDir)) return; + + const files = fs.readdirSync(migrationsDir) + .filter(f => f.endsWith('.sql')) + .sort(); + + for (const file of files) { + const checkRes = await pool.query( + 'SELECT 1 FROM core_migrations WHERE module_id = $1 AND migration_file = $2', + [moduleConfig.id, file] + ); + + if (checkRes.rowCount === 0) { + console.log(`[Migrations] Applying ${moduleConfig.id}/${file}...`); + const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf8'); + + const client = await pool.connect(); + try { + await client.query('BEGIN'); + await client.query(sql); + await client.query( + 'INSERT INTO core_migrations (module_id, migration_file) VALUES ($1, $2)', + [moduleConfig.id, file] + ); + await client.query('COMMIT'); + } catch (err) { + await client.query('ROLLBACK'); + throw new Error(`Migration failed: ${moduleConfig.id}/${file} - ${err.message}`); + } finally { + client.release(); + } + } + } +} + +module.exports = { runModuleMigrations }; +``` + +### `src/core/modules/routes.js` + +```javascript +const express = require('express'); +const path = require('path'); +const fs = require('fs'); + +function mountModuleRoutes(app, moduleConfig) { + const routesPath = path.join(moduleConfig.dirPath, 'routes.js'); + if (fs.existsSync(routesPath)) { + const router = require(routesPath); + const prefix = moduleConfig.routes || `/${moduleConfig.id}`; + app.use(prefix, router); + console.log(`[Routes] Mounted ${moduleConfig.id} at ${prefix}`); + } +} + +module.exports = { mountModuleRoutes }; +``` + +### `src/core/events/index.js` + +```javascript +const EventEmitter = require('events'); +const fs = require('fs'); +const path = require('path'); + +class CoreEvents extends EventEmitter {} +const coreEmitter = new CoreEvents(); + +function loadModuleEvents(moduleConfig) { + const eventsPath = path.join(moduleConfig.dirPath, 'events.js'); + if (fs.existsSync(eventsPath)) { + const registerEvents = require(eventsPath); + registerEvents(coreEmitter); + console.log(`[Events] Registered events for ${moduleConfig.id}`); + } +} + +module.exports = { + emitter: coreEmitter, + loadModuleEvents +}; +``` + +### `src/core/navigation/index.js` + +```javascript +const registry = require('../modules/registry'); + +function buildNavigation(userPermissions) { + const modules = registry.getAllModules(); + const navStructure = {}; + + modules.forEach(mod => { + if (!mod.nav) return; + + // Basic view permission check based on module ID (assumes {id}.view convention) + const canView = userPermissions.includes(`${mod.id}.view`) || userPermissions.includes(`${mod.id}.*`); + // If they have admin bypass or specific view permission + if (!canView && !userPermissions.includes('admin.*')) return; + + const section = mod.nav.section || 'General'; + if (!navStructure[section]) navStructure[section] = []; + + navStructure[section].push({ + name: mod.name, + icon: mod.icon || 'folder', + path: mod.routes || `/${mod.id}`, + position: mod.nav.position || 99, + badge: mod.nav.badge // Frontend can interpret this string later + }); + }); + + // Sort items within sections + for (const section in navStructure) { + navStructure[section].sort((a, b) => a.position - b.position); + } + + return navStructure; +} + +module.exports = { buildNavigation }; +``` + +### `src/core/boot.js` + +```javascript +const path = require('path'); +const { loadModules } = require('./modules/loader'); +const registry = require('./modules/registry'); +const { runModuleMigrations } = require('./database/migrations'); +const { syncPermissions } = require('./permissions/sync'); +const { mountModuleRoutes } = require('./modules/routes'); +const { loadModuleEvents } = require('./events'); +const { refreshCache } = require('./features'); + +async function bootSystem(app, pool) { + console.log('[Boot] Initializing Trinity Console 2.0...'); + const modulesPath = path.join(__dirname, '../../modules'); + + try { + // 1. Load and Sort Modules + const sortedModules = loadModules(modulesPath); + console.log(`[Boot] Found ${sortedModules.length} valid modules.`); + + for (const mod of sortedModules) { + // 2. Register in Memory + registry.register(mod); + + // 3. Run Migrations + await runModuleMigrations(pool, mod); + + // 4. Register Events + loadModuleEvents(mod); + + // 5. Mount Routes + mountModuleRoutes(app, mod); + } + + // 6. Sync RBAC Permissions + await syncPermissions(pool, sortedModules); + + // 7. Load Feature Flags + await refreshCache(pool); + + console.log('[Boot] System fully initialized and ready.'); + } catch (err) { + console.error('[Boot] FATAL ERROR during boot sequence:', err); + process.exit(1); + } +} + +module.exports = { bootSystem }; +``` + +--- + +## 3. Task #96: The RBAC Engine + +### `src/core/permissions/resolver.js` + +```javascript +async function fetchUserPermissions(pool, userId) { + const query = ` + SELECT p.permission_key + FROM role_permissions p + JOIN user_roles ur ON p.role_id = ur.role_id + WHERE ur.user_id = $1 + + UNION + + SELECT permission_key + FROM user_permission_overrides + WHERE user_id = $1 AND is_granted = TRUE + + EXCEPT + + SELECT permission_key + FROM user_permission_overrides + WHERE user_id = $1 AND is_granted = FALSE; + `; + + const { rows } = await pool.query(query, [userId]); + return rows.map(r => r.permission_key); +} + +function hasPermission(userPermissionsArray, requiredPermission) { + if (!userPermissionsArray || !Array.isArray(userPermissionsArray)) return false; + + // Exact match + if (userPermissionsArray.includes(requiredPermission)) return true; + + // Wildcard match (e.g., required: 'tasks.edit', user has: 'tasks.*') + const parts = requiredPermission.split('.'); + if (parts.length > 1) { + const wildcard = `${parts[0]}.*`; + if (userPermissionsArray.includes(wildcard)) return true; + } + + // Super admin wildcard fallback + if (userPermissionsArray.includes('admin.*') || userPermissionsArray.includes('*.*')) return true; + + return false; +} + +module.exports = { fetchUserPermissions, hasPermission }; +``` + +### `src/core/permissions/middleware.js` + +```javascript +const { hasPermission, fetchUserPermissions } = require('./resolver'); +const db = require('../../db'); // Adjust path to your db pool connection + +function requirePermission(requiredKey) { + return async (req, res, next) => { + if (!req.user) { + return res.status(401).send('Unauthorized'); + } + + // Cache resolution in session + if (!req.session.permissions) { + req.session.permissions = await fetchUserPermissions(db, req.user.id); + } + + if (hasPermission(req.session.permissions, requiredKey)) { + return next(); + } + + // Handle denial gracefully based on request type + const isHtmx = req.headers['hx-request'] === 'true'; + const msg = `Access Denied: Requires '${requiredKey}' permission.`; + + if (isHtmx) { + return res.status(403).send(` +
The Frostwall holds strong.
+ +12 / 12
+