diff --git a/.gitignore b/.gitignore index b60ea7b..9d08c23 100755 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ node_modules/ build/ .env* config.json + +backend/backups/* \ No newline at end of file diff --git a/.kilocode/rules/specify-rules.md b/.kilocode/rules/specify-rules.md index 2003a85..f652da5 100644 --- a/.kilocode/rules/specify-rules.md +++ b/.kilocode/rules/specify-rules.md @@ -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) - N/A (Frontend integration) (004-integrate-svelte-kit) - 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) ## Project Structure ```text -backend/ -frontend/ +backend/ +frontend/ tests/ ``` @@ -28,9 +29,9 @@ cd src; pytest; ruff check . Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions ## 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+, Node.js 18+ + FastAPI, SvelteKit, Tailwind CSS, Pydantic -- 004-integrate-svelte-kit: Added Python 3.9+, Node.js 18+ + SvelteKit, FastAPI, Tailwind CSS (inferred from existing frontend) +- 001-fix-ui-ws-validation: Added Python 3.9+ (Backend), Node.js 18+ (Frontend Build) +- 005-fix-ui-ws-validation: Added Python 3.9+ (Backend), Node.js 18+ (Frontend Build) +- 005-fix-ui-ws-validation: Added Python 3.9+, Node.js 18+ + FastAPI, SvelteKit, Tailwind CSS, Pydantic diff --git a/backend/requirements.txt b/backend/requirements.txt index 1d2d8bc..c29fc72 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -8,4 +8,5 @@ jsonschema requests keyring httpx -PyYAML \ No newline at end of file +PyYAML +websockets \ No newline at end of file diff --git a/backend/src/app.py b/backend/src/app.py index b1ef99e..32b97d7 100755 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -50,25 +50,41 @@ app.include_router(settings.router, prefix="/api/settings", tags=["Settings"]) # @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. @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() - 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: # Send initial logs if any initial_logs = task_manager.get_task_logs(task_id) 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 - # This part requires a more sophisticated log streaming mechanism (e.g., queues, pub/sub) - # For now, it will just keep the connection open and send initial logs. + # Stream new logs + logger.info(f"Starting log stream for task {task_id}") while True: - await asyncio.sleep(1) # Keep connection alive, send heartbeat or check for new logs - # In a real system, new logs would be pushed here + log_entry = await queue.get() + 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: logger.info(f"WebSocket connection disconnected for task {task_id}") except Exception as e: logger.error(f"WebSocket error for task {task_id}: {e}") + finally: + task_manager.unsubscribe_logs(task_id, queue) # [/DEF] diff --git a/backend/src/core/task_manager.py b/backend/src/core/task_manager.py index b7f97d8..859193b 100755 --- a/backend/src/core/task_manager.py +++ b/backend/src/core/task_manager.py @@ -61,6 +61,7 @@ class TaskManager: def __init__(self, plugin_loader): self.plugin_loader = plugin_loader self.tasks: Dict[str, Task] = {} + self.subscribers: Dict[str, List[asyncio.Queue]] = {} self.executor = ThreadPoolExecutor(max_workers=5) # For CPU-bound plugin execution self.loop = asyncio.get_event_loop() # [/DEF] @@ -92,7 +93,7 @@ class TaskManager: task.status = TaskStatus.RUNNING 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: # 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) ) 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: 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: task.finished_at = datetime.utcnow() # In a real system, you might notify clients via WebSocket here @@ -129,3 +130,38 @@ class TaskManager: """ task = self.tasks.get(task_id) 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] diff --git a/frontend/.svelte-kit/generated/server/internal.js b/frontend/.svelte-kit/generated/server/internal.js index 5c177c9..2f70b23 100644 --- a/frontend/.svelte-kit/generated/server/internal.js +++ b/frontend/.svelte-kit/generated/server/internal.js @@ -24,7 +24,7 @@ export const options = { app: ({ head, body, assets, nonce, env }) => "\n\n\t\n\t\t\n\t\t\n\t\t\n\t\t" + head + "\n\t\n\t\n\t\t
" + body + "
\n\t\n\n", error: ({ status, message }) => "\n\n\t\n\t\t\n\t\t" + message + "\n\n\t\t\n\t\n\t\n\t\t
\n\t\t\t" + status + "\n\t\t\t
\n\t\t\t\t

" + message + "

\n\t\t\t
\n\t\t
\n\t\n\n" }, - version_hash: "cx7alu" + version_hash: "1eogxsl" }; export async function get_hooks() { diff --git a/frontend/src/components/TaskRunner.svelte b/frontend/src/components/TaskRunner.svelte index 33d3ede..bc0b2ab 100755 --- a/frontend/src/components/TaskRunner.svelte +++ b/frontend/src/components/TaskRunner.svelte @@ -14,39 +14,107 @@ import { get } from 'svelte/store'; import { selectedTask, taskLogs } from '../lib/stores.js'; import { getWsUrl } from '../lib/api.js'; + import { addToast } from '../lib/toasts.js'; // [/SECTION] 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] - /** - * @purpose Initialize WebSocket connection for task logs. - */ onMount(() => { - const task = get(selectedTask); - if (task) { - console.log(`[TaskRunner][Entry] Connecting to logs for task: ${task.id}`); - taskLogs.set([]); // Clear previous logs - const wsUrl = getWsUrl(task.id); - ws = new WebSocket(wsUrl); - - ws.onopen = () => { - console.log('[TaskRunner][Coherence:OK] WebSocket connection established'); - }; - - ws.onmessage = (event) => { - const logEntry = JSON.parse(event.data); - 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'); - }; - } + // Subscribe to selectedTask changes + const unsubscribe = selectedTask.subscribe(task => { + if (task) { + console.log(`[TaskRunner][Action] Task selected: ${task.id}. Initializing connection.`); + if (ws) ws.close(); + clearTimeout(reconnectTimeout); + reconnectAttempts = 0; + connectionStatus = 'disconnected'; + taskLogs.set([]); + connect(); + } + }); + return unsubscribe; }); // [/DEF:onMount] @@ -55,6 +123,8 @@ * @purpose Close WebSocket connection when the component is destroyed. */ onDestroy(() => { + clearTimeout(reconnectTimeout); + clearTimeout(dataTimeout); if (ws) { console.log("[TaskRunner][Action] Closing WebSocket connection."); ws.close(); @@ -66,8 +136,29 @@
{#if $selectedTask} -

Task: {$selectedTask.plugin_id}

-
+
+

Task: {$selectedTask.plugin_id}

+
+ {#if connectionStatus === 'connecting'} + + + + + Connecting... + {:else if connectionStatus === 'connected'} + + Live + {:else if connectionStatus === 'completed'} + + Completed + {:else} + + Disconnected + {/if} +
+
+ +
{#each $taskLogs as log}
{new Date(log.timestamp).toLocaleTimeString()} @@ -75,6 +166,12 @@ {log.message}
{/each} + + {#if waitingForData} +
+ Waiting for data... +
+ {/if}
{:else}

No task selected.

diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index f383aef..98078ea 100755 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -14,7 +14,12 @@ const API_BASE_URL = '/api'; * @returns {string} */ 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}`; }; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 85d0db7..46ce1c5 100755 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -11,7 +11,8 @@ export default defineConfig({ }, '/ws': { target: 'ws://localhost:8000', - ws: true + ws: true, + changeOrigin: true } } } diff --git a/specs/001-plugin-arch-svelte-ui/tasks.md b/specs/001-plugin-arch-svelte-ui/tasks.md index e2f7fff..0876e9b 100755 --- a/specs/001-plugin-arch-svelte-ui/tasks.md +++ b/specs/001-plugin-arch-svelte-ui/tasks.md @@ -1,7 +1,7 @@ # Tasks: Plugin Architecture & Svelte Web UI **Feature**: `001-plugin-arch-svelte-ui` -**Status**: Planned + ## Dependencies diff --git a/specs/001-fix-ui-ws-validation/checklists/requirements.md b/specs/005-fix-ui-ws-validation/checklists/requirements.md similarity index 95% rename from specs/001-fix-ui-ws-validation/checklists/requirements.md rename to specs/005-fix-ui-ws-validation/checklists/requirements.md index 49471c4..8d06b6a 100644 --- a/specs/001-fix-ui-ws-validation/checklists/requirements.md +++ b/specs/005-fix-ui-ws-validation/checklists/requirements.md @@ -2,7 +2,7 @@ **Purpose**: Validate specification completeness and quality before proceeding to planning **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 diff --git a/specs/005-fix-ui-ws-validation/checklists/ws-connection.md b/specs/005-fix-ui-ws-validation/checklists/ws-connection.md new file mode 100644 index 0000000..01471f2 --- /dev/null +++ b/specs/005-fix-ui-ws-validation/checklists/ws-connection.md @@ -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] diff --git a/specs/001-fix-ui-ws-validation/data-model.md b/specs/005-fix-ui-ws-validation/data-model.md similarity index 100% rename from specs/001-fix-ui-ws-validation/data-model.md rename to specs/005-fix-ui-ws-validation/data-model.md diff --git a/specs/001-fix-ui-ws-validation/plan.md b/specs/005-fix-ui-ws-validation/plan.md similarity index 92% rename from specs/001-fix-ui-ws-validation/plan.md rename to specs/005-fix-ui-ws-validation/plan.md index 7d94f4d..9236455 100644 --- a/specs/001-fix-ui-ws-validation/plan.md +++ b/specs/005-fix-ui-ws-validation/plan.md @@ -1,8 +1,8 @@ # 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 @@ -36,7 +36,7 @@ This feature addresses three critical issues: unstyled UI due to missing Tailwin ### Documentation (this feature) ```text -specs/001-fix-ui-ws-validation/ +specs/005-fix-ui-ws-validation/ ├── plan.md # This file ├── research.md # Phase 0 output ├── data-model.md # Phase 1 output diff --git a/specs/001-fix-ui-ws-validation/quickstart.md b/specs/005-fix-ui-ws-validation/quickstart.md similarity index 100% rename from specs/001-fix-ui-ws-validation/quickstart.md rename to specs/005-fix-ui-ws-validation/quickstart.md diff --git a/specs/001-fix-ui-ws-validation/research.md b/specs/005-fix-ui-ws-validation/research.md similarity index 100% rename from specs/001-fix-ui-ws-validation/research.md rename to specs/005-fix-ui-ws-validation/research.md diff --git a/specs/001-fix-ui-ws-validation/spec.md b/specs/005-fix-ui-ws-validation/spec.md similarity index 87% rename from specs/001-fix-ui-ws-validation/spec.md rename to specs/005-fix-ui-ws-validation/spec.md index 1ac106c..4d56ec5 100644 --- a/specs/001-fix-ui-ws-validation/spec.md +++ b/specs/005-fix-ui-ws-validation/spec.md @@ -1,6 +1,6 @@ # 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 **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." @@ -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-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-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. ### 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 ### 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: Global Styling Implementation → A: Import in `src/routes/+layout.svelte` (Option A). - Q: WebSocket Port Configuration → A: Use environment variable with fallback (Option A). diff --git a/specs/001-fix-ui-ws-validation/tasks.md b/specs/005-fix-ui-ws-validation/tasks.md similarity index 85% rename from specs/001-fix-ui-ws-validation/tasks.md rename to specs/005-fix-ui-ws-validation/tasks.md index 466c902..1dda19e 100644 --- a/specs/001-fix-ui-ws-validation/tasks.md +++ b/specs/005-fix-ui-ws-validation/tasks.md @@ -1,6 +1,6 @@ # 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/ **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 - [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 ---