mirror of
https://github.com/TheFunny/ArisuAutoSweeper
synced 2026-06-10 00:24:51 +00:00
Implement FastAPI backend with REST API and basic frontend
Co-authored-by: TheFunny <26841179+TheFunny@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }}</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom CSS - reuse existing styles -->
|
||||
<link rel="stylesheet" href="/assets/css/alas.css">
|
||||
<link rel="stylesheet" href="/assets/css/alas-pc.css">
|
||||
{% if theme == 'dark' %}
|
||||
<link rel="stylesheet" href="/assets/css/dark-alas.css">
|
||||
{% else %}
|
||||
<link rel="stylesheet" href="/assets/css/light-alas.css">
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"header header header"
|
||||
"aside menu content";
|
||||
grid-template-columns: 4rem 12rem 1fr;
|
||||
grid-template-rows: 3.3125rem 1fr;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
grid-area: header;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
border-bottom: 1px solid var(--border-color, #dee2e6);
|
||||
}
|
||||
|
||||
.aside {
|
||||
grid-area: aside;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid var(--border-color, #dee2e6);
|
||||
}
|
||||
|
||||
.menu {
|
||||
grid-area: menu;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 0.5rem;
|
||||
border-right: 1px solid var(--border-color, #dee2e6);
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-area: content;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.instance-btn {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.status-running { background: #28a745; color: white; }
|
||||
.status-stopped { background: #6c757d; color: white; }
|
||||
|
||||
.card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<h3 style="margin: 0;">{{ title }}</h3>
|
||||
<div class="ms-auto">
|
||||
<span id="status-indicator"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aside Navigation -->
|
||||
<div class="aside">
|
||||
<div class="text-center py-3">
|
||||
<button class="btn btn-sm btn-primary instance-btn" onclick="showHome()">Home</button>
|
||||
</div>
|
||||
<div id="instance-list">
|
||||
{% for instance in instances %}
|
||||
<div class="text-center py-2">
|
||||
<button class="btn btn-sm btn-outline-primary instance-btn"
|
||||
onclick="selectInstance('{{ instance }}')">{{ instance }}</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu">
|
||||
<div id="menu-content">
|
||||
<button class="btn btn-sm btn-menu w-100 mb-2" onclick="showOverview()">Overview</button>
|
||||
<button class="btn btn-sm btn-menu w-100 mb-2" onclick="showConfig()">Config</button>
|
||||
<button class="btn btn-sm btn-menu w-100 mb-2" onclick="showLogs()">Logs</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="content">
|
||||
<div id="main-content">
|
||||
<h2>Welcome to ArisuAutoSweeper</h2>
|
||||
<p>Select an instance from the sidebar to get started.</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">System Information</h5>
|
||||
<div id="system-info">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Language & Theme</h5>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Language:</label>
|
||||
<select class="form-select" id="language-select" onchange="changeLanguage()">
|
||||
<option value="zh-CN" {% if language == 'zh-CN' %}selected{% endif %}>简体中文</option>
|
||||
<option value="en-US" {% if language == 'en-US' %}selected{% endif %}>English</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Theme:</label>
|
||||
<select class="form-select" id="theme-select" onchange="changeTheme()">
|
||||
<option value="default" {% if theme != 'dark' %}selected{% endif %}>Light</option>
|
||||
<option value="dark" {% if theme == 'dark' %}selected{% endif %}>Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Custom JavaScript -->
|
||||
<script>
|
||||
let currentInstance = null;
|
||||
let ws = null;
|
||||
|
||||
// Load system info on startup
|
||||
async function loadSystemInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/system/info');
|
||||
const data = await response.json();
|
||||
document.getElementById('system-info').innerHTML = `
|
||||
<p><strong>Version:</strong> ${data.version}</p>
|
||||
<p><strong>Language:</strong> ${data.language}</p>
|
||||
<p><strong>Theme:</strong> ${data.theme}</p>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('Error loading system info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function showHome() {
|
||||
document.getElementById('main-content').innerHTML = `
|
||||
<h2>Welcome to ArisuAutoSweeper</h2>
|
||||
<p>Select an instance from the sidebar to get started.</p>
|
||||
`;
|
||||
loadSystemInfo();
|
||||
}
|
||||
|
||||
async function selectInstance(instance) {
|
||||
currentInstance = instance;
|
||||
document.getElementById('main-content').innerHTML = `
|
||||
<h2>${instance}</h2>
|
||||
<div id="instance-overview">Loading...</div>
|
||||
`;
|
||||
await loadInstanceOverview(instance);
|
||||
}
|
||||
|
||||
async function loadInstanceOverview(instance) {
|
||||
try {
|
||||
const statusResponse = await fetch(`/api/process/${instance}/status`);
|
||||
const status = await statusResponse.json();
|
||||
|
||||
const alive = status.alive;
|
||||
const statusClass = alive ? 'status-running' : 'status-stopped';
|
||||
const statusText = alive ? 'Running' : 'Stopped';
|
||||
|
||||
document.getElementById('instance-overview').innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Status</h5>
|
||||
<p>Status: <span class="status-badge ${statusClass}">${statusText}</span></p>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-success" onclick="startProcess('${instance}')">Start</button>
|
||||
<button class="btn btn-danger" onclick="stopProcess('${instance}')">Stop</button>
|
||||
<button class="btn btn-warning" onclick="restartProcess('${instance}')">Restart</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('Error loading instance:', error);
|
||||
document.getElementById('instance-overview').innerHTML = `
|
||||
<div class="alert alert-danger">Error loading instance information</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function startProcess(instance) {
|
||||
try {
|
||||
const response = await fetch(`/api/process/${instance}/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
const data = await response.json();
|
||||
alert(data.message);
|
||||
await loadInstanceOverview(instance);
|
||||
} catch (error) {
|
||||
alert('Error starting process: ' + error);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopProcess(instance) {
|
||||
try {
|
||||
const response = await fetch(`/api/process/${instance}/stop`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
alert(data.message);
|
||||
await loadInstanceOverview(instance);
|
||||
} catch (error) {
|
||||
alert('Error stopping process: ' + error);
|
||||
}
|
||||
}
|
||||
|
||||
async function restartProcess(instance) {
|
||||
try {
|
||||
const response = await fetch(`/api/process/${instance}/restart`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
alert(data.message);
|
||||
await loadInstanceOverview(instance);
|
||||
} catch (error) {
|
||||
alert('Error restarting process: ' + error);
|
||||
}
|
||||
}
|
||||
|
||||
function showOverview() {
|
||||
if (currentInstance) {
|
||||
loadInstanceOverview(currentInstance);
|
||||
} else {
|
||||
alert('Please select an instance first');
|
||||
}
|
||||
}
|
||||
|
||||
function showConfig() {
|
||||
document.getElementById('main-content').innerHTML = `
|
||||
<h2>Configuration</h2>
|
||||
<p>Configuration editor is under development.</p>
|
||||
`;
|
||||
}
|
||||
|
||||
function showLogs() {
|
||||
if (!currentInstance) {
|
||||
alert('Please select an instance first');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('main-content').innerHTML = `
|
||||
<h2>Logs - ${currentInstance}</h2>
|
||||
<div class="log-container" id="log-output">
|
||||
<div>Connecting to log stream...</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Connect WebSocket for logs
|
||||
connectLogStream(currentInstance);
|
||||
}
|
||||
|
||||
function connectLogStream(instance) {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/logs/${instance}`;
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
document.getElementById('log-output').innerHTML = '<div>Connected to log stream.</div>';
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const logOutput = document.getElementById('log-output');
|
||||
if (logOutput) {
|
||||
const data = JSON.parse(event.data);
|
||||
logOutput.innerHTML += `<div>[${data.type}] ${JSON.stringify(data)}</div>`;
|
||||
logOutput.scrollTop = logOutput.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket connection closed');
|
||||
};
|
||||
}
|
||||
|
||||
async function changeLanguage() {
|
||||
const language = document.getElementById('language-select').value;
|
||||
try {
|
||||
await fetch('/api/system/language', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ language })
|
||||
});
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
alert('Error changing language: ' + error);
|
||||
}
|
||||
}
|
||||
|
||||
async function changeTheme() {
|
||||
const theme = document.getElementById('theme-select').value;
|
||||
try {
|
||||
await fetch('/api/system/theme', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ theme })
|
||||
});
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
alert('Error changing theme: ' + error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
window.addEventListener('load', () => {
|
||||
loadSystemInfo();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user