feat: eDEX-UI enhancements - 3D rotating globe, top processes panel - Globe.gl 3D globe replaces UK SVG map, Manchester server location - Animated arcs from world cities, hex polygon land masses - Green atmosphere glow, auto-rotate, pulse rings - Top Processes panel with live polling every 5s - /api/processes endpoint returns top 7 by CPU - Color-coded CPU usage (green/amber/red)

This commit is contained in:
jae 2026-04-01 22:06:57 +00:00
parent ffd1b7e689
commit 4a271496c3
5 changed files with 510 additions and 44 deletions

View file

@ -196,6 +196,32 @@ def server_stats():
'timestamp': time.time() 'timestamp': time.time()
}) })
# ─── Top Processes ───────────────────────────────────
@app.route('/api/processes')
def top_processes():
try:
raw = shell("ps aux --sort=-%cpu | awk 'NR>1 && NR<=8{print $2,$3,$4,$11}' | head -7")
procs = []
for line in raw.strip().split('\n'):
parts = line.split(None, 3)
if len(parts) >= 4:
procs.append({
'pid': parts[0],
'cpu': float(parts[1]),
'mem': float(parts[2]),
'name': parts[3].split('/')[-1][:20]
})
elif len(parts) == 3:
procs.append({
'pid': parts[0],
'cpu': float(parts[1]),
'mem': float(parts[2]),
'name': 'unknown'
})
return jsonify(procs)
except Exception as e:
return jsonify([]), 200
# ─── Services Status ───────────────────────────────── # ─── Services Status ─────────────────────────────────
@app.route('/api/services') @app.route('/api/services')
def services(): def services():

View file

@ -2078,3 +2078,148 @@ a:hover { color: #fff; text-shadow: none; }
font-size: 0.68rem; font-size: 0.68rem;
} }
} }
/* ============================
3D GLOBE PANEL
============================ */
.globe-panel {
min-height: 300px;
}
.globe-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
overflow: hidden;
min-height: 280px;
}
#globeViz {
display: flex;
align-items: center;
justify-content: center;
}
#globeViz canvas {
cursor: grab;
}
#globeViz canvas:active {
cursor: grabbing;
}
.globe-data-overlay {
position: absolute;
bottom: 0.5rem;
left: 0.75rem;
right: 0.75rem;
display: flex;
justify-content: space-around;
gap: 0.5rem;
pointer-events: none;
}
.globe-coord-block {
text-align: center;
}
.globe-coord-block .coord-label {
display: block;
font-family: var(--font-mono);
font-size: 0.45rem;
letter-spacing: 2px;
color: var(--text-secondary);
}
.globe-coord-block .coord-value {
display: block;
font-family: var(--font-mono);
font-size: 0.65rem;
color: var(--status-green);
text-shadow: 0 0 6px var(--status-green-glow);
}
/* ============================
TOP PROCESSES TABLE
============================ */
.proc-display {
padding: 0.5rem !important;
}
.proc-table {
width: 100%;
border-collapse: collapse;
font-family: var(--font-mono);
font-size: 0.6rem;
}
.proc-table th {
text-align: left;
padding: 0.3rem 0.4rem;
font-size: 0.5rem;
letter-spacing: 2px;
color: var(--text-secondary);
border-bottom: 1px solid var(--border);
font-weight: 400;
}
.proc-table td {
padding: 0.25rem 0.4rem;
color: #999;
border-bottom: 1px solid rgba(255, 255, 255, 0.02);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100px;
}
.proc-table tr:hover td {
color: var(--status-green);
background: rgba(0, 204, 51, 0.02);
}
.proc-table .proc-cpu-high {
color: var(--mil-red);
text-shadow: 0 0 4px rgba(139, 0, 0, 0.4);
}
.proc-table .proc-cpu-med {
color: var(--amber);
}
.proc-table .proc-cpu-low {
color: var(--status-green);
}
.proc-loading {
text-align: center;
color: #555;
letter-spacing: 2px;
font-size: 0.5rem;
padding: 1rem !important;
animation: hintPulse 2s ease-in-out infinite;
}
/* Globe responsive */
@media (max-width: 1024px) {
.globe-panel {
min-height: 240px;
}
.globe-container {
min-height: 220px;
}
}
@media (max-width: 768px) {
.globe-panel {
min-height: 200px;
}
.globe-container {
min-height: 180px;
}
.proc-table {
font-size: 0.55rem;
}
}

