This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
const express = require("express");
|
||||
const cors = require('cors');
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
@@ -27,6 +28,33 @@ app.get("/api/test", (req, res) => {
|
||||
res.json({"message": "Hello from backend!"});
|
||||
});
|
||||
|
||||
app.get("/api/status", (req, res) => {
|
||||
const mem = process.memoryUsage();
|
||||
const uptime = Math.floor(process.uptime());
|
||||
const hours = Math.floor(uptime / 3600);
|
||||
const minutes = Math.floor((uptime % 3600) / 60);
|
||||
|
||||
mongoose.connection.readyState === 1
|
||||
? res.json({
|
||||
status: "online",
|
||||
uptime: `${hours}h ${minutes}m`,
|
||||
memory: {
|
||||
rss: `${(mem.rss / 1024 / 1024).toFixed(1)} MB`,
|
||||
heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(1)} MB`,
|
||||
},
|
||||
mongo: "connected",
|
||||
})
|
||||
: res.json({
|
||||
status: "online",
|
||||
uptime: `${hours}h ${minutes}m`,
|
||||
memory: {
|
||||
rss: `${(mem.rss / 1024 / 1024).toFixed(1)} MB`,
|
||||
heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(1)} MB`,
|
||||
},
|
||||
mongo: "disconnected",
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(5000, () => {
|
||||
console.log("Server running on port 5000");
|
||||
});
|
||||
@@ -22,6 +22,12 @@
|
||||
position: relative;
|
||||
margin: 15px;
|
||||
background: var(--color-background-fore);
|
||||
box-shadow: 4px -4px 0px 0px var(--color-container-shadow);
|
||||
}
|
||||
|
||||
.tui-frame:first-child {
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tui-inner {
|
||||
|
||||
157
frontend/app/components/parts/ServerStatus.vue
Normal file
157
frontend/app/components/parts/ServerStatus.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
const { get } = api();
|
||||
const { t } = useI18n();
|
||||
|
||||
const status = ref<{
|
||||
status: string;
|
||||
uptime: string;
|
||||
memory: { rss: string; heapUsed: string };
|
||||
mongo: string;
|
||||
} | null>(null);
|
||||
const loading = ref(true);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
status.value = await get('/status');
|
||||
} catch (e) {
|
||||
console.error('Status fetch error:', e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const online = computed(() => status.value?.mongo === 'connected');
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="server-status-card">
|
||||
<div class="monitor-bar">
|
||||
<span class="monitor-label">SERVER STATUS</span>
|
||||
<span v-if="loading" class="monitor-blink">▚</span>
|
||||
<span v-else class="monitor-dot" :class="online ? 'green' : 'red'"></span>
|
||||
</div>
|
||||
|
||||
<div class="monitor-screen">
|
||||
<div class="screen-grid">
|
||||
<div class="screen-line">
|
||||
<span class="screen-key">UPTIME</span>
|
||||
<span class="screen-val">{{ loading ? '.......' : status?.uptime || '--' }}</span>
|
||||
</div>
|
||||
<div class="screen-line">
|
||||
<span class="screen-key">MEM_RSS</span>
|
||||
<span class="screen-val">{{ loading ? '......' : status?.memory.rss || '--' }}</span>
|
||||
</div>
|
||||
<div class="screen-line">
|
||||
<span class="screen-key">HEAP</span>
|
||||
<span class="screen-val">{{ loading ? '......' : status?.memory.heapUsed || '--' }}</span>
|
||||
</div>
|
||||
<div class="screen-line">
|
||||
<span class="screen-key">MONGO</span>
|
||||
<span class="screen-val" :class="status?.mongo === 'connected' ? 'ok' : status?.mongo ? 'err' : ''">
|
||||
[{{ loading ? '.+.' : status?.mongo || '--' }}]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.server-status-card {
|
||||
position: relative;
|
||||
background: var(--color-background-fore);
|
||||
box-shadow: 4px -4px 0px 0px var(--color-container-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
}
|
||||
|
||||
.monitor-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
background: #0e0e0e;
|
||||
color: #d4d4d4;
|
||||
font-family: 'Hurmit', monospace;
|
||||
font-size: 0.5rem;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 2px solid #3a3a3a;
|
||||
|
||||
.monitor-blink {
|
||||
animation: blink-cursor 0.8s steps(1) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
display: inline-block;
|
||||
shape-rendering: pixelated;
|
||||
|
||||
&.green {
|
||||
background-color: #4ade80;
|
||||
box-shadow: 0 0 6px #4ade80, 0 0 12px rgba(74, 222, 128, 0.4);
|
||||
}
|
||||
|
||||
&.red {
|
||||
background-color: #ef4444;
|
||||
box-shadow: 0 0 6px #ef4444, 0 0 12px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-screen {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.screen-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.screen-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.screen-key {
|
||||
font-family: 'Hurmit', monospace;
|
||||
font-size: 0.55rem;
|
||||
color: var(--color-text);
|
||||
opacity: 0.4;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.screen-val {
|
||||
font-family: 'Hurmit', monospace;
|
||||
font-size: 0.65rem;
|
||||
color: var(--color-link);
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&.ok {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
&.err {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink-cursor {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
||||
import TableHeader from '~/components/parts/TableHeader.vue';
|
||||
import ServerStatus from '~/components/parts/ServerStatus.vue';
|
||||
import api from '~/composables/api'
|
||||
import { useSeo } from '~/composables/seo'
|
||||
|
||||
@@ -167,9 +168,16 @@ const sectionTargets = {
|
||||
|
||||
<FixedLayout>
|
||||
<section class="intro-section">
|
||||
<div class="intro-layout">
|
||||
<div class="intro-content">
|
||||
<Container>
|
||||
<ContentRenderer v-if="markdown" :value="markdown"></ContentRenderer>
|
||||
</Container>
|
||||
</div>
|
||||
<div class="intro-sidebar">
|
||||
<ServerStatus />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="projects-section" id="scroll-projects" v-if="projects && projects.length > 0">
|
||||
@@ -356,6 +364,31 @@ const sectionTargets = {
|
||||
<style lang="scss" scoped>
|
||||
.intro-section {
|
||||
|
||||
.intro-layout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.intro-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.intro-sidebar {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -364,6 +397,10 @@ const sectionTargets = {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tui-frame:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
margin-top: 290px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user