docs: Complete Trinity Console 2.0 code package from Gemini
Production-ready code for: Task #95 - Core Engine: - modules/registry.js (module storage) - modules/loader.js (fs discovery + topological sort) - database/migrations.js (per-module migration runner) - modules/routes.js (dynamic route mounting) - events/index.js (EventEmitter registry) - navigation/index.js (permission-filtered nav builder) - boot.js (orchestrates full startup sequence) Task #96 - RBAC Engine: - 001_rbac_tables.sql (5 tables + indexes + seed data) - permissions/resolver.js (override → role → deny logic) - permissions/middleware.js (requirePermission) - permissions/sync.js (upsert from module.json) - permissions/routes.js (API endpoints) Task #98 - Feature Flags: - 002_feature_flags.sql - features/index.js (in-memory cache) - features/middleware.js (requireFeature) - features/routes.js (toggle API) Example Dashboard module with all components. This is the foundation of Trinity Console 2.0. Signed-off-by: Claude (Chronicler #61) <claude@firefrostgaming.com>
This commit is contained in:
@@ -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(`
|
||||
<div class="p-4 bg-red-900 border border-red-500 text-white rounded">
|
||||
<strong>Error:</strong> ${msg}
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
return res.status(403).render('error', { message: msg }); // Adjust to your error template
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { requirePermission };
|
||||
```
|
||||
|
||||
### `src/core/permissions/sync.js`
|
||||
|
||||
```javascript
|
||||
async function syncPermissions(pool, modules) {
|
||||
console.log('[RBAC] Syncing permissions from module.json files...');
|
||||
const allDeclaredPermissions = [];
|
||||
|
||||
modules.forEach(mod => {
|
||||
if (mod.permissions && Array.isArray(mod.permissions)) {
|
||||
mod.permissions.forEach(p => {
|
||||
allDeclaredPermissions.push({
|
||||
key: p.key,
|
||||
moduleId: mod.id,
|
||||
name: p.name,
|
||||
description: p.description
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 1. Mark everything as inactive first
|
||||
await client.query('UPDATE permissions SET is_active = FALSE');
|
||||
|
||||
// 2. Upsert declared permissions and mark active
|
||||
for (const p of allDeclaredPermissions) {
|
||||
await client.query(`
|
||||
INSERT INTO permissions (permission_key, module_id, name, description, is_active)
|
||||
VALUES ($1, $2, $3, $4, TRUE)
|
||||
ON CONFLICT (permission_key) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
description = EXCLUDED.description,
|
||||
is_active = TRUE;
|
||||
`, [p.key, p.moduleId, p.name, p.description]);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log(`[RBAC] Synced ${allDeclaredPermissions.length} permissions successfully.`);
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('[RBAC] Failed to sync permissions:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { syncPermissions };
|
||||
```
|
||||
|
||||
### `src/core/permissions/routes.js`
|
||||
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../../db');
|
||||
const { requirePermission } = require('./middleware');
|
||||
|
||||
// Protect all RBAC routes
|
||||
router.use(requirePermission('system.rbac.manage'));
|
||||
|
||||
router.get('/api/permissions', async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM permissions WHERE is_active = TRUE ORDER BY module_id, name');
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
router.get('/api/roles', async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM roles ORDER BY id');
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
router.post('/api/users/:id/role', async (req, res) => {
|
||||
const { role_id } = req.body;
|
||||
await db.query(
|
||||
`INSERT INTO user_roles (user_id, role_id, assigned_by) VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id) DO UPDATE SET role_id = EXCLUDED.role_id, assigned_by = EXCLUDED.assigned_by`,
|
||||
[req.params.id, role_id, req.user.id]
|
||||
);
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
router.post('/api/users/:id/override', async (req, res) => {
|
||||
const { permission_key, is_granted } = req.body;
|
||||
await db.query(
|
||||
`INSERT INTO user_permission_overrides (user_id, permission_key, is_granted, created_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (user_id, permission_key) DO UPDATE
|
||||
SET is_granted = EXCLUDED.is_granted, created_by = EXCLUDED.created_by`,
|
||||
[req.params.id, permission_key, is_granted, req.user.id]
|
||||
);
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
router.delete('/api/users/:id/override/:key', async (req, res) => {
|
||||
await db.query(
|
||||
'DELETE FROM user_permission_overrides WHERE user_id = $1 AND permission_key = $2',
|
||||
[req.params.id, req.params.key]
|
||||
);
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Task #98: Feature Flags
|
||||
|
||||
### `src/core/features/index.js`
|
||||
|
||||
```javascript
|
||||
let featureCache = {};
|
||||
|
||||
async function refreshCache(pool) {
|
||||
try {
|
||||
const { rows } = await pool.query('SELECT key, is_enabled FROM feature_flags');
|
||||
const newCache = {};
|
||||
rows.forEach(r => { newCache[r.key] = r.is_enabled; });
|
||||
featureCache = newCache;
|
||||
console.log('[Features] Feature flag cache refreshed.');
|
||||
} catch (err) {
|
||||
console.error('[Features] Failed to refresh cache:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function isEnabled(key) {
|
||||
return !!featureCache[key]; // Default to false if missing
|
||||
}
|
||||
|
||||
async function setFlag(pool, key, enabled, updatedBy) {
|
||||
await pool.query(
|
||||
`INSERT INTO feature_flags (key, is_enabled, updated_by) VALUES ($1, $2, $3)
|
||||
ON CONFLICT (key) DO UPDATE SET is_enabled = EXCLUDED.is_enabled, updated_by = EXCLUDED.updated_by`,
|
||||
[key, enabled, updatedBy]
|
||||
);
|
||||
await refreshCache(pool);
|
||||
}
|
||||
|
||||
module.exports = { refreshCache, isEnabled, setFlag };
|
||||
```
|
||||
|
||||
### `src/core/features/middleware.js`
|
||||
|
||||
```javascript
|
||||
const { isEnabled } = require('./index');
|
||||
|
||||
function requireFeature(featureKey) {
|
||||
return (req, res, next) => {
|
||||
if (isEnabled(featureKey)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const isHtmx = req.headers['hx-request'] === 'true';
|
||||
const msg = `Feature '${featureKey}' is currently disabled.`;
|
||||
|
||||
if (isHtmx) {
|
||||
return res.status(503).send(`
|
||||
<div class="p-4 bg-yellow-900 border border-yellow-500 text-white rounded">
|
||||
<strong>Service Unavailable:</strong> ${msg}
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
return res.status(503).send(msg); // Adjust to your 503 template
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { requireFeature };
|
||||
```
|
||||
|
||||
### `src/core/features/routes.js`
|
||||
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../../db');
|
||||
const { requirePermission } = require('../permissions/middleware');
|
||||
const { setFlag } = require('./index');
|
||||
|
||||
router.use(requirePermission('system.features.manage'));
|
||||
|
||||
router.get('/api/features', async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM feature_flags ORDER BY key');
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
router.post('/api/features/:key', async (req, res) => {
|
||||
const { is_enabled } = req.body;
|
||||
await setFlag(db, req.params.key, is_enabled, req.user.id);
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Boot Integration (`src/index.js`)
|
||||
|
||||
### `src/index.js` *(Snippet showing integration with existing Express setup)*
|
||||
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const pgSession = require('connect-pg-simple')(session);
|
||||
const db = require('./db'); // Your existing PG pool
|
||||
const { bootSystem } = require('./core/boot');
|
||||
const featureRoutes = require('./core/features/routes');
|
||||
const rbacRoutes = require('./core/permissions/routes');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Existing Middlewares (Body parser, Session, Passport, etc.)
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
// ... session setup ...
|
||||
|
||||
// Initialize Trinity Core Architecture
|
||||
(async () => {
|
||||
try {
|
||||
await bootSystem(app, db);
|
||||
|
||||
// Mount core API routes
|
||||
app.use('/core', featureRoutes);
|
||||
app.use('/core', rbacRoutes);
|
||||
|
||||
// Start listening only after boot is complete
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`[Arbiter] Trinity Console 2.0 running on port ${PORT}`);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Arbiter] FAILED TO START:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Complete Example Module (Dashboard)
|
||||
|
||||
### `src/modules/dashboard/module.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "dashboard",
|
||||
"name": "Dashboard",
|
||||
"description": "System overview and quick stats",
|
||||
"version": "1.0.0",
|
||||
"author": "Trinity",
|
||||
"dependencies": {},
|
||||
"icon": "home",
|
||||
"nav": {
|
||||
"section": "General",
|
||||
"position": 1
|
||||
},
|
||||
"permissions": [
|
||||
{
|
||||
"key": "dashboard.view",
|
||||
"name": "View Dashboard",
|
||||
"description": "Access the main overview screen"
|
||||
}
|
||||
],
|
||||
"routes": "/dashboard"
|
||||
}
|
||||
```
|
||||
|
||||
### `src/modules/dashboard/routes.js`
|
||||
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { requirePermission } = require('../../core/permissions/middleware');
|
||||
|
||||
// Protect entire module
|
||||
router.use(requirePermission('dashboard.view'));
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
// res.render path defaults relative to your app's view engine setup.
|
||||
// You may need to adjust the path based on your exact EJS setup.
|
||||
res.render('../src/modules/dashboard/views/index', { user: req.user });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
```
|
||||
|
||||
### `src/modules/dashboard/views/index.ejs`
|
||||
|
||||
```html
|
||||
<div class="p-6 bg-slate-900 text-white min-h-screen">
|
||||
<h1 class="text-3xl font-bold mb-4 text-blue-400">Welcome back, <%= user.username %></h1>
|
||||
<p class="text-slate-300">The Frostwall holds strong.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
|
||||
<div class="bg-slate-800 p-4 border border-slate-700 rounded-lg">
|
||||
<h2 class="text-xl font-bold">Servers Online</h2>
|
||||
<p class="text-3xl text-emerald-400 mt-2">12 / 12</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### `src/modules/dashboard/events.js`
|
||||
|
||||
```javascript
|
||||
module.exports = function(coreEvents) {
|
||||
coreEvents.on('server.status_change', (data) => {
|
||||
console.log(`[Dashboard Module] Noticed server ${data.serverId} changed to ${data.status}`);
|
||||
// Could update a cache here or emit over WebSockets to UI
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### `src/modules/dashboard/api.js`
|
||||
|
||||
```javascript
|
||||
// Functions exposed for other modules to use via registry.getModule('dashboard').api
|
||||
module.exports = {
|
||||
getSystemHealthScore: async () => {
|
||||
return 100; // Placeholder logic
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### `src/modules/dashboard/migrations/001_dashboard_stats.sql`
|
||||
|
||||
```sql
|
||||
-- Placeholder migration to prove the engine works
|
||||
CREATE TABLE IF NOT EXISTS dashboard_daily_stats (
|
||||
date DATE PRIMARY KEY,
|
||||
total_logins INT DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure Summary
|
||||
|
||||
```
|
||||
/opt/arbiter-3.0/
|
||||
├── migrations/
|
||||
│ ├── 001_rbac_tables.sql
|
||||
│ └── 002_feature_flags.sql
|
||||
├── src/
|
||||
│ ├── core/
|
||||
│ │ ├── boot.js
|
||||
│ │ ├── database/
|
||||
│ │ │ └── migrations.js
|
||||
│ │ ├── events/
|
||||
│ │ │ └── index.js
|
||||
│ │ ├── features/
|
||||
│ │ │ ├── index.js
|
||||
│ │ │ ├── middleware.js
|
||||
│ │ │ └── routes.js
|
||||
│ │ ├── modules/
|
||||
│ │ │ ├── loader.js
|
||||
│ │ │ ├── registry.js
|
||||
│ │ │ └── routes.js
|
||||
│ │ ├── navigation/
|
||||
│ │ │ └── index.js
|
||||
│ │ └── permissions/
|
||||
│ │ ├── middleware.js
|
||||
│ │ ├── resolver.js
|
||||
│ │ ├── routes.js
|
||||
│ │ └── sync.js
|
||||
│ ├── modules/
|
||||
│ │ └── dashboard/
|
||||
│ │ ├── module.json
|
||||
│ │ ├── routes.js
|
||||
│ │ ├── views/
|
||||
│ │ │ └── index.ejs
|
||||
│ │ ├── events.js
|
||||
│ │ ├── migrations/
|
||||
│ │ │ └── 001_dashboard_stats.sql
|
||||
│ │ └── api.js
|
||||
│ ├── db.js (existing)
|
||||
│ └── index.js (updated)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Response received April 5, 2026*
|
||||
Reference in New Issue
Block a user