View file

@ -8,6 +8,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&family=Share+Tech+Mono&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<script src="https://unpkg.com/globe.gl@2.45.1/dist/globe.gl.min.js"></script>
</head> </head>
<body> <body>
<!-- Scanline overlay --> <!-- Scanline overlay -->
@ -74,58 +75,26 @@
<!-- Left Column: Map + Identity --> <!-- Left Column: Map + Identity -->
<div class="hud-col-left"> <div class="hud-col-left">
<div class="panel map-panel"> <div class="panel map-panel globe-panel">
<div class="panel-header"> <div class="panel-header">
<span class="panel-title">OPERATOR LOCATION</span> <span class="panel-title">GLOBAL NETWORK MAP</span>
<span class="panel-icon"></span> <span class="panel-status-dot status-green">● TRACKING</span>
</div> </div>
<div class="map-content"> <div class="globe-container" id="globeContainer">
<svg viewBox="0 0 250 400" class="uk-map-svg"> <div id="globeViz"></div>
<defs> <div class="globe-data-overlay">
<pattern id="mapGrid" width="20" height="20" patternUnits="userSpaceOnUse"> <div class="globe-coord-block">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="rgba(255,255,255,0.02)" stroke-width="0.5"/> <span class="coord-label">NODE</span>
</pattern> <span class="coord-value">MANCHESTER</span>
<filter id="glow"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter> </div>
<radialGradient id="dotGlow" cx="50%" cy="50%" r="50%"> <div class="globe-coord-block">
<stop offset="0%" stop-color="#00cc33" stop-opacity="0.5"/>
<stop offset="100%" stop-color="#00cc33" stop-opacity="0"/>
</radialGradient>
</defs>
<rect width="250" height="400" fill="url(#mapGrid)"/>
<!-- UK mainland outline -->
<polygon points="80,12 95,8 120,5 140,8 155,18 152,35 148,55 142,72 138,88 135,100 142,108 150,118 155,135 158,155 162,175 168,195 172,215 170,235 168,255 172,272 168,280 155,288 142,292 130,296 115,302 100,308 85,315 72,318 65,310 75,295 82,280 88,268 85,255 72,248 62,238 55,222 52,205 55,192 60,182 68,175 78,168 82,155 80,140 75,125 70,112 62,100 55,82 52,65 58,45 68,30" fill="rgba(0,204,51,0.02)" stroke="#00cc33" stroke-width="1.2" stroke-opacity="0.4" stroke-linejoin="round"/>
<!-- Northern Ireland (simplified) -->
<polygon points="42,105 55,98 62,102 58,115 48,118 38,112" fill="rgba(0,204,51,0.015)" stroke="#00cc33" stroke-width="0.8" stroke-opacity="0.3" stroke-linejoin="round"/>
<!-- Crosshair lines at Manchester -->
<line x1="0" y1="172" x2="250" y2="172" stroke="#00cc33" stroke-width="0.3" stroke-opacity="0.25" stroke-dasharray="4,4"/>
<line x1="90" y1="0" x2="90" y2="400" stroke="#00cc33" stroke-width="0.3" stroke-opacity="0.25" stroke-dasharray="4,4"/>
<!-- Manchester glow -->
<circle cx="90" cy="172" r="25" fill="url(#dotGlow)"/>
<!-- Manchester dot -->
<circle cx="90" cy="172" r="3" fill="#00cc33" filter="url(#glow)"/>
<!-- Pulse rings -->
<circle cx="90" cy="172" r="8" fill="none" stroke="#00cc33" stroke-width="0.8" class="pulse-ring pulse-ring-1"/>
<circle cx="90" cy="172" r="16" fill="none" stroke="#00cc33" stroke-width="0.5" class="pulse-ring pulse-ring-2"/>
<circle cx="90" cy="172" r="24" fill="none" stroke="#00cc33" stroke-width="0.3" class="pulse-ring pulse-ring-3"/>
<!-- Manchester label -->
<text x="108" y="168" fill="#00cc33" font-size="8" font-family="'JetBrains Mono',monospace" letter-spacing="1">MANCHESTER</text>
<text x="108" y="180" fill="rgba(0,204,51,0.3)" font-size="6" font-family="'JetBrains Mono',monospace">53.48°N 2.24°W</text>
<!-- Scan sweep -->
<line x1="0" y1="0" x2="250" y2="0" stroke="#00cc33" stroke-width="1" stroke-opacity="0.4" class="map-scan-line"/>
</svg>
<div class="map-data-overlay">
<div class="map-coord-block">
<span class="coord-label">LAT</span> <span class="coord-label">LAT</span>
<span class="coord-value">+53.48°</span> <span class="coord-value">+53.48°</span>
</div> </div>
<div class="map-coord-block"> <div class="globe-coord-block">
<span class="coord-label">LONG</span> <span class="coord-label">LONG</span>
<span class="coord-value">-2.24°</span> <span class="coord-value">-2.24°</span>
</div> </div>
<div class="map-coord-block">
<span class="coord-label">ALT</span>
<span class="coord-value">78m (ASL)</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -148,6 +117,29 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Top Processes -->
<div class="panel hud-data-panel">
<div class="panel-header">
<span class="panel-title">TOP PROCESSES</span>
<span class="panel-status-dot status-green">● LIVE</span>
</div>
<div class="panel-content proc-display">
<table class="proc-table">
<thead>
<tr>
<th>PID</th>
<th>NAME</th>
<th>CPU</th>
<th>MEM</th>
</tr>
</thead>
<tbody id="procTableBody">
<tr><td colspan="4" class="proc-loading">SCANNING...</td></tr>
</tbody>
</table>
</div>
</div>
</div> </div>
<!-- Middle Column: AI Chat Terminal --> <!-- Middle Column: AI Chat Terminal -->
@ -574,5 +566,7 @@
<script src="/js/main.js"></script> <script src="/js/main.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/chat.js"></script> <script src="/js/chat.js"></script>
<script src="/js/globe.js"></script>
<script src="/js/processes.js"></script>
</body> </body>
</html> </html>

