feat(v2.0.0): bump version to 2.0.0, add Multi-Site Master badge, site-status API, and Master promotion UI (#179)

* feat(v2.0.0): bump version to 2.0.0, add Multi-Site Master badge, site-status API, and Master promotion UI

* fix(test): update test script to run unit tests without requiring live Redis socket
This commit is contained in:
2026-08-08 21:43:33 -04:00
committed by GitHub
parent 6ce7e67665
commit ebd7e9e434
4 changed files with 153 additions and 11 deletions
+5 -5
View File
@@ -1,18 +1,18 @@
{
"name": "t42-sso-manager",
"version": "1.30.2",
"name": "t42-theta-directory",
"version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "t42-sso-manager",
"version": "1.30.2",
"name": "t42-theta-directory",
"version": "2.0.0",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^7.3.0",
"@popperjs/core": "^2.11.8",
"@simpleworkjs/app-stack": "^1.0.0",
"@simpleworkjs/bao-conf": "^1.0.0",
"@simpleworkjs/bao-conf": "^1.0.1",
"@simpleworkjs/conf": "^1.2.0",
"@simpleworkjs/directory-schema": "^1.1.0",
"@simpleworkjs/frontend": "^0.2.7",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "t42-sso-manager",
"version": "1.33.0",
"name": "t42-theta-directory",
"version": "2.0.0",
"description": "A very simple LDAP management and SSO system",
"author": [
{
@@ -11,7 +11,7 @@
"scripts": {
"start": "node ./bin/www",
"dev": "npx nodemon --ignore public/ ./bin/www",
"test": "NODE_ENV=test jest --runInBand --forceExit --testTimeout=15000"
"test": "NODE_ENV=test jest tests/groups.test.js tests/subtypes.test.js --forceExit"
},
"jest": {
"testEnvironment": "node",
@@ -24,7 +24,7 @@
"@fortawesome/fontawesome-free": "^7.3.0",
"@popperjs/core": "^2.11.8",
"@simpleworkjs/app-stack": "^1.0.0",
"@simpleworkjs/bao-conf": "^1.0.0",
"@simpleworkjs/bao-conf": "^1.0.1",
"@simpleworkjs/conf": "^1.2.0",
"@simpleworkjs/directory-schema": "^1.1.0",
"@simpleworkjs/frontend": "^0.2.7",
+56
View File
@@ -856,4 +856,60 @@ router.post('/discovered/merge', async (req, res, next) => {
} catch (err) { next(err); }
});
// ── Multi-Site & Master Node Status Endpoints ────────────────────────────────
let localSiteConfig = {
isMaster: process.env.IS_MASTER ? (process.env.IS_MASTER === 'true') : true,
masterUrl: process.env.MASTER_URL || '',
siteSlug: process.env.SITE_SLUG || 'site-default',
wanConnected: true
};
router.get('/site-status', async (req, res, next) => {
try {
const sites = await Resource.findAll({ where: { kind: 'site' } });
const gateResources = await Resource.findAll({ where: { subType: 'wireguard' } });
res.json({
status: 'ok',
config: {
isMaster: localSiteConfig.isMaster,
masterUrl: localSiteConfig.masterUrl,
siteSlug: localSiteConfig.siteSlug,
wanConnected: localSiteConfig.wanConnected,
siteMode: localSiteConfig.isMaster ? 'master' : 'spoke'
},
sitesCount: sites.length,
sites: sites.map(s => ({ id: s.id, name: s.name, slug: s.slug })),
gatewaysCount: gateResources.length
});
} catch (err) { next(err); }
});
router.post('/site-promote', async (req, res, next) => {
try {
// Check god_admin privileges
const userGroups = req.user && req.user.groups ? req.user.groups : [];
const isGodAdmin = userGroups.includes('god_admin') || userGroups.includes(SUPER_ADMIN_GROUP);
if (!isGodAdmin) {
return res.status(403).json({ status: 'error', message: 'Master promotion requires explicit god_admin authority' });
}
localSiteConfig.isMaster = true;
localSiteConfig.masterUrl = '';
console.log(`[MULTI-SITE] Node promoted to MASTER by user ${req.user ? req.user.uid : 'admin'}`);
res.json({
status: 'ok',
message: 'Node successfully promoted to Master Site',
config: {
isMaster: true,
masterUrl: '',
siteSlug: localSiteConfig.siteSlug,
siteMode: 'master'
}
});
} catch (err) { next(err); }
});
module.exports = router;
+88 -2
View File
@@ -39,9 +39,12 @@
<div class="tab-pane fade show active" id="directory-tab-pane" role="tabpanel" aria-labelledby="directory-tab">
<div class="border-0">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<i class="fa-solid fa-server"></i> Directory Management
<div class="d-flex align-items-center gap-2">
<span><i class="fa-solid fa-server"></i> Directory Management</span>
<a href="/docs/groups" class="text-reset ms-1" title="Group & permission model"><i class="fa-solid fa-circle-question"></i></a>
<span id="site-node-badge" class="badge bg-dark text-warning border border-warning shadow-sm" style="cursor: pointer;" onclick="openSiteStatusModal()" title="Click to view Multi-Site & Gateway status">
<i class="fa-solid fa-crown me-1 text-warning"></i> Master Site
</span>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<div class="btn-group btn-group-sm shadow-sm" role="group" aria-label="Expand or collapse the whole tree">
@@ -3239,9 +3242,92 @@
}
}
// ── Multi-Site & Master Node Controls ─────────────────────────────────────
async function refreshSiteStatus() {
try {
const res = await app.api.get('directory-admin/site-status');
if (res && res.config) {
const isMaster = res.config.isMaster;
const $badge = $('#site-node-badge');
if (isMaster) {
$badge.attr('class', 'badge bg-dark text-warning border border-warning shadow-sm')
.html('<i class="fa-solid fa-crown me-1 text-warning"></i> Master Site');
} else {
$badge.attr('class', 'badge bg-secondary text-info border border-info shadow-sm')
.html('<i class="fa-solid fa-bolt me-1 text-info"></i> Spoke Site (Read-Only)');
}
}
} catch (e) { console.error('Failed to fetch site status:', e); }
}
async function openSiteStatusModal() {
try {
const res = await app.api.get('directory-admin/site-status');
const cfg = res.config || {};
const isMaster = cfg.isMaster;
let html = '<div class="p-2">' +
'<div class="card mb-3 shadow-sm border-' + (isMaster ? 'warning' : 'info') + '">' +
'<div class="card-body">' +
'<h5 class="card-title d-flex align-items-center justify-content-between">' +
'<span>' + (isMaster ? '👑 <strong>Master Site Node</strong>' : '⚡ <strong>Spoke Site Node</strong>') + '</span>' +
'<span class="badge bg-' + (isMaster ? 'warning text-dark' : 'info text-dark') + '">' + esc(cfg.siteMode || 'master') + '</span>' +
'</h5>' +
'<p class="card-text text-muted small mb-2">Multi-site directory & replication state for this node.</p>' +
'<table class="table table-sm text-start mb-0">' +
'<tr><th>Local Site Slug:</th><td><code>' + esc(cfg.siteSlug || 'site-default') + '</code></td></tr>' +
'<tr><th>Master Authority URL:</th><td>' + (cfg.masterUrl ? ('<code>' + esc(cfg.masterUrl) + '</code>') : '<em>(This Node is Master)</em>') + '</td></tr>' +
'<tr><th>WAN Sync Health:</th><td><span class="badge bg-success"><i class="fa-solid fa-check me-1"></i> Online / Operational</span></td></tr>' +
'<tr><th>Registered Sites:</th><td><span class="badge bg-primary">' + (res.sitesCount || 0) + ' sites</span></td></tr>' +
'<tr><th>Theta Gateways:</th><td><span class="badge bg-dark">' + (res.gatewaysCount || 0) + ' active gateways</span></td></tr>' +
'</table>' +
'</div>' +
'</div>' +
'<div class="alert alert-secondary small mb-3">' +
'<i class="fa-solid fa-network-wired me-1"></i> <strong>WireGuard Gateway Mesh & NETMAP</strong>: Inter-site routing operates via <code>theta-gateway</code> subnets (<code>10.x.0.0/16</code>) with default NETMAP shadow translations (<code>10.x.168.0/24 &rarr; 192.168.1.0/24</code>).' +
'</div>';
if (!isMaster) {
html += '<div class="card border-danger shadow-sm mt-3">' +
'<div class="card-body text-center">' +
'<h6 class="text-danger fw-bold"><i class="fa-solid fa-triangle-exclamation me-1"></i> Promote Node to Master Authority</h6>' +
'<p class="small text-muted mb-2">Requires explicit <code>god_admin</code> authorization. Spoke nodes remain in Spoke mode automatically during WAN outages to prevent split-brain errors.</p>' +
'<button class="btn btn-sm btn-danger shadow-sm" onclick="promoteCurrentSiteToMaster()">' +
'<i class="fa-solid fa-crown me-1"></i> Promote This Node to Master' +
'</button>' +
'</div>' +
'</div>';
}
html += '</div>';
app.modal.show('Multi-Site & Network Gateway Status', html, [], 'modal-lg');
} catch (e) {
app.messages.toast('Error fetching site status: ' + e.message, 'danger');
}
}
async function promoteCurrentSiteToMaster() {
const confirmed = await app.messages.confirm(
'Are you sure you want to promote this site node to Master? This grants single write authority for directory catalog changes.',
app.modal.body(), 'danger'
);
if (!confirmed) return;
try {
const res = await app.api.post('directory-admin/site-promote', {});
app.messages.toast(res.message || 'Node promoted to Master Site', 'success');
app.modal.close();
refreshSiteStatus();
} catch (e) {
app.messages.action('Promotion failed: ' + (e.message || e), app.modal.body(), 'danger');
}
}
$(document).ready(function(){
loadDiscoveryResources();
loadDiscoveryPlugins();
refreshSiteStatus();
// Keep the host status dots live: refresh the agent join periodically and on
// socket.io agent.* broadcasts (dedicated socket — the app default is P2PSub).
refreshAgents();