mirror of
https://github.com/TheFunny/ArisuAutoSweeper
synced 2025-12-16 22:05:12 +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:
parent
df4f3ef359
commit
4efae500d6
92
gui_fastapi.py
Normal file
92
gui_fastapi.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import threading
|
||||||
|
from multiprocessing import Event, Process
|
||||||
|
|
||||||
|
from module.logger import logger
|
||||||
|
from module.webui.setting import State
|
||||||
|
|
||||||
|
|
||||||
|
def func(ev: threading.Event):
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
if sys.platform.startswith("win"):
|
||||||
|
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
||||||
|
|
||||||
|
State.restart_event = ev
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Alas FastAPI web service")
|
||||||
|
parser.add_argument(
|
||||||
|
"--host",
|
||||||
|
type=str,
|
||||||
|
help="Host to listen. Default to WebuiHost in deploy setting",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--port",
|
||||||
|
type=int,
|
||||||
|
help="Port to listen. Default to WebuiPort in deploy setting",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-k", "--key", type=str, help="Password of alas. No password by default"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--electron", action="store_true", help="Runs by electron client."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--run",
|
||||||
|
nargs="+",
|
||||||
|
type=str,
|
||||||
|
help="Run alas by config names on startup",
|
||||||
|
)
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
|
host = args.host or State.deploy_config.WebuiHost or "0.0.0.0"
|
||||||
|
port = args.port or int(State.deploy_config.WebuiPort) or 23467
|
||||||
|
State.electron = args.electron
|
||||||
|
|
||||||
|
logger.hr("Launcher config")
|
||||||
|
logger.attr("Host", host)
|
||||||
|
logger.attr("Port", port)
|
||||||
|
logger.attr("Backend", "FastAPI")
|
||||||
|
logger.attr("Electron", args.electron)
|
||||||
|
logger.attr("Reload", ev is not None)
|
||||||
|
|
||||||
|
if State.electron:
|
||||||
|
logger.info("Electron detected, remove log output to stdout")
|
||||||
|
from module.logger.logger import console_hdlr
|
||||||
|
logger.removeHandler(console_hdlr)
|
||||||
|
|
||||||
|
# Use the new FastAPI backend
|
||||||
|
uvicorn.run(
|
||||||
|
"module.webui.fastapi_backend.main:app",
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
reload=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if State.deploy_config.EnableReload:
|
||||||
|
should_exit = False
|
||||||
|
while not should_exit:
|
||||||
|
event = Event()
|
||||||
|
process = Process(target=func, args=(event,))
|
||||||
|
process.start()
|
||||||
|
while not should_exit:
|
||||||
|
try:
|
||||||
|
b = event.wait(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
should_exit = True
|
||||||
|
break
|
||||||
|
if b:
|
||||||
|
process.kill()
|
||||||
|
break
|
||||||
|
elif process.is_alive():
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
should_exit = True
|
||||||
|
else:
|
||||||
|
func(None)
|
||||||
130
module/webui/fastapi_backend/README.md
Normal file
130
module/webui/fastapi_backend/README.md
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# FastAPI Backend for ArisuAutoSweeper
|
||||||
|
|
||||||
|
This is the new FastAPI-based backend for the ArisuAutoSweeper WebUI, providing a modern REST API architecture while maintaining the same visual style as the original PyWebIO interface.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Backend (FastAPI)
|
||||||
|
- **main.py**: Main FastAPI application with route registration and lifecycle management
|
||||||
|
- **routes/**: API endpoint modules
|
||||||
|
- `config.py`: Configuration management endpoints
|
||||||
|
- `process.py`: Process control endpoints (start/stop/restart)
|
||||||
|
- `system.py`: System settings and update management
|
||||||
|
- **websocket_handler.py**: WebSocket endpoints for real-time log streaming
|
||||||
|
- **templates/**: Jinja2 HTML templates
|
||||||
|
- **static/**: Static assets (CSS, JS)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- Simple HTML/CSS/JS frontend that reuses existing CSS from `assets/gui/css/`
|
||||||
|
- Bootstrap 5 for base styling
|
||||||
|
- Native JavaScript for API interactions
|
||||||
|
- WebSocket for real-time updates
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Starting the FastAPI Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use the new FastAPI backend
|
||||||
|
python gui_fastapi.py
|
||||||
|
|
||||||
|
# Or with custom host/port
|
||||||
|
python gui_fastapi.py --host 0.0.0.0 --port 23467
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
#### Configuration Management
|
||||||
|
- `GET /api/config/instances` - Get list of all instances
|
||||||
|
- `GET /api/config/{instance_name}` - Get configuration for an instance
|
||||||
|
- `POST /api/config/{instance_name}` - Update configuration
|
||||||
|
- `POST /api/config/create` - Create new instance
|
||||||
|
- `DELETE /api/config/{instance_name}` - Delete instance
|
||||||
|
|
||||||
|
#### Process Management
|
||||||
|
- `GET /api/process/` - Get all processes status
|
||||||
|
- `GET /api/process/{instance_name}/status` - Get process status
|
||||||
|
- `POST /api/process/{instance_name}/start` - Start process
|
||||||
|
- `POST /api/process/{instance_name}/stop` - Stop process
|
||||||
|
- `POST /api/process/{instance_name}/restart` - Restart process
|
||||||
|
|
||||||
|
#### System Management
|
||||||
|
- `GET /api/system/info` - Get system information
|
||||||
|
- `POST /api/system/language` - Set language
|
||||||
|
- `POST /api/system/theme` - Set theme
|
||||||
|
- `GET /api/system/update/status` - Get update status
|
||||||
|
- `POST /api/system/update/check` - Check for updates
|
||||||
|
- `POST /api/system/update/run` - Run update
|
||||||
|
- `POST /api/system/restart` - Restart system
|
||||||
|
|
||||||
|
#### WebSocket
|
||||||
|
- `WS /ws/logs/{instance_name}` - Real-time log streaming for an instance
|
||||||
|
- `WS /ws/system` - System-wide real-time updates
|
||||||
|
|
||||||
|
## Comparison with PyWebIO Backend
|
||||||
|
|
||||||
|
### PyWebIO Backend (Original)
|
||||||
|
- **Location**: `module/webui/app.py`
|
||||||
|
- **Entry Point**: `gui.py`
|
||||||
|
- **Architecture**: Monolithic, UI generated from Python code
|
||||||
|
- **Advantages**: Simpler development, no frontend/backend separation
|
||||||
|
- **Disadvantages**: Tightly coupled, harder to extend, limited API access
|
||||||
|
|
||||||
|
### FastAPI Backend (New)
|
||||||
|
- **Location**: `module/webui/fastapi_backend/`
|
||||||
|
- **Entry Point**: `gui_fastapi.py`
|
||||||
|
- **Architecture**: Separated backend (REST API) and frontend
|
||||||
|
- **Advantages**:
|
||||||
|
- Modern REST API
|
||||||
|
- Can be used by multiple clients (web, mobile, CLI)
|
||||||
|
- Better separation of concerns
|
||||||
|
- Easier to test and extend
|
||||||
|
- Real-time updates via WebSocket
|
||||||
|
- **Disadvantages**: More code to maintain, requires frontend development
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
|
||||||
|
Both backends can coexist:
|
||||||
|
- Use `python gui.py` for the original PyWebIO interface
|
||||||
|
- Use `python gui_fastapi.py` for the new FastAPI interface
|
||||||
|
|
||||||
|
Users can gradually migrate from PyWebIO to FastAPI as features are completed.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Adding New Endpoints
|
||||||
|
|
||||||
|
1. Create or modify files in `routes/`
|
||||||
|
2. Add Pydantic models for request/response validation
|
||||||
|
3. Register the router in `main.py`
|
||||||
|
4. Update the frontend to use the new endpoints
|
||||||
|
|
||||||
|
### Reusing Existing CSS
|
||||||
|
|
||||||
|
The frontend reuses CSS from `assets/gui/css/`:
|
||||||
|
- `alas.css` - Base styles
|
||||||
|
- `alas-pc.css` - Desktop styles
|
||||||
|
- `light-alas.css` / `dark-alas.css` - Theme styles
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test that the app loads
|
||||||
|
python -c "from module.webui.fastapi_backend.main import app; print('OK')"
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
python gui_fastapi.py
|
||||||
|
|
||||||
|
# Access the interface
|
||||||
|
# Open browser to http://localhost:23467
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- [ ] Complete configuration editor UI
|
||||||
|
- [ ] Enhanced log viewer with filtering
|
||||||
|
- [ ] Scheduler visualization
|
||||||
|
- [ ] Task queue management
|
||||||
|
- [ ] Mobile-responsive design improvements
|
||||||
|
- [ ] Authentication/authorization
|
||||||
|
- [ ] API documentation (Swagger UI at /docs)
|
||||||
3
module/webui/fastapi_backend/__init__.py
Normal file
3
module/webui/fastapi_backend/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
FastAPI backend for ArisuAutoSweeper WebUI
|
||||||
|
"""
|
||||||
103
module/webui/fastapi_backend/main.py
Normal file
103
module/webui/fastapi_backend/main.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
"""
|
||||||
|
FastAPI main application
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from module.webui.fastapi_backend.routes import config, process, system
|
||||||
|
from module.webui.fastapi_backend.websocket_handler import router as ws_router
|
||||||
|
from module.webui.setting import State
|
||||||
|
from module.webui import lang
|
||||||
|
from module.webui.updater import updater
|
||||||
|
from module.webui.process_manager import ProcessManager
|
||||||
|
from module.logger import logger
|
||||||
|
|
||||||
|
# Get base directory
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
# Create FastAPI app
|
||||||
|
app = FastAPI(
|
||||||
|
title="ArisuAutoSweeper",
|
||||||
|
description="FastAPI backend for ArisuAutoSweeper WebUI",
|
||||||
|
version="1.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add CORS middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Setup templates
|
||||||
|
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
||||||
|
|
||||||
|
# Mount static files
|
||||||
|
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
|
||||||
|
|
||||||
|
# Mount CSS assets from the main assets folder
|
||||||
|
assets_path = Path(__file__).resolve().parent.parent.parent.parent / "assets" / "gui"
|
||||||
|
if assets_path.exists():
|
||||||
|
app.mount("/assets", StaticFiles(directory=str(assets_path)), name="assets")
|
||||||
|
|
||||||
|
# Include API routers
|
||||||
|
app.include_router(config.router, prefix="/api/config", tags=["config"])
|
||||||
|
app.include_router(process.router, prefix="/api/process", tags=["process"])
|
||||||
|
app.include_router(system.router, prefix="/api/system", tags=["system"])
|
||||||
|
app.include_router(ws_router, prefix="/ws", tags=["websocket"])
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
"""Initialize application on startup"""
|
||||||
|
logger.info("FastAPI WebUI starting up")
|
||||||
|
State.init()
|
||||||
|
lang.reload()
|
||||||
|
updater.event = State.manager.Event()
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
"""Cleanup on shutdown"""
|
||||||
|
logger.info("FastAPI WebUI shutting down")
|
||||||
|
for alas in ProcessManager._processes.values():
|
||||||
|
alas.stop()
|
||||||
|
State.clearup()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
async def root(request: Request):
|
||||||
|
"""Serve the main page"""
|
||||||
|
from module.config.utils import alas_instance
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"request": request,
|
||||||
|
"title": "ArisuAutoSweeper",
|
||||||
|
"instances": alas_instance(),
|
||||||
|
"theme": State.deploy_config.Theme,
|
||||||
|
"language": lang.LANG
|
||||||
|
}
|
||||||
|
return templates.TemplateResponse("index.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
"""Factory function to create the FastAPI app"""
|
||||||
|
return app
|
||||||
3
module/webui/fastapi_backend/routes/__init__.py
Normal file
3
module/webui/fastapi_backend/routes/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
API routes for FastAPI backend
|
||||||
|
"""
|
||||||
119
module/webui/fastapi_backend/routes/config.py
Normal file
119
module/webui/fastapi_backend/routes/config.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
"""
|
||||||
|
Configuration management API endpoints
|
||||||
|
"""
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from module.config.utils import alas_instance, alas_template, filepath_args, read_file
|
||||||
|
from module.webui.fake import load_config, get_config_mod
|
||||||
|
from module.webui.setting import State
|
||||||
|
from module.logger import logger
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigValue(BaseModel):
|
||||||
|
"""Config value update model"""
|
||||||
|
path: str
|
||||||
|
value: Any
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/instances")
|
||||||
|
async def get_instances():
|
||||||
|
"""Get list of all alas instances"""
|
||||||
|
return {
|
||||||
|
"instances": alas_instance(),
|
||||||
|
"templates": alas_template()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{instance_name}")
|
||||||
|
async def get_config(instance_name: str):
|
||||||
|
"""Get configuration for a specific instance"""
|
||||||
|
try:
|
||||||
|
config_obj = load_config(instance_name)
|
||||||
|
config_data = config_obj.read_file(instance_name)
|
||||||
|
mod = get_config_mod(instance_name)
|
||||||
|
|
||||||
|
# Get menu and args for this instance
|
||||||
|
menu = read_file(filepath_args("menu", mod))
|
||||||
|
args = read_file(filepath_args("args", mod))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": instance_name,
|
||||||
|
"mod": mod,
|
||||||
|
"config": config_data,
|
||||||
|
"menu": menu,
|
||||||
|
"args": args
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=404, detail=f"Config not found: {instance_name}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{instance_name}")
|
||||||
|
async def update_config(instance_name: str, updates: List[ConfigValue]):
|
||||||
|
"""Update configuration values"""
|
||||||
|
try:
|
||||||
|
config_obj = load_config(instance_name)
|
||||||
|
config_data = config_obj.read_file(instance_name)
|
||||||
|
|
||||||
|
# Apply updates
|
||||||
|
for update in updates:
|
||||||
|
path_parts = update.path.split(".")
|
||||||
|
# Navigate to the nested dict and update
|
||||||
|
current = config_data
|
||||||
|
for part in path_parts[:-1]:
|
||||||
|
if part not in current:
|
||||||
|
current[part] = {}
|
||||||
|
current = current[part]
|
||||||
|
current[path_parts[-1]] = update.value
|
||||||
|
|
||||||
|
# Save config
|
||||||
|
config_obj.write_file(instance_name, config_data)
|
||||||
|
|
||||||
|
logger.info(f"Updated config for {instance_name}")
|
||||||
|
return {"status": "success", "message": "Config updated"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create")
|
||||||
|
async def create_instance(name: str, copy_from: str = "template-aas"):
|
||||||
|
"""Create a new alas instance"""
|
||||||
|
try:
|
||||||
|
# Validate name
|
||||||
|
if name in alas_instance():
|
||||||
|
raise HTTPException(status_code=400, detail="Instance already exists")
|
||||||
|
|
||||||
|
if set(name) & set(".\\/:*?\"'<>|"):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid characters in name")
|
||||||
|
|
||||||
|
if name.lower().startswith("template"):
|
||||||
|
raise HTTPException(status_code=400, detail="Cannot start with 'template'")
|
||||||
|
|
||||||
|
# Copy config
|
||||||
|
origin_config = load_config(copy_from).read_file(copy_from)
|
||||||
|
State.config_updater.write_file(name, origin_config, get_config_mod(copy_from))
|
||||||
|
|
||||||
|
logger.info(f"Created new instance: {name}")
|
||||||
|
return {"status": "success", "name": name}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{instance_name}")
|
||||||
|
async def delete_instance(instance_name: str):
|
||||||
|
"""Delete an alas instance"""
|
||||||
|
try:
|
||||||
|
# Add implementation for deleting instance
|
||||||
|
# This would need to be added based on how configs are stored
|
||||||
|
raise HTTPException(status_code=501, detail="Delete not implemented")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
90
module/webui/fastapi_backend/routes/process.py
Normal file
90
module/webui/fastapi_backend/routes/process.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""
|
||||||
|
Process management API endpoints
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from module.webui.process_manager import ProcessManager
|
||||||
|
from module.webui.updater import updater
|
||||||
|
from module.logger import logger
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessCommand(BaseModel):
|
||||||
|
"""Process command model"""
|
||||||
|
task: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{instance_name}/status")
|
||||||
|
async def get_process_status(instance_name: str):
|
||||||
|
"""Get process status"""
|
||||||
|
try:
|
||||||
|
alas = ProcessManager.get_manager(instance_name)
|
||||||
|
return {
|
||||||
|
"name": instance_name,
|
||||||
|
"alive": alas.alive,
|
||||||
|
"state": alas.state,
|
||||||
|
"config_name": alas.config_name
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=404, detail=f"Process not found: {instance_name}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{instance_name}/start")
|
||||||
|
async def start_process(instance_name: str, command: ProcessCommand = ProcessCommand()):
|
||||||
|
"""Start a process"""
|
||||||
|
try:
|
||||||
|
alas = ProcessManager.get_manager(instance_name)
|
||||||
|
alas.start(command.task, updater.event if command.task is None else None)
|
||||||
|
logger.info(f"Started process: {instance_name}")
|
||||||
|
return {"status": "success", "message": f"Started {instance_name}"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{instance_name}/stop")
|
||||||
|
async def stop_process(instance_name: str):
|
||||||
|
"""Stop a process"""
|
||||||
|
try:
|
||||||
|
alas = ProcessManager.get_manager(instance_name)
|
||||||
|
alas.stop()
|
||||||
|
logger.info(f"Stopped process: {instance_name}")
|
||||||
|
return {"status": "success", "message": f"Stopped {instance_name}"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{instance_name}/restart")
|
||||||
|
async def restart_process(instance_name: str):
|
||||||
|
"""Restart a process"""
|
||||||
|
try:
|
||||||
|
alas = ProcessManager.get_manager(instance_name)
|
||||||
|
alas.stop()
|
||||||
|
alas.start(None, updater.event)
|
||||||
|
logger.info(f"Restarted process: {instance_name}")
|
||||||
|
return {"status": "success", "message": f"Restarted {instance_name}"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def get_all_processes():
|
||||||
|
"""Get status of all processes"""
|
||||||
|
try:
|
||||||
|
processes = []
|
||||||
|
for name, alas in ProcessManager._processes.items():
|
||||||
|
processes.append({
|
||||||
|
"name": name,
|
||||||
|
"alive": alas.alive,
|
||||||
|
"state": alas.state
|
||||||
|
})
|
||||||
|
return {"processes": processes}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
115
module/webui/fastapi_backend/routes/system.py
Normal file
115
module/webui/fastapi_backend/routes/system.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
System management API endpoints
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from module.webui.updater import updater
|
||||||
|
from module.webui.setting import State
|
||||||
|
from module.webui import lang
|
||||||
|
from module.logger import logger
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class LanguageSetting(BaseModel):
|
||||||
|
"""Language setting model"""
|
||||||
|
language: str
|
||||||
|
|
||||||
|
|
||||||
|
class ThemeSetting(BaseModel):
|
||||||
|
"""Theme setting model"""
|
||||||
|
theme: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/info")
|
||||||
|
async def get_system_info():
|
||||||
|
"""Get system information"""
|
||||||
|
return {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": lang.LANG,
|
||||||
|
"theme": State.deploy_config.Theme,
|
||||||
|
"deploy_config": {
|
||||||
|
"host": State.deploy_config.WebuiHost,
|
||||||
|
"port": State.deploy_config.WebuiPort,
|
||||||
|
"password_enabled": State.deploy_config.Password is not None,
|
||||||
|
"remote_access": State.deploy_config.EnableRemoteAccess,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/language")
|
||||||
|
async def set_language(setting: LanguageSetting):
|
||||||
|
"""Set system language"""
|
||||||
|
try:
|
||||||
|
lang.set_language(setting.language)
|
||||||
|
State.deploy_config.Language = setting.language
|
||||||
|
return {"status": "success", "language": setting.language}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/theme")
|
||||||
|
async def set_theme(setting: ThemeSetting):
|
||||||
|
"""Set system theme"""
|
||||||
|
try:
|
||||||
|
State.deploy_config.Theme = setting.theme
|
||||||
|
State.theme = setting.theme
|
||||||
|
return {"status": "success", "theme": setting.theme}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/update/status")
|
||||||
|
async def get_update_status():
|
||||||
|
"""Get update status"""
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"state": updater.state,
|
||||||
|
"branch": updater.Branch,
|
||||||
|
"local_commit": updater.get_commit(short_sha1=True),
|
||||||
|
"upstream_commit": updater.get_commit(f"origin/{updater.Branch}", short_sha1=True)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update/check")
|
||||||
|
async def check_update():
|
||||||
|
"""Check for updates"""
|
||||||
|
try:
|
||||||
|
updater.check_update()
|
||||||
|
return {"status": "success", "message": "Checking for updates"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update/run")
|
||||||
|
async def run_update():
|
||||||
|
"""Run update"""
|
||||||
|
try:
|
||||||
|
updater.run_update()
|
||||||
|
return {"status": "success", "message": "Update started"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/restart")
|
||||||
|
async def restart_system():
|
||||||
|
"""Restart the system"""
|
||||||
|
try:
|
||||||
|
if State.restart_event is not None:
|
||||||
|
State.restart_event.set()
|
||||||
|
return {"status": "success", "message": "Restart initiated"}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail="Restart not enabled")
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
371
module/webui/fastapi_backend/templates/index.html
Normal file
371
module/webui/fastapi_backend/templates/index.html
Normal file
@ -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>
|
||||||
114
module/webui/fastapi_backend/websocket_handler.py
Normal file
114
module/webui/fastapi_backend/websocket_handler.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
"""
|
||||||
|
WebSocket handler for real-time log streaming
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from typing import Set
|
||||||
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||||
|
from module.webui.process_manager import ProcessManager
|
||||||
|
from module.logger import logger
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionManager:
|
||||||
|
"""Manage WebSocket connections"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.active_connections: Set[WebSocket] = set()
|
||||||
|
|
||||||
|
async def connect(self, websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
self.active_connections.add(websocket)
|
||||||
|
|
||||||
|
def disconnect(self, websocket: WebSocket):
|
||||||
|
self.active_connections.discard(websocket)
|
||||||
|
|
||||||
|
async def broadcast(self, message: str):
|
||||||
|
for connection in self.active_connections.copy():
|
||||||
|
try:
|
||||||
|
await connection.send_text(message)
|
||||||
|
except Exception:
|
||||||
|
self.disconnect(connection)
|
||||||
|
|
||||||
|
|
||||||
|
manager = ConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
|
@router.websocket("/logs/{instance_name}")
|
||||||
|
async def websocket_logs(websocket: WebSocket, instance_name: str):
|
||||||
|
"""WebSocket endpoint for streaming logs"""
|
||||||
|
await manager.connect(websocket)
|
||||||
|
|
||||||
|
try:
|
||||||
|
alas = ProcessManager.get_manager(instance_name)
|
||||||
|
|
||||||
|
# Send initial connection message
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "connected",
|
||||||
|
"instance": instance_name
|
||||||
|
})
|
||||||
|
|
||||||
|
# Keep connection alive and send log updates
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Check if process is alive
|
||||||
|
if hasattr(alas, 'alive') and alas.alive:
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "status",
|
||||||
|
"alive": True,
|
||||||
|
"state": alas.state
|
||||||
|
})
|
||||||
|
|
||||||
|
# Wait a bit before next update
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
# Check if client sent any message (to keep connection alive)
|
||||||
|
try:
|
||||||
|
data = await asyncio.wait_for(websocket.receive_text(), timeout=0.1)
|
||||||
|
if data == "ping":
|
||||||
|
await websocket.send_text("pong")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in WebSocket loop: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
logger.info(f"WebSocket disconnected for {instance_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"WebSocket error: {e}")
|
||||||
|
finally:
|
||||||
|
manager.disconnect(websocket)
|
||||||
|
|
||||||
|
|
||||||
|
@router.websocket("/system")
|
||||||
|
async def websocket_system(websocket: WebSocket):
|
||||||
|
"""WebSocket endpoint for system-wide updates"""
|
||||||
|
await manager.connect(websocket)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "connected",
|
||||||
|
"message": "System WebSocket connected"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Keep connection alive
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = await asyncio.wait_for(websocket.receive_text(), timeout=10)
|
||||||
|
if data == "ping":
|
||||||
|
await websocket.send_text("pong")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
# Send heartbeat
|
||||||
|
await websocket.send_json({"type": "heartbeat"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in system WebSocket: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
logger.info("System WebSocket disconnected")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"System WebSocket error: {e}")
|
||||||
|
finally:
|
||||||
|
manager.disconnect(websocket)
|
||||||
@ -27,12 +27,12 @@ pponnxcr==2.0
|
|||||||
|
|
||||||
# Webui
|
# Webui
|
||||||
pywebio==1.6.2
|
pywebio==1.6.2
|
||||||
starlette==0.14.2
|
starlette>=0.27.0
|
||||||
uvicorn[standard]==0.17.6
|
uvicorn[standard]>=0.20.0
|
||||||
aiofiles
|
aiofiles
|
||||||
fastapi>=0.100.0
|
fastapi>=0.100.0
|
||||||
python-multipart
|
python-multipart
|
||||||
websockets
|
jinja2>=3.0.0
|
||||||
|
|
||||||
# GUI
|
# GUI
|
||||||
customtkinter
|
customtkinter
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user