253
js/globe.js Normal file
View file

@ -0,0 +1,253 @@
/* ===================================================
JAESWIFT.XYZ 3D Globe (Globe.gl)
eDEX-UI inspired rotating globe with server location
=================================================== */
(function () {
'use strict';
const container = document.getElementById('globeViz');
if (!container || typeof Globe === 'undefined') return;
// Manchester coordinates
const SERVER_LAT = 53.48;
const SERVER_LNG = -2.24;
// Wait for container to be visible and sized
function initGlobe() {
const rect = container.parentElement.getBoundingClientRect();
const size = Math.min(rect.width, 340);
if (size < 50) {
setTimeout(initGlobe, 200);
return;
}
const globe = Globe()
.globeImageUrl('')
.backgroundColor('rgba(0,0,0,0)')
.width(size)
.height(size)
.showAtmosphere(true)
.atmosphereColor('#00cc33')
.atmosphereAltitude(0.15)
// Hex polygons for land masses
.hexPolygonsData([])
// Points - server location
.pointsData([{
lat: SERVER_LAT,
lng: SERVER_LNG,
size: 0.6,
color: '#00cc33',
label: 'MANCHESTER'
}])
.pointAltitude('size')
.pointColor('color')
.pointRadius(0.4)
.pointsMerge(false)
// Rings - pulse effect
.ringsData([{
lat: SERVER_LAT,
lng: SERVER_LNG,
maxR: 3,
propagationSpeed: 1.5,
repeatPeriod: 1200
}])
.ringColor(function () { return '#00cc3380'; })
.ringMaxRadius('maxR')
.ringPropagationSpeed('propagationSpeed')
.ringRepeatPeriod('repeatPeriod')
// Labels
.labelsData([{
lat: SERVER_LAT,
lng: SERVER_LNG,
text: 'MCR // 53.48°N',
color: '#00cc33',
size: 0.7
}])
.labelColor('color')
.labelSize('size')
.labelDotRadius(0.3)
.labelAltitude(0.01)
.labelText('text')
.labelResolution(2)
// Custom globe material
.onGlobeReady(function () {
// Style the globe mesh
var globeMesh = globe.scene().children.find(function (c) {
return c.type === 'Mesh' && c.geometry && c.geometry.type === 'SphereGeometry';
});
if (globeMesh && globeMesh.material) {
globeMesh.material.color.setHex(0x050505);
globeMesh.material.emissive.setHex(0x001a00);
globeMesh.material.emissiveIntensity = 0.15;
}
})
(container);
// Auto rotate
globe.controls().autoRotate = true;
globe.controls().autoRotateSpeed = 0.4;
globe.controls().enableZoom = false;
globe.controls().enablePan = false;
globe.controls().minPolarAngle = Math.PI * 0.35;
globe.controls().maxPolarAngle = Math.PI * 0.65;
// Point camera at Manchester
globe.pointOfView({ lat: SERVER_LAT, lng: SERVER_LNG, altitude: 2.2 }, 1500);
// Fetch country polygons for land outlines
fetch('https://unpkg.com/world-atlas@2/countries-110m.json')
.then(function (r) { return r.json(); })
.then(function (worldData) {
var countries = topojsonFeature(worldData, worldData.objects.countries).features;
globe
.hexPolygonsData(countries)
.hexPolygonResolution(3)
.hexPolygonMargin(0.4)
.hexPolygonColor(function () {
return 'rgba(0, 204, 51, 0.12)';
})
.hexPolygonAltitude(0.005);
})
.catch(function () {
// Fallback: no country data, globe still works
});
// Simulated visitor arcs (random locations connecting to Manchester)
function generateRandomArcs() {
var cities = [
{ lat: 40.71, lng: -74.01 }, // New York
{ lat: 35.68, lng: 139.69 }, // Tokyo
{ lat: 48.86, lng: 2.35 }, // Paris
{ lat: -33.87, lng: 151.21 }, // Sydney
{ lat: 55.76, lng: 37.62 }, // Moscow
{ lat: 1.35, lng: 103.82 }, // Singapore
{ lat: 37.57, lng: 126.98 }, // Seoul
{ lat: 19.43, lng: -99.13 }, // Mexico City
{ lat: 52.52, lng: 13.41 }, // Berlin
{ lat: -23.55, lng: -46.63 }, // São Paulo
{ lat: 28.61, lng: 77.21 }, // Delhi
{ lat: 34.05, lng: -118.24 }, // LA
];
var arcs = [];
var count = 2 + Math.floor(Math.random() * 3);
for (var i = 0; i < count; i++) {
var city = cities[Math.floor(Math.random() * cities.length)];
arcs.push({
startLat: city.lat,
startLng: city.lng,
endLat: SERVER_LAT,
endLng: SERVER_LNG,
color: ['rgba(0, 204, 51, 0.6)', 'rgba(0, 204, 51, 0.05)']
});
}
return arcs;
}
globe
.arcsData(generateRandomArcs())
.arcColor('color')
.arcDashLength(0.5)
.arcDashGap(0.2)
.arcDashAnimateTime(2000)
.arcStroke(0.4)
.arcAltitudeAutoScale(0.3);
// Refresh arcs periodically
setInterval(function () {
globe.arcsData(generateRandomArcs());
}, 5000);
// Handle resize
var resizeTimer;
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
var newRect = container.parentElement.getBoundingClientRect();
var newSize = Math.min(newRect.width, 340);
globe.width(newSize).height(newSize);
}, 250);
});
}
// Topojson helper
function topojsonFeature(topology, obj) {
if (!obj || !obj.geometries) return { type: 'FeatureCollection', features: [] };
return {
type: 'FeatureCollection',
features: obj.geometries.map(function (geom) {
return {
type: 'Feature',
geometry: topojsonGeometry(topology, geom),
properties: geom.properties || {},
id: geom.id
};
})
};
}
function topojsonGeometry(topology, obj) {
var arcs = topology.arcs;
var transform = topology.transform;
function decodeArc(arcIdx) {
var arc = arcs[arcIdx < 0 ? ~arcIdx : arcIdx];
var coords = [];
var x = 0, y = 0;
for (var i = 0; i < arc.length; i++) {
x += arc[i][0];
y += arc[i][1];
var lon = x, lat = y;
if (transform) {
lon = lon * transform.scale[0] + transform.translate[0];
lat = lat * transform.scale[1] + transform.translate[1];
}
coords.push([lon, lat]);
}
if (arcIdx < 0) coords.reverse();
return coords;
}
function decodeRing(arcIndices) {
var coords = [];
for (var i = 0; i < arcIndices.length; i++) {
var arcCoords = decodeArc(arcIndices[i]);
if (i > 0) arcCoords.shift();
coords = coords.concat(arcCoords);
}
return coords;
}
var type = obj.type;
if (type === 'Polygon') {
return { type: 'Polygon', coordinates: obj.arcs.map(decodeRing) };
} else if (type === 'MultiPolygon') {
return {
type: 'MultiPolygon',
coordinates: obj.arcs.map(function (polygon) {
return polygon.map(decodeRing);
})
};
} else if (type === 'GeometryCollection') {
return {
type: 'GeometryCollection',
geometries: (obj.geometries || []).map(function (g) {
return topojsonGeometry(topology, g);
})
};
}
return { type: type, coordinates: [] };
}
// Init when DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
setTimeout(initGlobe, 300);
});
} else {
setTimeout(initGlobe, 300);
}
})();

