worked backup
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,3 +17,5 @@ node_modules/
|
|||||||
build/
|
build/
|
||||||
.env*
|
.env*
|
||||||
config.json
|
config.json
|
||||||
|
|
||||||
|
backend/backups/*
|
||||||
@@ -7,7 +7,8 @@ Auto-generated from all feature plans. Last updated: 2025-12-19
|
|||||||
- Python 3.9+, Node.js 18+ + SvelteKit, FastAPI, Tailwind CSS (inferred from existing frontend) (004-integrate-svelte-kit)
|
- Python 3.9+, Node.js 18+ + SvelteKit, FastAPI, Tailwind CSS (inferred from existing frontend) (004-integrate-svelte-kit)
|
||||||
- N/A (Frontend integration) (004-integrate-svelte-kit)
|
- N/A (Frontend integration) (004-integrate-svelte-kit)
|
||||||
- Python 3.9+, Node.js 18+ + FastAPI, SvelteKit, Tailwind CSS, Pydantic (001-fix-ui-ws-validation)
|
- Python 3.9+, Node.js 18+ + FastAPI, SvelteKit, Tailwind CSS, Pydantic (001-fix-ui-ws-validation)
|
||||||
- N/A (Configuration based) (001-fix-ui-ws-validation)
|
- N/A (Configuration based) (005-fix-ui-ws-validation)
|
||||||
|
- Filesystem (plugins, logs, backups), SQLite (optional, for job history if needed) (005-fix-ui-ws-validation)
|
||||||
|
|
||||||
- Python 3.9+ (Backend), Node.js 18+ (Frontend Build) (001-plugin-arch-svelte-ui)
|
- Python 3.9+ (Backend), Node.js 18+ (Frontend Build) (001-plugin-arch-svelte-ui)
|
||||||
|
|
||||||
@@ -28,9 +29,9 @@ cd src; pytest; ruff check .
|
|||||||
Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions
|
Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
- 001-fix-ui-ws-validation: Added Python 3.9+, Node.js 18+ + FastAPI, SvelteKit, Tailwind CSS, Pydantic
|
- 001-fix-ui-ws-validation: Added Python 3.9+ (Backend), Node.js 18+ (Frontend Build)
|
||||||
- 001-fix-ui-ws-validation: Added Python 3.9+, Node.js 18+ + FastAPI, SvelteKit, Tailwind CSS, Pydantic
|
- 005-fix-ui-ws-validation: Added Python 3.9+ (Backend), Node.js 18+ (Frontend Build)
|
||||||
- 004-integrate-svelte-kit: Added Python 3.9+, Node.js 18+ + SvelteKit, FastAPI, Tailwind CSS (inferred from existing frontend)
|
- 005-fix-ui-ws-validation: Added Python 3.9+, Node.js 18+ + FastAPI, SvelteKit, Tailwind CSS, Pydantic
|
||||||
|
|
||||||
|
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ requests
|
|||||||
keyring
|
keyring
|
||||||
httpx
|
httpx
|
||||||
PyYAML
|
PyYAML
|
||||||
|
websockets
|
||||||
@@ -50,25 +50,41 @@ app.include_router(settings.router, prefix="/api/settings", tags=["Settings"])
|
|||||||
# @SEMANTICS: websocket, logs, streaming, real-time
|
# @SEMANTICS: websocket, logs, streaming, real-time
|
||||||
# @PURPOSE: Provides a WebSocket endpoint for clients to connect to and receive real-time log entries for a specific task.
|
# @PURPOSE: Provides a WebSocket endpoint for clients to connect to and receive real-time log entries for a specific task.
|
||||||
@app.websocket("/ws/logs/{task_id}")
|
@app.websocket("/ws/logs/{task_id}")
|
||||||
async def websocket_endpoint(websocket: WebSocket, task_id: str, task_manager=Depends(get_task_manager)):
|
async def websocket_endpoint(websocket: WebSocket, task_id: str):
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
logger.info(f"WebSocket connection established for task {task_id}")
|
logger.info(f"WebSocket connection accepted for task {task_id}")
|
||||||
|
task_manager = get_task_manager()
|
||||||
|
queue = await task_manager.subscribe_logs(task_id)
|
||||||
try:
|
try:
|
||||||
# Send initial logs if any
|
# Send initial logs if any
|
||||||
initial_logs = task_manager.get_task_logs(task_id)
|
initial_logs = task_manager.get_task_logs(task_id)
|
||||||
for log_entry in initial_logs:
|
for log_entry in initial_logs:
|
||||||
await websocket.send_json(log_entry.dict())
|
# Convert datetime to string for JSON serialization
|
||||||
|
log_dict = log_entry.dict()
|
||||||
|
log_dict['timestamp'] = log_dict['timestamp'].isoformat()
|
||||||
|
await websocket.send_json(log_dict)
|
||||||
|
|
||||||
# Keep connection alive, ideally stream new logs as they come
|
# Stream new logs
|
||||||
# This part requires a more sophisticated log streaming mechanism (e.g., queues, pub/sub)
|
logger.info(f"Starting log stream for task {task_id}")
|
||||||
# For now, it will just keep the connection open and send initial logs.
|
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1) # Keep connection alive, send heartbeat or check for new logs
|
log_entry = await queue.get()
|
||||||
# In a real system, new logs would be pushed here
|
log_dict = log_entry.dict()
|
||||||
|
log_dict['timestamp'] = log_dict['timestamp'].isoformat()
|
||||||
|
await websocket.send_json(log_dict)
|
||||||
|
|
||||||
|
# If task is finished, we could potentially close the connection
|
||||||
|
# but let's keep it open for a bit or until the client disconnects
|
||||||
|
if "Task completed successfully" in log_entry.message or "Task failed" in log_entry.message:
|
||||||
|
# Wait a bit to ensure client receives the last message
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
break
|
||||||
|
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
logger.info(f"WebSocket connection disconnected for task {task_id}")
|
logger.info(f"WebSocket connection disconnected for task {task_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"WebSocket error for task {task_id}: {e}")
|
logger.error(f"WebSocket error for task {task_id}: {e}")
|
||||||
|
finally:
|
||||||
|
task_manager.unsubscribe_logs(task_id, queue)
|
||||||
|
|
||||||
# [/DEF]
|
# [/DEF]
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class TaskManager:
|
|||||||
def __init__(self, plugin_loader):
|
def __init__(self, plugin_loader):
|
||||||
self.plugin_loader = plugin_loader
|
self.plugin_loader = plugin_loader
|
||||||
self.tasks: Dict[str, Task] = {}
|
self.tasks: Dict[str, Task] = {}
|
||||||
|
self.subscribers: Dict[str, List[asyncio.Queue]] = {}
|
||||||
self.executor = ThreadPoolExecutor(max_workers=5) # For CPU-bound plugin execution
|
self.executor = ThreadPoolExecutor(max_workers=5) # For CPU-bound plugin execution
|
||||||
self.loop = asyncio.get_event_loop()
|
self.loop = asyncio.get_event_loop()
|
||||||
# [/DEF]
|
# [/DEF]
|
||||||
@@ -92,7 +93,7 @@ class TaskManager:
|
|||||||
|
|
||||||
task.status = TaskStatus.RUNNING
|
task.status = TaskStatus.RUNNING
|
||||||
task.started_at = datetime.utcnow()
|
task.started_at = datetime.utcnow()
|
||||||
task.logs.append(LogEntry(level="INFO", message=f"Task started for plugin '{plugin.name}'"))
|
self._add_log(task_id, "INFO", f"Task started for plugin '{plugin.name}'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Execute plugin in a separate thread to avoid blocking the event loop
|
# Execute plugin in a separate thread to avoid blocking the event loop
|
||||||
@@ -103,10 +104,10 @@ class TaskManager:
|
|||||||
lambda: asyncio.run(plugin.execute(task.params)) if asyncio.iscoroutinefunction(plugin.execute) else plugin.execute(task.params)
|
lambda: asyncio.run(plugin.execute(task.params)) if asyncio.iscoroutinefunction(plugin.execute) else plugin.execute(task.params)
|
||||||
)
|
)
|
||||||
task.status = TaskStatus.SUCCESS
|
task.status = TaskStatus.SUCCESS
|
||||||
task.logs.append(LogEntry(level="INFO", message=f"Task completed successfully for plugin '{plugin.name}'"))
|
self._add_log(task_id, "INFO", f"Task completed successfully for plugin '{plugin.name}'")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
task.status = TaskStatus.FAILED
|
task.status = TaskStatus.FAILED
|
||||||
task.logs.append(LogEntry(level="ERROR", message=f"Task failed: {e}", context={"error_type": type(e).__name__}))
|
self._add_log(task_id, "ERROR", f"Task failed: {e}", {"error_type": type(e).__name__})
|
||||||
finally:
|
finally:
|
||||||
task.finished_at = datetime.utcnow()
|
task.finished_at = datetime.utcnow()
|
||||||
# In a real system, you might notify clients via WebSocket here
|
# In a real system, you might notify clients via WebSocket here
|
||||||
@@ -129,3 +130,38 @@ class TaskManager:
|
|||||||
"""
|
"""
|
||||||
task = self.tasks.get(task_id)
|
task = self.tasks.get(task_id)
|
||||||
return task.logs if task else []
|
return task.logs if task else []
|
||||||
|
|
||||||
|
def _add_log(self, task_id: str, level: str, message: str, context: Optional[Dict[str, Any]] = None):
|
||||||
|
"""
|
||||||
|
Adds a log entry to a task and notifies subscribers.
|
||||||
|
"""
|
||||||
|
task = self.tasks.get(task_id)
|
||||||
|
if not task:
|
||||||
|
return
|
||||||
|
|
||||||
|
log_entry = LogEntry(level=level, message=message, context=context)
|
||||||
|
task.logs.append(log_entry)
|
||||||
|
|
||||||
|
# Notify subscribers
|
||||||
|
if task_id in self.subscribers:
|
||||||
|
for queue in self.subscribers[task_id]:
|
||||||
|
self.loop.call_soon_threadsafe(queue.put_nowait, log_entry)
|
||||||
|
|
||||||
|
async def subscribe_logs(self, task_id: str) -> asyncio.Queue:
|
||||||
|
"""
|
||||||
|
Subscribes to real-time logs for a task.
|
||||||
|
"""
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
if task_id not in self.subscribers:
|
||||||
|
self.subscribers[task_id] = []
|
||||||
|
self.subscribers[task_id].append(queue)
|
||||||
|
return queue
|
||||||
|
|
||||||
|
def unsubscribe_logs(self, task_id: str, queue: asyncio.Queue):
|
||||||
|
"""
|
||||||
|
Unsubscribes from real-time logs for a task.
|
||||||
|
"""
|
||||||
|
if task_id in self.subscribers:
|
||||||
|
self.subscribers[task_id].remove(queue)
|
||||||
|
if not self.subscribers[task_id]:
|
||||||
|
del self.subscribers[task_id]
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const options = {
|
|||||||
app: ({ head, body, assets, nonce, env }) => "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"" + assets + "/favicon.png\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t" + head + "\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n",
|
app: ({ head, body, assets, nonce, env }) => "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"" + assets + "/favicon.png\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t" + head + "\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n",
|
||||||
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
|
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
|
||||||
},
|
},
|
||||||
version_hash: "cx7alu"
|
version_hash: "1eogxsl"
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function get_hooks() {
|
export async function get_hooks() {
|
||||||
|
|||||||
@@ -14,39 +14,107 @@
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { selectedTask, taskLogs } from '../lib/stores.js';
|
import { selectedTask, taskLogs } from '../lib/stores.js';
|
||||||
import { getWsUrl } from '../lib/api.js';
|
import { getWsUrl } from '../lib/api.js';
|
||||||
|
import { addToast } from '../lib/toasts.js';
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
let ws;
|
let ws;
|
||||||
|
let reconnectAttempts = 0;
|
||||||
|
let maxReconnectAttempts = 10;
|
||||||
|
let initialReconnectDelay = 1000;
|
||||||
|
let maxReconnectDelay = 30000;
|
||||||
|
let reconnectTimeout;
|
||||||
|
let waitingForData = false;
|
||||||
|
let dataTimeout;
|
||||||
|
let connectionStatus = 'disconnected'; // 'connecting', 'connected', 'disconnected', 'waiting', 'completed'
|
||||||
|
|
||||||
|
// [DEF:connect:Function]
|
||||||
|
/**
|
||||||
|
* @purpose Establishes WebSocket connection with exponential backoff.
|
||||||
|
*/
|
||||||
|
function connect() {
|
||||||
|
const task = get(selectedTask);
|
||||||
|
if (!task || connectionStatus === 'completed') return;
|
||||||
|
|
||||||
|
console.log(`[TaskRunner][Entry] Connecting to logs for task: ${task.id} (Attempt ${reconnectAttempts + 1})`);
|
||||||
|
connectionStatus = 'connecting';
|
||||||
|
|
||||||
|
const wsUrl = getWsUrl(task.id);
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('[TaskRunner][Coherence:OK] WebSocket connection established');
|
||||||
|
connectionStatus = 'connected';
|
||||||
|
reconnectAttempts = 0;
|
||||||
|
startDataTimeout();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const logEntry = JSON.parse(event.data);
|
||||||
|
taskLogs.update(logs => [...logs, logEntry]);
|
||||||
|
resetDataTimeout();
|
||||||
|
|
||||||
|
// Check for completion message (if backend sends one)
|
||||||
|
if (logEntry.message && logEntry.message.includes('Task completed successfully')) {
|
||||||
|
connectionStatus = 'completed';
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('[TaskRunner][Coherence:Failed] WebSocket error:', error);
|
||||||
|
connectionStatus = 'disconnected';
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = (event) => {
|
||||||
|
console.log(`[TaskRunner][Exit] WebSocket connection closed (Code: ${event.code})`);
|
||||||
|
clearTimeout(dataTimeout);
|
||||||
|
waitingForData = false;
|
||||||
|
|
||||||
|
if (connectionStatus !== 'completed' && reconnectAttempts < maxReconnectAttempts) {
|
||||||
|
const delay = Math.min(initialReconnectDelay * Math.pow(2, reconnectAttempts), maxReconnectDelay);
|
||||||
|
console.log(`[TaskRunner][Action] Reconnecting in ${delay}ms...`);
|
||||||
|
reconnectTimeout = setTimeout(() => {
|
||||||
|
reconnectAttempts++;
|
||||||
|
connect();
|
||||||
|
}, delay);
|
||||||
|
} else if (reconnectAttempts >= maxReconnectAttempts) {
|
||||||
|
console.error('[TaskRunner][Coherence:Failed] Max reconnect attempts reached.');
|
||||||
|
addToast('Failed to connect to log stream after multiple attempts.', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// [/DEF:connect]
|
||||||
|
|
||||||
|
function startDataTimeout() {
|
||||||
|
waitingForData = false;
|
||||||
|
dataTimeout = setTimeout(() => {
|
||||||
|
if (connectionStatus === 'connected') {
|
||||||
|
waitingForData = true;
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDataTimeout() {
|
||||||
|
clearTimeout(dataTimeout);
|
||||||
|
waitingForData = false;
|
||||||
|
startDataTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
// [DEF:onMount:Function]
|
// [DEF:onMount:Function]
|
||||||
/**
|
|
||||||
* @purpose Initialize WebSocket connection for task logs.
|
|
||||||
*/
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const task = get(selectedTask);
|
// Subscribe to selectedTask changes
|
||||||
if (task) {
|
const unsubscribe = selectedTask.subscribe(task => {
|
||||||
console.log(`[TaskRunner][Entry] Connecting to logs for task: ${task.id}`);
|
if (task) {
|
||||||
taskLogs.set([]); // Clear previous logs
|
console.log(`[TaskRunner][Action] Task selected: ${task.id}. Initializing connection.`);
|
||||||
const wsUrl = getWsUrl(task.id);
|
if (ws) ws.close();
|
||||||
ws = new WebSocket(wsUrl);
|
clearTimeout(reconnectTimeout);
|
||||||
|
reconnectAttempts = 0;
|
||||||
ws.onopen = () => {
|
connectionStatus = 'disconnected';
|
||||||
console.log('[TaskRunner][Coherence:OK] WebSocket connection established');
|
taskLogs.set([]);
|
||||||
};
|
connect();
|
||||||
|
}
|
||||||
ws.onmessage = (event) => {
|
});
|
||||||
const logEntry = JSON.parse(event.data);
|
return unsubscribe;
|
||||||
taskLogs.update(logs => [...logs, logEntry]);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error('[TaskRunner][Coherence:Failed] WebSocket error:', error);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.log('[TaskRunner][Exit] WebSocket connection closed');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// [/DEF:onMount]
|
// [/DEF:onMount]
|
||||||
|
|
||||||
@@ -55,6 +123,8 @@
|
|||||||
* @purpose Close WebSocket connection when the component is destroyed.
|
* @purpose Close WebSocket connection when the component is destroyed.
|
||||||
*/
|
*/
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
clearTimeout(reconnectTimeout);
|
||||||
|
clearTimeout(dataTimeout);
|
||||||
if (ws) {
|
if (ws) {
|
||||||
console.log("[TaskRunner][Action] Closing WebSocket connection.");
|
console.log("[TaskRunner][Action] Closing WebSocket connection.");
|
||||||
ws.close();
|
ws.close();
|
||||||
@@ -66,8 +136,29 @@
|
|||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
<div class="p-4 border rounded-lg bg-white shadow-md">
|
<div class="p-4 border rounded-lg bg-white shadow-md">
|
||||||
{#if $selectedTask}
|
{#if $selectedTask}
|
||||||
<h2 class="text-xl font-semibold mb-2">Task: {$selectedTask.plugin_id}</h2>
|
<div class="flex justify-between items-center mb-2">
|
||||||
<div class="bg-gray-900 text-white font-mono text-sm p-4 rounded-md h-96 overflow-y-auto">
|
<h2 class="text-xl font-semibold">Task: {$selectedTask.plugin_id}</h2>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
{#if connectionStatus === 'connecting'}
|
||||||
|
<span class="flex h-3 w-3 relative">
|
||||||
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-yellow-400 opacity-75"></span>
|
||||||
|
<span class="relative inline-flex rounded-full h-3 w-3 bg-yellow-500"></span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-500">Connecting...</span>
|
||||||
|
{:else if connectionStatus === 'connected'}
|
||||||
|
<span class="h-3 w-3 rounded-full bg-green-500"></span>
|
||||||
|
<span class="text-xs text-gray-500">Live</span>
|
||||||
|
{:else if connectionStatus === 'completed'}
|
||||||
|
<span class="h-3 w-3 rounded-full bg-blue-500"></span>
|
||||||
|
<span class="text-xs text-gray-500">Completed</span>
|
||||||
|
{:else}
|
||||||
|
<span class="h-3 w-3 rounded-full bg-red-500"></span>
|
||||||
|
<span class="text-xs text-gray-500">Disconnected</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-900 text-white font-mono text-sm p-4 rounded-md h-96 overflow-y-auto relative">
|
||||||
{#each $taskLogs as log}
|
{#each $taskLogs as log}
|
||||||
<div>
|
<div>
|
||||||
<span class="text-gray-400">{new Date(log.timestamp).toLocaleTimeString()}</span>
|
<span class="text-gray-400">{new Date(log.timestamp).toLocaleTimeString()}</span>
|
||||||
@@ -75,6 +166,12 @@
|
|||||||
<span>{log.message}</span>
|
<span>{log.message}</span>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
{#if waitingForData}
|
||||||
|
<div class="text-gray-500 italic mt-2 animate-pulse">
|
||||||
|
Waiting for data...
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p>No task selected.</p>
|
<p>No task selected.</p>
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ const API_BASE_URL = '/api';
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export const getWsUrl = (taskId) => {
|
export const getWsUrl = (taskId) => {
|
||||||
const baseUrl = PUBLIC_WS_URL || `ws://${window.location.hostname}:8000`;
|
let baseUrl = PUBLIC_WS_URL;
|
||||||
|
if (!baseUrl) {
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
// Use the current host and port to allow Vite proxy to handle the connection
|
||||||
|
baseUrl = `${protocol}//${window.location.host}`;
|
||||||
|
}
|
||||||
return `${baseUrl}/ws/logs/${taskId}`;
|
return `${baseUrl}/ws/logs/${taskId}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
'/ws': {
|
'/ws': {
|
||||||
target: 'ws://localhost:8000',
|
target: 'ws://localhost:8000',
|
||||||
ws: true
|
ws: true,
|
||||||
|
changeOrigin: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Tasks: Plugin Architecture & Svelte Web UI
|
# Tasks: Plugin Architecture & Svelte Web UI
|
||||||
|
|
||||||
**Feature**: `001-plugin-arch-svelte-ui`
|
**Feature**: `001-plugin-arch-svelte-ui`
|
||||||
**Status**: Planned
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
**Created**: 2025-12-20
|
**Created**: 2025-12-20
|
||||||
**Feature**: [specs/001-fix-ui-ws-validation/spec.md](../spec.md)
|
**Feature**: [specs/005-fix-ui-ws-validation/spec.md](../spec.md)
|
||||||
|
|
||||||
## Content Quality
|
## Content Quality
|
||||||
|
|
||||||
38
specs/005-fix-ui-ws-validation/checklists/ws-connection.md
Normal file
38
specs/005-fix-ui-ws-validation/checklists/ws-connection.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Requirements Quality Checklist: WebSocket Connection
|
||||||
|
|
||||||
|
## Meta
|
||||||
|
- **Feature**: Fix UI Styling, WebSocket Port Mismatch, and URL Validation
|
||||||
|
- **Domain**: WebSocket / Real-time Logs
|
||||||
|
- **Focus**: Connection Lifecycle & Environment Requirements
|
||||||
|
- **Depth**: Lightweight Sanity
|
||||||
|
- **Created**: 2025-12-20
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
- [x] CHK001 - Are the environment-specific URL construction rules (e.g., `ws` vs `wss`) explicitly defined for different deployment targets? [Gap]
|
||||||
|
- [x] CHK002 - Is the fallback mechanism for `PUBLIC_WS_URL` documented for production environments where port 8000 might be blocked? [Completeness, Spec §FR-002]
|
||||||
|
- [x] CHK003 - Are requirements defined for handling WebSocket authentication/authorization if the API becomes protected? [Gap - Out of scope for this fix, handled by global ADFS requirement]
|
||||||
|
|
||||||
|
## Requirement Clarity
|
||||||
|
- [x] CHK004 - Is the "exponential backoff" strategy quantified with specific initial delays, multipliers, and maximum retry counts? [Clarity, Spec §Clarifications]
|
||||||
|
- [x] CHK005 - Are the visual feedback requirements for connection failure (toast vs status indicator) clearly prioritized or combined? [Clarity, Spec §FR-004]
|
||||||
|
- [x] CHK006 - Is the term "real-time" quantified with a maximum latency threshold for log delivery? [Clarity, Spec §SC-004]
|
||||||
|
|
||||||
|
## Requirement Consistency
|
||||||
|
- [x] CHK007 - Does the WebSocket endpoint path in the contract (`/ws/logs/{id}`) align with the implementation plan and frontend routing? [Conflict, Contract §Endpoint]
|
||||||
|
- [x] CHK008 - Are the error handling requirements in the contract consistent with the visual feedback requirements in the spec? [Consistency, Contract §Error Handling]
|
||||||
|
|
||||||
|
## Acceptance Criteria Quality
|
||||||
|
- [x] CHK009 - Can the "100% success rate" in development be objectively measured and verified across different OS/browsers? [Measurability, Spec §SC-002]
|
||||||
|
- [x] CHK010 - Is there a measurable criterion for "successful reconnection" (e.g., within X attempts or Y seconds)? [Gap - Defined in Clarifications]
|
||||||
|
|
||||||
|
## Scenario Coverage
|
||||||
|
- [x] CHK011 - Are requirements specified for the "Partial Log" scenario (e.g., connection established but no data received)? [Coverage, Gap]
|
||||||
|
- [x] CHK012 - Does the spec define the behavior when a task completes while the WebSocket is still active? [Coverage, Gap]
|
||||||
|
|
||||||
|
## Edge Case Coverage
|
||||||
|
- [x] CHK013 - Does the spec define the behavior when the backend port (8000) is unavailable or occupied by another process? [Edge Case, Spec §FR-002]
|
||||||
|
- [x] CHK014 - Are requirements defined for handling browser-side WebSocket limits (e.g., maximum concurrent connections)? [Edge Case, Gap - Handled by single-connection-per-task design]
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
- [x] CHK015 - Are there requirements for WebSocket connection stability under high network jitter or packet loss? [Gap - Handled by exponential backoff]
|
||||||
|
- [x] CHK016 - Is the impact of long-lived WebSocket connections on server resources (memory/CPU) addressed? [Gap - Handled by graceful closing on task completion]
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Implementation Plan: Fix UI Styling, WebSocket Port Mismatch, and URL Validation
|
# Implementation Plan: Fix UI Styling, WebSocket Port Mismatch, and URL Validation
|
||||||
|
|
||||||
**Branch**: `001-fix-ui-ws-validation` | **Date**: 2025-12-20 | **Spec**: [specs/001-fix-ui-ws-validation/spec.md](specs/001-fix-ui-ws-validation/spec.md)
|
**Branch**: `005-fix-ui-ws-validation` | **Date**: 2025-12-20 | **Spec**: [specs/005-fix-ui-ws-validation/spec.md](specs/005-fix-ui-ws-validation/spec.md)
|
||||||
|
|
||||||
**Input**: Feature specification from `/specs/001-fix-ui-ws-validation/spec.md`
|
**Input**: Feature specification from `/specs/005-fix-ui-ws-validation/spec.md`
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ This feature addresses three critical issues: unstyled UI due to missing Tailwin
|
|||||||
### Documentation (this feature)
|
### Documentation (this feature)
|
||||||
|
|
||||||
```text
|
```text
|
||||||
specs/001-fix-ui-ws-validation/
|
specs/005-fix-ui-ws-validation/
|
||||||
├── plan.md # This file
|
├── plan.md # This file
|
||||||
├── research.md # Phase 0 output
|
├── research.md # Phase 0 output
|
||||||
├── data-model.md # Phase 1 output
|
├── data-model.md # Phase 1 output
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Feature Specification: Fix UI Styling, WebSocket Port Mismatch, and URL Validation
|
# Feature Specification: Fix UI Styling, WebSocket Port Mismatch, and URL Validation
|
||||||
|
|
||||||
**Feature Branch**: `001-fix-ui-ws-validation`
|
**Feature Branch**: `005-fix-ui-ws-validation`
|
||||||
**Created**: 2025-12-20
|
**Created**: 2025-12-20
|
||||||
**Status**: Draft
|
**Status**: Draft
|
||||||
**Input**: User description: "UI Styling: Tailwind CSS is not imported in the root layout, causing the unstyled appearance. WebSocket Mismatch: Port mismatch in dev mode is breaking real-time logs. Validation Error: Strict URL validation in superset_tool/models.py requires /api/v1, which caused the connection failure reported in your feedback."
|
**Input**: User description: "UI Styling: Tailwind CSS is not imported in the root layout, causing the unstyled appearance. WebSocket Mismatch: Port mismatch in dev mode is breaking real-time logs. Validation Error: Strict URL validation in superset_tool/models.py requires /api/v1, which caused the connection failure reported in your feedback."
|
||||||
@@ -64,8 +64,13 @@ As an administrator, I want to connect to external services using their base URL
|
|||||||
|
|
||||||
- **FR-001**: System MUST ensure global styling (Tailwind CSS) is imported in `src/routes/+layout.svelte` to ensure consistent appearance.
|
- **FR-001**: System MUST ensure global styling (Tailwind CSS) is imported in `src/routes/+layout.svelte` to ensure consistent appearance.
|
||||||
- **FR-002**: System MUST use an environment variable (e.g., `PUBLIC_WS_URL`) with a fallback to the backend port (8000) to determine the WebSocket connection URL.
|
- **FR-002**: System MUST use an environment variable (e.g., `PUBLIC_WS_URL`) with a fallback to the backend port (8000) to determine the WebSocket connection URL.
|
||||||
|
- **FR-002.2**: In production environments, `PUBLIC_WS_URL` MUST be explicitly configured to avoid reliance on the port 8000 fallback.
|
||||||
|
- **FR-002.1**: Protocol MUST be environment-aware: use `wss://` if the page is served over HTTPS, otherwise `ws://`.
|
||||||
- **FR-003**: System MUST relax URL validation for external services to allow base URLs and automatically append `/api/v1` if the version suffix is missing.
|
- **FR-003**: System MUST relax URL validation for external services to allow base URLs and automatically append `/api/v1` if the version suffix is missing.
|
||||||
- **FR-004**: System MUST provide visual feedback (toast notification and status indicator in log view) when a real-time connection fails to establish.
|
- **FR-004**: System MUST provide visual feedback (toast notification and status indicator in log view) when a real-time connection fails to establish.
|
||||||
|
- **FR-004.1**: System MUST handle "Partial Log" scenarios by displaying a "Waiting for data..." indicator if the connection is open but no messages are received within 5 seconds.
|
||||||
|
- **FR-004.2**: System MUST handle "Task Completed" state by closing the WebSocket gracefully and displaying a final status summary.
|
||||||
|
- **FR-004.3**: If the backend port (8000) is unavailable, the frontend MUST display a clear error message indicating the service is unreachable.
|
||||||
- **FR-005**: System MUST ensure that service clients correctly handle API versioning internally by using the normalized URL.
|
- **FR-005**: System MUST ensure that service clients correctly handle API versioning internally by using the normalized URL.
|
||||||
|
|
||||||
### Key Entities *(include if feature involves data)*
|
### Key Entities *(include if feature involves data)*
|
||||||
@@ -87,7 +92,7 @@ As an administrator, I want to connect to external services using their base URL
|
|||||||
## Clarifications
|
## Clarifications
|
||||||
|
|
||||||
### Session 2025-12-20
|
### Session 2025-12-20
|
||||||
- Q: WebSocket Reconnection Strategy → A: Automatic reconnection with exponential backoff (Option A).
|
- Q: WebSocket Reconnection Strategy → A: Automatic reconnection with exponential backoff (Initial delay: 1s, Multiplier: 2x, Max delay: 30s, Max retries: 10).
|
||||||
- Q: URL Validation Strictness → A: Automatically append `/api/v1` if missing (Option A).
|
- Q: URL Validation Strictness → A: Automatically append `/api/v1` if missing (Option A).
|
||||||
- Q: Global Styling Implementation → A: Import in `src/routes/+layout.svelte` (Option A).
|
- Q: Global Styling Implementation → A: Import in `src/routes/+layout.svelte` (Option A).
|
||||||
- Q: WebSocket Port Configuration → A: Use environment variable with fallback (Option A).
|
- Q: WebSocket Port Configuration → A: Use environment variable with fallback (Option A).
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Tasks: Fix UI Styling, WebSocket Port Mismatch, and URL Validation
|
# Tasks: Fix UI Styling, WebSocket Port Mismatch, and URL Validation
|
||||||
|
|
||||||
**Input**: Design documents from `/specs/001-fix-ui-ws-validation/`
|
**Input**: Design documents from `/specs/005-fix-ui-ws-validation/`
|
||||||
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
|
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
|
||||||
|
|
||||||
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
||||||
@@ -81,7 +81,19 @@
|
|||||||
**Purpose**: Improvements that affect multiple user stories
|
**Purpose**: Improvements that affect multiple user stories
|
||||||
|
|
||||||
- [x] T009 [P] Update `docs/settings.md` with new URL validation behavior
|
- [x] T009 [P] Update `docs/settings.md` with new URL validation behavior
|
||||||
- [ ] T010 Run full verification suite per `quickstart.md`
|
- [x] T010 Run full verification suite per `quickstart.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7: Addressing Requirements Gaps (from ws-connection.md)
|
||||||
|
|
||||||
|
**Purpose**: Close gaps identified during requirements quality review to ensure robust WebSocket communication.
|
||||||
|
|
||||||
|
- [x] T011 [US2] Resolve WebSocket endpoint path conflict between contract and implementation (Contract: `/ws/logs/{id}` vs Actual: `/ws/logs/{id}`)
|
||||||
|
- [x] T012 [US2] Implement environment-aware protocol selection (`ws` vs `wss`) based on `PUBLIC_WS_URL`
|
||||||
|
- [x] T013 [US2] Implement robust exponential backoff with specific initial delays and max retry counts
|
||||||
|
- [x] T014 [US2] Add UI handling for "Partial Log" and "Task Completed" WebSocket states in `TaskRunner.svelte`
|
||||||
|
- [x] T015 [US2] Implement backend port availability check and user-friendly error reporting in the frontend
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Reference in New Issue
Block a user