first commit

This commit is contained in:
2025-09-24 18:42:16 +07:00
commit daffbc67dc
72 changed files with 40710 additions and 0 deletions

View File

@@ -0,0 +1,665 @@
<template>
<div class="admin-tab">
<div class="controls">
<div class="input-group">
<label for="adminCommand">Admin Command</label>
<select id="adminCommand" v-model="adminCommand">
<option value="restart_server">Restart Server</option>
<option value="shutdown_server">Shutdown Server</option>
<option value="clear_cache">Clear Cache</option>
<option value="reload_config">Reload Configuration</option>
<option value="backup_database">Backup Database</option>
<option value="restore_database">Restore Database</option>
<option value="export_logs">Export Logs</option>
<option value="clear_logs">Clear Logs</option>
<option value="update_permissions">Update Permissions</option>
<option value="custom">Custom Command</option>
</select>
</div>
<div class="input-group">
<label for="commandParams">Command Parameters (JSON)</label>
<textarea
id="commandParams"
v-model="commandParams"
placeholder='{"target": "all", "force": false}'
rows="3"
></textarea>
</div>
<div class="input-group">
<label for="customCommand">Custom Command</label>
<input
id="customCommand"
v-model="customCommand"
type="text"
placeholder="custom_admin_command"
:disabled="adminCommand !== 'custom'"
/>
</div>
<div class="input-group">
<label for="confirmationCode">Confirmation Code</label>
<input
id="confirmationCode"
v-model="confirmationCode"
type="text"
placeholder="Enter confirmation code"
/>
</div>
</div>
<div class="controls">
<button
class="btn btn-danger"
:disabled="!isConnected || !confirmationCode.trim()"
@click="executeAdminCommand"
>
Execute Command
</button>
<button
class="btn btn-secondary"
:disabled="!isConnected"
@click="getServerInfo"
>
Get Server Info
</button>
<button
class="btn btn-secondary"
:disabled="!isConnected"
@click="getSystemHealth"
>
Get System Health
</button>
<button
class="btn btn-warning"
@click="clearCommandHistory"
>
Clear History
</button>
</div>
<!-- Server Information -->
<div class="server-info-section" v-if="serverInfo">
<h3>Server Information</h3>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Server Version</div>
<div class="info-value">{{ serverInfo.version }}</div>
</div>
<div class="info-item">
<div class="info-label">Environment</div>
<div class="info-value">{{ serverInfo.environment }}</div>
</div>
<div class="info-item">
<div class="info-label">Node Version</div>
<div class="info-value">{{ serverInfo.nodeVersion }}</div>
</div>
<div class="info-item">
<div class="info-label">Platform</div>
<div class="info-value">{{ serverInfo.platform }}</div>
</div>
<div class="info-item">
<div class="info-label">Architecture</div>
<div class="info-value">{{ serverInfo.architecture }}</div>
</div>
<div class="info-item">
<div class="info-label">CPU Cores</div>
<div class="info-value">{{ serverInfo.cpuCores }}</div>
</div>
<div class="info-item">
<div class="info-label">Memory Usage</div>
<div class="info-value">{{ formatBytes(serverInfo.memoryUsage) }}</div>
</div>
<div class="info-item">
<div class="info-label">Uptime</div>
<div class="info-value">{{ formatUptime(serverInfo.uptime) }}</div>
</div>
</div>
</div>
<!-- System Health -->
<div class="system-health-section" v-if="systemHealth">
<h3>System Health</h3>
<div class="health-grid">
<div class="health-item">
<div class="health-label">CPU Usage</div>
<div class="health-value">{{ systemHealth.cpuUsage }}%</div>
<div class="health-bar">
<div
class="health-bar-fill"
:style="{ width: systemHealth.cpuUsage + '%' }"
:class="getCpuUsageClass(systemHealth.cpuUsage)"
></div>
</div>
</div>
<div class="health-item">
<div class="health-label">Memory Usage</div>
<div class="health-value">{{ systemHealth.memoryUsage }}%</div>
<div class="health-bar">
<div
class="health-bar-fill"
:style="{ width: systemHealth.memoryUsage + '%' }"
:class="getMemoryUsageClass(systemHealth.memoryUsage)"
></div>
</div>
</div>
<div class="health-item">
<div class="health-label">Disk Usage</div>
<div class="health-value">{{ systemHealth.diskUsage }}%</div>
<div class="health-bar">
<div
class="health-bar-fill"
:style="{ width: systemHealth.diskUsage + '%' }"
:class="getDiskUsageClass(systemHealth.diskUsage)"
></div>
</div>
</div>
<div class="health-item">
<div class="health-label">Network I/O</div>
<div class="health-value">{{ formatBytes(systemHealth.networkRx) }} / {{ formatBytes(systemHealth.networkTx) }}</div>
</div>
<div class="health-item">
<div class="health-label">Active Connections</div>
<div class="health-value">{{ systemHealth.activeConnections }}</div>
</div>
<div class="health-item">
<div class="health-label">Error Rate</div>
<div class="health-value">{{ systemHealth.errorRate }}%</div>
</div>
</div>
</div>
<!-- Command History -->
<div class="command-history-section">
<h3>Command History</h3>
<div class="command-history">
<div
v-for="(command, index) in commandHistory.slice(0, 10)"
:key="index"
class="command-item"
>
<div class="command-header">
<div class="command-type">{{ command.command }}</div>
<div class="command-time">{{ formatTime(command.timestamp) }}</div>
<div class="command-status" :class="command.success ? 'success' : 'error'">
{{ command.success ? 'Success' : 'Error' }}
</div>
</div>
<div class="command-details">
<div v-if="command.params" class="command-params">
<strong>Parameters:</strong> {{ command.params }}
</div>
<div v-if="command.result" class="command-result">
<strong>Result:</strong>
<pre>{{ JSON.stringify(command.result, null, 2) }}</pre>
</div>
<div v-if="command.error" class="command-error">
<strong>Error:</strong> {{ command.error }}
</div>
</div>
</div>
<div v-if="commandHistory.length === 0" class="no-commands">
No commands executed yet
</div>
</div>
</div>
<!-- Security Notice -->
<div class="security-notice">
<h4> Security Notice</h4>
<p>Admin commands can have serious consequences. Please ensure you have proper authorization and understand the impact of each command before execution.</p>
<ul>
<li>Server restart will disconnect all clients</li>
<li>Database operations are irreversible</li>
<li>Always backup before destructive operations</li>
<li>Some commands require elevated permissions</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useWebSocket } from '../../composables/useWebSocket'
const {
isConnected,
serverInfo,
systemHealth,
executeAdminCommand: executeAdminCommandFn,
getServerInfo,
getSystemHealth
} = useWebSocket()
const adminCommand = ref('restart_server')
const commandParams = ref('')
const customCommand = ref('')
const confirmationCode = ref('')
const commandHistory = ref<Array<{
command: string
params: string
result: any
error: string
success: boolean
timestamp: number
}>>([])
const executeAdminCommand = async () => {
if (!isConnected.value || !confirmationCode.value.trim()) return
let command = adminCommand.value
let params = {}
try {
if (adminCommand.value === 'custom') {
command = customCommand.value.trim()
if (!command) {
alert('Please enter a custom command')
return
}
}
if (commandParams.value.trim()) {
params = JSON.parse(commandParams.value)
}
const result = await executeAdminCommandFn(command, {
...params,
confirmationCode: confirmationCode.value.trim(),
timestamp: Date.now()
})
// Add to history
commandHistory.value.unshift({
command,
params: JSON.stringify(params),
result: result,
error: '',
success: true,
timestamp: Date.now()
})
// Clear form
confirmationCode.value = ''
commandParams.value = ''
} catch (error) {
// Add error to history
commandHistory.value.unshift({
command,
params: commandParams.value,
result: null,
error: error instanceof Error ? error.message : String(error),
success: false,
timestamp: Date.now()
})
}
}
const clearCommandHistory = () => {
commandHistory.value = []
}
const formatTime = (timestamp: number) => {
return new Date(timestamp).toLocaleString()
}
const formatUptime = (uptime: number) => {
const seconds = Math.floor(uptime / 1000)
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const getCpuUsageClass = (usage: number) => {
if (usage < 50) return 'usage-low'
if (usage < 80) return 'usage-medium'
return 'usage-high'
}
const getMemoryUsageClass = (usage: number) => {
if (usage < 60) return 'usage-low'
if (usage < 85) return 'usage-medium'
return 'usage-high'
}
const getDiskUsageClass = (usage: number) => {
if (usage < 70) return 'usage-low'
if (usage < 90) return 'usage-medium'
return 'usage-high'
}
</script>
<style scoped>
.admin-tab {
padding: 20px 0;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 24px;
padding: 24px;
background: #f8f9fa;
border-radius: 8px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-group label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.input-group input,
.input-group select,
.input-group textarea {
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.input-group input:focus,
.input-group select:focus,
.input-group textarea:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
}
.input-group input:disabled {
background: #e9ecef;
cursor: not-allowed;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #c82333;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
}
.btn-warning {
background: #FFC107;
color: #212529;
}
.btn-warning:hover {
background: #e0a800;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.server-info-section,
.system-health-section {
margin: 32px 0;
}
.server-info-section h3,
.system-health-section h3 {
margin-bottom: 16px;
color: #333;
font-size: 18px;
}
.info-grid,
.health-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.info-item,
.health-item {
background: white;
padding: 20px;
border-radius: 8px;
border: 1px solid #e9ecef;
text-align: center;
}
.info-label,
.health-label {
color: #666;
font-size: 14px;
margin-bottom: 8px;
}
.info-value,
.health-value {
color: #333;
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.health-bar {
width: 100%;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.health-bar-fill {
height: 100%;
transition: width 0.3s ease;
}
.usage-low {
background: #28a745;
}
.usage-medium {
background: #ffc107;
}
.usage-high {
background: #dc3545;
}
.command-history-section {
margin-top: 32px;
}
.command-history-section h3 {
margin-bottom: 16px;
color: #333;
font-size: 18px;
}
.command-history {
max-height: 300px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
border: 1px solid #e9ecef;
}
.command-item {
margin: 16px 0;
background: white;
border-radius: 8px;
border: 1px solid #e9ecef;
overflow: hidden;
}
.command-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.command-type {
font-weight: 600;
color: #1976d2;
text-transform: uppercase;
font-size: 12px;
}
.command-time {
color: #666;
font-size: 12px;
}
.command-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.command-status.success {
background: #d4edda;
color: #155724;
}
.command-status.error {
background: #f8d7da;
color: #721c24;
}
.command-details {
padding: 16px;
}
.command-params,
.command-result,
.command-error {
margin: 12px 0;
font-size: 13px;
}
.command-params strong,
.command-result strong,
.command-error strong {
color: #333;
display: block;
margin-bottom: 4px;
}
.command-result pre {
background: #f8f9fa;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
color: #333;
}
.command-error {
color: #dc3545;
background: #f8d7da;
padding: 12px;
border-radius: 4px;
}
.no-commands {
text-align: center;
color: #666;
font-style: italic;
padding: 40px;
}
.security-notice {
margin-top: 32px;
padding: 20px;
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
border-left: 4px solid #ffc107;
}
.security-notice h4 {
margin: 0 0 12px 0;
color: #856404;
font-size: 16px;
}
.security-notice p {
margin: 0 0 12px 0;
color: #856404;
font-size: 14px;
}
.security-notice ul {
margin: 0;
padding-left: 20px;
color: #856404;
}
.security-notice li {
margin: 4px 0;
font-size: 13px;
}
/* Responsive Design */
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
padding: 16px;
}
.info-grid,
.health-grid {
grid-template-columns: 1fr;
}
}
</style>