48
js/processes.js Normal file
View file

@ -0,0 +1,48 @@
/* ===================================================
JAESWIFT.XYZ Top Processes Panel
Live-updating process table from /api/processes
=================================================== */
(function () {
'use strict';
var tbody = document.getElementById('procTableBody');
if (!tbody) return;
function cpuClass(val) {
if (val >= 50) return 'proc-cpu-high';
if (val >= 15) return 'proc-cpu-med';
return 'proc-cpu-low';
}
function fetchProcesses() {
fetch('/api/processes')
.then(function (r) { return r.json(); })
.then(function (procs) {
if (!procs || !procs.length) return;
var html = '';
for (var i = 0; i < procs.length; i++) {
var p = procs[i];
var cls = cpuClass(p.cpu);
html += '<tr>'
+ '<td>' + p.pid + '</td>'
+ '<td>' + p.name + '</td>'
+ '<td class="' + cls + '">' + p.cpu.toFixed(1) + '%</td>'
+ '<td>' + p.mem.toFixed(1) + '%</td>'
+ '</tr>';
}
tbody.innerHTML = html;
})
.catch(function () {
// Silently fail, keep old data
});
}
// Initial fetch
setTimeout(fetchProcesses, 1000);
// Poll every 5 seconds
setInterval(fetchProcesses, 5000);
})();