worked backup

This commit is contained in:
2025-12-21 00:16:12 +03:00
parent d05344e604
commit 43b4c75e36
18 changed files with 273 additions and 59 deletions

2
.gitignore vendored
View File

@@ -17,3 +17,5 @@ node_modules/
build/ build/
.env* .env*
config.json config.json
backend/backups/*

View File

@@ -7,15 +7,16 @@ 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)
## Project Structure ## Project Structure
```text ```text
backend/ backend/
frontend/ frontend/
tests/ tests/
``` ```
@@ -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 -->

View File

@@ -8,4 +8,5 @@ jsonschema
requests requests
keyring keyring
httpx httpx
PyYAML PyYAML
websockets

View File

@@ -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]

View File

@@ -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]

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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}`;
}; };

View File

@@ -11,7 +11,8 @@ export default defineConfig({
}, },
'/ws': { '/ws': {
target: 'ws://localhost:8000', target: 'ws://localhost:8000',
ws: true ws: true,
changeOrigin: true
} }
} }
} }

View File

@@ -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

View File

@@ -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

View 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]

View File

@@ -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

View File

@@ -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).

View File

@@ -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
--- ---