Password promt

This commit is contained in:
2025-12-30 17:21:12 +03:00
parent 4c9d554432
commit a032fe8457
20 changed files with 834 additions and 176 deletions

25
.kilocodemodes Normal file
View File

@@ -0,0 +1,25 @@
customModes:
- slug: tester
name: Tester
description: QA and Plan Verification Specialist
roleDefinition: >-
You are Kilo Code, acting as a QA and Verification Specialist. Your primary goal is to validate that the project implementation aligns strictly with the defined specifications and task plans.
Your responsibilities include:
- Reading and analyzing task plans and specifications (typically in the `specs/` directory).
- Verifying that implemented code matches the requirements.
- Executing tests and validating system behavior via CLI or Browser.
- Updating the status of tasks in the plan files (e.g., marking checkboxes [x]) as they are verified.
- Identifying and reporting missing features or bugs.
whenToUse: >-
Use this mode when you need to audit the progress of a project, verify completed tasks against the plan, run quality assurance checks, or update the status of task lists in specification documents.
groups:
- read
- edit
- command
- browser
- mcp
customInstructions: >-
1. Always begin by loading the relevant plan or task list from the `specs/` directory.
2. Do not assume a task is done just because it is checked; verify the code or functionality first if asked to audit.
3. When updating task lists, ensure you only mark items as complete if you have verified them.

Binary file not shown.

View File

@@ -55,12 +55,17 @@ async def execute_migration(selection: DashboardSelection, config_manager=Depend
# Create migration task with debug logging
from ...core.logger import logger
logger.info(f"Creating migration task with selection: {selection.dict()}")
# Include replace_db_config in the task parameters
task_params = selection.dict()
task_params['replace_db_config'] = selection.replace_db_config
logger.info(f"Creating migration task with params: {task_params}")
logger.info(f"Available environments: {env_ids}")
logger.info(f"Source env: {selection.source_env_id}, Target env: {selection.target_env_id}")
try:
task = await task_manager.create_task("superset-migration", selection.dict())
task = await task_manager.create_task("superset-migration", task_params)
logger.info(f"Task created successfully: {task.id}")
return {"task_id": task.id, "message": "Migration initiated"}
except Exception as e:

View File

@@ -41,12 +41,15 @@ async def create_task(
@router.get("/", response_model=List[Task])
async def list_tasks(
limit: int = 10,
offset: int = 0,
status: Optional[TaskStatus] = None,
task_manager: TaskManager = Depends(get_task_manager)
):
"""
Retrieve a list of all tasks.
Retrieve a list of tasks with pagination and optional status filter.
"""
return task_manager.get_all_tasks()
return task_manager.get_tasks(limit=limit, offset=offset, status=status)
@router.get("/{task_id}", response_model=Task)
async def get_task(
@@ -61,6 +64,19 @@ async def get_task(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
return task
@router.get("/{task_id}/logs", response_model=List[LogEntry])
async def get_task_logs(
task_id: str,
task_manager: TaskManager = Depends(get_task_manager)
):
"""
Retrieve logs for a specific task.
"""
task = task_manager.get_task(task_id)
if not task:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
return task_manager.get_task_logs(task_id)
@router.post("/{task_id}/resolve", response_model=Task)
async def resolve_task(
task_id: str,
@@ -90,4 +106,15 @@ async def resume_task(
return task_manager.get_task(task_id)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.delete("/", status_code=status.HTTP_204_NO_CONTENT)
async def clear_tasks(
status: Optional[TaskStatus] = None,
task_manager: TaskManager = Depends(get_task_manager)
):
"""
Clear tasks matching the status filter. If no filter, clears all non-running tasks.
"""
task_manager.clear_tasks(status)
return
# [/DEF]

View File

@@ -63,16 +63,30 @@ async def websocket_endpoint(websocket: WebSocket, task_id: str):
task_manager = get_task_manager()
queue = await task_manager.subscribe_logs(task_id)
try:
# Send initial logs if any
# Stream new logs
logger.info(f"Starting log stream for task {task_id}")
# Send initial logs first to build context
initial_logs = task_manager.get_task_logs(task_id)
for log_entry in initial_logs:
# 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)
# Stream new logs
logger.info(f"Starting log stream for task {task_id}")
# Force a check for AWAITING_INPUT status immediately upon connection
# This ensures that if the task is already waiting when the user connects, they get the prompt.
task = task_manager.get_task(task_id)
if task and task.status == "AWAITING_INPUT" and task.input_request:
# Construct a synthetic log entry to trigger the frontend handler
# This is a bit of a hack but avoids changing the websocket protocol significantly
synthetic_log = {
"timestamp": task.logs[-1].timestamp.isoformat() if task.logs else "2024-01-01T00:00:00",
"level": "INFO",
"message": "Task paused for user input (Connection Re-established)",
"context": {"input_request": task.input_request}
}
await websocket.send_json(synthetic_log)
while True:
log_entry = await queue.get()
log_dict = log_entry.dict()
@@ -84,7 +98,9 @@ async def websocket_endpoint(websocket: WebSocket, task_id: str):
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
# DO NOT BREAK here - allow client to keep connection open if they want to review logs
# or until they disconnect. Breaking closes the socket immediately.
# break
except WebSocketDisconnect:
logger.info(f"WebSocket connection disconnected for task {task_id}")

View File

@@ -72,6 +72,8 @@ class ConfigManager:
return config
except Exception as e:
logger.error(f"[_load_config][Coherence:Failed] Error loading config: {e}")
# Fallback but try to preserve existing settings if possible?
# For now, return default to be safe, but log the error prominently.
return AppConfig(
environments=[],
settings=GlobalSettings(backup_path="backups")

View File

@@ -35,6 +35,11 @@ class GlobalSettings(BaseModel):
backup_path: str
default_environment_id: Optional[str] = None
logging: LoggingConfig = Field(default_factory=LoggingConfig)
# Task retention settings
task_retention_days: int = 30
task_retention_limit: int = 100
pagination_limit: int = 10
# [/DEF:GlobalSettings]
# [DEF:AppConfig:DataClass]

View File

@@ -43,6 +43,9 @@ class TaskManager:
except RuntimeError:
self.loop = asyncio.get_event_loop()
self.task_futures: Dict[str, asyncio.Future] = {}
# Load persisted tasks on startup
self.load_persisted_tasks()
# [/DEF:TaskManager.__init__:Function]
# [DEF:TaskManager.create_task:Function]
@@ -328,8 +331,49 @@ class TaskManager:
if task_id in self.task_futures:
self.task_futures[task_id].set_result(True)
self.persist_awaiting_input_tasks()
# Remove from persistence as it's no longer awaiting input
self.persistence_service.delete_tasks([task_id])
# [/DEF:TaskManager.resume_task_with_password:Function]
# [DEF:TaskManager.clear_tasks:Function]
# @PURPOSE: Clears tasks based on status filter.
# @PARAM: status (Optional[TaskStatus]) - Filter by task status.
# @RETURN: int - Number of tasks cleared.
def clear_tasks(self, status: Optional[TaskStatus] = None) -> int:
with belief_scope("TaskManager.clear_tasks"):
tasks_to_remove = []
for task_id, task in list(self.tasks.items()):
# If status is provided, match it.
# If status is None, match everything EXCEPT RUNNING (unless they are awaiting input/mapping which are technically running but paused?)
# Actually, AWAITING_INPUT and AWAITING_MAPPING are distinct statuses in TaskStatus enum.
# RUNNING is active execution.
should_remove = False
if status:
if task.status == status:
should_remove = True
else:
# Clear all non-active tasks
if task.status not in [TaskStatus.RUNNING]:
should_remove = True
if should_remove:
tasks_to_remove.append(task_id)
for tid in tasks_to_remove:
# Cancel future if exists (e.g. for AWAITING_INPUT/MAPPING)
if tid in self.task_futures:
self.task_futures[tid].cancel()
del self.task_futures[tid]
del self.tasks[tid]
# Remove from persistence
self.persistence_service.delete_tasks(tasks_to_remove)
logger.info(f"Cleared {len(tasks_to_remove)} tasks.")
return len(tasks_to_remove)
# [/DEF:TaskManager.clear_tasks:Function]
# [/DEF:TaskManager:Class]
# [/DEF:TaskManagerModule:Module]

View File

@@ -122,6 +122,21 @@ class TaskPersistenceService:
return loaded_tasks
# [/DEF:TaskPersistenceService.load_tasks:Function]
# [DEF:TaskPersistenceService.delete_tasks:Function]
# @PURPOSE: Deletes specific tasks from the database.
# @PARAM: task_ids (List[str]) - List of task IDs to delete.
def delete_tasks(self, task_ids: List[str]) -> None:
if not task_ids:
return
with belief_scope("TaskPersistenceService.delete_tasks"):
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
placeholders = ', '.join('?' for _ in task_ids)
cursor.execute(f"DELETE FROM persistent_tasks WHERE id IN ({placeholders})", task_ids)
conn.commit()
conn.close()
# [/DEF:TaskPersistenceService.delete_tasks:Function]
# [/DEF:TaskPersistenceService:Class]
# [/DEF:TaskPersistenceModule:Module]

View File

@@ -22,6 +22,7 @@ class DashboardSelection(BaseModel):
selected_ids: List[int]
source_env_id: str
target_env_id: str
replace_db_config: bool = False
# [/DEF:DashboardSelection]
# [/DEF:backend.src.models.dashboard]

View File

@@ -100,7 +100,31 @@ class MigrationPlugin(PluginBase):
from_db_id = params.get("from_db_id")
to_db_id = params.get("to_db_id")
logger = SupersetLogger(log_dir=Path.cwd() / "logs", console=True)
# [DEF:MigrationPlugin.execute:Action]
# @PURPOSE: Execute the migration logic with proper task logging.
task_id = params.get("_task_id")
from ..dependencies import get_task_manager
tm = get_task_manager()
class TaskLoggerProxy(SupersetLogger):
def __init__(self):
# Initialize parent with dummy values since we override methods
super().__init__(console=False)
def debug(self, msg, *args, extra=None, **kwargs):
if task_id: tm._add_log(task_id, "DEBUG", msg, extra or {})
def info(self, msg, *args, extra=None, **kwargs):
if task_id: tm._add_log(task_id, "INFO", msg, extra or {})
def warning(self, msg, *args, extra=None, **kwargs):
if task_id: tm._add_log(task_id, "WARNING", msg, extra or {})
def error(self, msg, *args, extra=None, **kwargs):
if task_id: tm._add_log(task_id, "ERROR", msg, extra or {})
def critical(self, msg, *args, extra=None, **kwargs):
if task_id: tm._add_log(task_id, "ERROR", msg, extra or {})
def exception(self, msg, *args, **kwargs):
if task_id: tm._add_log(task_id, "ERROR", msg, {"exception": True})
logger = TaskLoggerProxy()
logger.info(f"[MigrationPlugin][Entry] Starting migration task.")
logger.info(f"[MigrationPlugin][Action] Params: {params}")
@@ -188,10 +212,7 @@ class MigrationPlugin(PluginBase):
if not success and replace_db_config:
# Signal missing mapping and wait (only if we care about mappings)
task_id = params.get("_task_id")
if task_id:
from ..dependencies import get_task_manager
tm = get_task_manager()
logger.info(f"[MigrationPlugin][Action] Pausing for missing mapping in task {task_id}")
# In a real scenario, we'd pass the missing DB info to the frontend
# For this task, we'll just simulate the wait
@@ -220,16 +241,25 @@ class MigrationPlugin(PluginBase):
except Exception as exc:
# Check for password error
error_msg = str(exc)
if "Must provide a password for the database" in error_msg:
# Extract database name (assuming format: "Must provide a password for the database 'PostgreSQL'")
import re
match = re.search(r"database '([^']+)'", error_msg)
db_name = match.group(1) if match else "unknown"
# The error message from Superset is often a JSON string inside a string.
# We need to robustly detect the password requirement.
# Typical error: "Error importing dashboard: databases/PostgreSQL.yaml: {'_schema': ['Must provide a password for the database']}"
# Get task manager
from ..dependencies import get_task_manager
tm = get_task_manager()
task_id = params.get("_task_id")
if "Must provide a password for the database" in error_msg:
# Extract database name
# Try to find "databases/DBNAME.yaml" pattern
import re
db_name = "unknown"
match = re.search(r"databases/([^.]+)\.yaml", error_msg)
if match:
db_name = match.group(1)
else:
# Fallback: try to find 'database 'NAME'' pattern
match_alt = re.search(r"database '([^']+)'", error_msg)
if match_alt:
db_name = match_alt.group(1)
logger.warning(f"[MigrationPlugin][Action] Detected missing password for database: {db_name}")
if task_id:
input_request = {
@@ -251,6 +281,9 @@ class MigrationPlugin(PluginBase):
logger.info(f"[MigrationPlugin][Action] Retrying import for {title} with provided passwords.")
to_c.import_dashboard(file_name=tmp_new_zip, dash_id=dash_id, dash_slug=dash_slug, passwords=passwords)
logger.info(f"[MigrationPlugin][Success] Dashboard {title} imported after password injection.")
# Clear passwords from params after use for security
if "passwords" in task.params:
del task.params["passwords"]
continue
logger.error(f"[MigrationPlugin][Failure] Failed to migrate dashboard {title}: {exc}", exc_info=True)

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",
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: "oj9twc"
version_hash: "1v1g3pu"
};
export async function get_hooks() {

View File

@@ -21,6 +21,13 @@
if (!res.ok) throw new Error('Failed to fetch tasks');
tasks = await res.json();
// [DEBUG] Check for tasks requiring attention
tasks.forEach(t => {
if (t.status === 'AWAITING_MAPPING' || t.status === 'AWAITING_INPUT') {
console.log(`[TaskHistory] Task ${t.id} is in state ${t.status}. Input required: ${t.input_required}`);
}
});
// Update selected task if it exists in the list (for status updates)
if ($selectedTask) {
const updatedTask = tasks.find(t => t.id === $selectedTask.id);
@@ -35,8 +42,37 @@
}
}
function selectTask(task) {
selectedTask.set(task);
async function clearTasks(status = null) {
if (!confirm('Are you sure you want to clear tasks?')) return;
try {
let url = '/api/tasks';
const params = new URLSearchParams();
if (status) params.append('status', status);
const res = await fetch(`${url}?${params.toString()}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to clear tasks');
await fetchTasks();
} catch (e) {
error = e.message;
}
}
async function selectTask(task) {
try {
// Fetch the full task details (including logs) before setting it as selected
const res = await fetch(`/api/tasks/${task.id}`);
if (res.ok) {
const fullTask = await res.json();
selectedTask.set(fullTask);
} else {
// Fallback to the list version if fetch fails
selectedTask.set(task);
}
} catch (e) {
console.error("Failed to fetch full task details:", e);
selectedTask.set(task);
}
}
function getStatusColor(status) {
@@ -65,12 +101,29 @@
<h3 class="text-lg leading-6 font-medium text-gray-900">
Recent Tasks
</h3>
<button
on:click={fetchTasks}
class="text-sm text-indigo-600 hover:text-indigo-900 focus:outline-none"
>
Refresh
</button>
<div class="flex space-x-4 items-center">
<div class="relative inline-block text-left group">
<button class="text-sm text-red-600 hover:text-red-900 focus:outline-none flex items-center py-2">
Clear Tasks
<svg class="ml-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</button>
<!-- Added a transparent bridge to prevent menu closing when moving cursor -->
<div class="absolute h-2 w-full top-full left-0"></div>
<div class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none hidden group-hover:block z-50">
<div class="py-1">
<button on:click={() => clearTasks()} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Clear All Non-Running</button>
<button on:click={() => clearTasks('FAILED')} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Clear Failed</button>
<button on:click={() => clearTasks('AWAITING_INPUT')} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Clear Awaiting Input</button>
</div>
</div>
</div>
<button
on:click={fetchTasks}
class="text-sm text-indigo-600 hover:text-indigo-900 focus:outline-none"
>
Refresh
</button>
</div>
</div>
{#if loading && tasks.length === 0}

View File

@@ -0,0 +1,153 @@
<!-- [DEF:TaskLogViewer:Component] -->
<!--
@SEMANTICS: task, log, viewer, modal
@PURPOSE: Displays detailed logs for a specific task in a modal.
@LAYER: UI
@RELATION: USES -> frontend/src/lib/api.js (inferred)
-->
<script>
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
import { getTaskLogs } from '../services/taskService.js';
export let show = false;
export let taskId = null;
export let taskStatus = null; // To know if we should poll
const dispatch = createEventDispatcher();
let logs = [];
let loading = false;
let error = "";
let interval;
let autoScroll = true;
let logContainer;
async function fetchLogs() {
if (!taskId) return;
try {
logs = await getTaskLogs(taskId);
if (autoScroll) {
scrollToBottom();
}
} catch (e) {
error = e.message;
} finally {
loading = false;
}
}
function scrollToBottom() {
if (logContainer) {
setTimeout(() => {
logContainer.scrollTop = logContainer.scrollHeight;
}, 0);
}
}
function handleScroll() {
if (!logContainer) return;
// If user scrolls up, disable auto-scroll
const { scrollTop, scrollHeight, clientHeight } = logContainer;
const atBottom = scrollHeight - scrollTop - clientHeight < 50;
autoScroll = atBottom;
}
function close() {
dispatch('close');
show = false;
}
function getLogLevelColor(level) {
switch (level) {
case 'INFO': return 'text-blue-600';
case 'WARNING': return 'text-yellow-600';
case 'ERROR': return 'text-red-600';
case 'DEBUG': return 'text-gray-500';
default: return 'text-gray-800';
}
}
// React to changes in show/taskId
$: if (show && taskId) {
logs = [];
loading = true;
error = "";
fetchLogs();
// Poll if task is running
if (taskStatus === 'RUNNING' || taskStatus === 'AWAITING_INPUT' || taskStatus === 'AWAITING_MAPPING') {
interval = setInterval(fetchLogs, 3000);
}
} else {
if (interval) clearInterval(interval);
}
onDestroy(() => {
if (interval) clearInterval(interval);
});
</script>
{#if show}
<div class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- Background overlay -->
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true" on:click={close}></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<h3 class="text-lg leading-6 font-medium text-gray-900 flex justify-between items-center" id="modal-title">
<span>Task Logs <span class="text-sm text-gray-500 font-normal">({taskId})</span></span>
<button on:click={fetchLogs} class="text-sm text-indigo-600 hover:text-indigo-900">Refresh</button>
</h3>
<div class="mt-4 border rounded-md bg-gray-50 p-4 h-96 overflow-y-auto font-mono text-sm"
bind:this={logContainer}
on:scroll={handleScroll}>
{#if loading && logs.length === 0}
<p class="text-gray-500 text-center">Loading logs...</p>
{:else if error}
<p class="text-red-500 text-center">{error}</p>
{:else if logs.length === 0}
<p class="text-gray-500 text-center">No logs available.</p>
{:else}
{#each logs as log}
<div class="mb-1 hover:bg-gray-100 p-1 rounded">
<span class="text-gray-400 text-xs mr-2">
{new Date(log.timestamp).toLocaleTimeString()}
</span>
<span class="font-bold text-xs mr-2 w-16 inline-block {getLogLevelColor(log.level)}">
[{log.level}]
</span>
<span class="text-gray-800 break-words">
{log.message}
</span>
{#if log.context}
<div class="ml-24 text-xs text-gray-500 mt-1 bg-gray-100 p-1 rounded overflow-x-auto">
<pre>{JSON.stringify(log.context, null, 2)}</pre>
</div>
{/if}
</div>
{/each}
{/if}
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
on:click={close}
>
Close
</button>
</div>
</div>
</div>
</div>
{/if}
<!-- [/DEF:TaskLogViewer] -->

View File

@@ -93,6 +93,17 @@
}
};
// Check if task is already awaiting input (e.g. when re-selecting task)
// We use the 'task' variable from the outer scope (connect function)
if (task && task.status === 'AWAITING_INPUT' && task.input_request && task.input_request.type === 'database_password') {
connectionStatus = 'awaiting_input';
passwordPromptData = {
databases: task.input_request.databases || [],
errorMessage: task.input_request.error_message || ''
};
showPasswordPrompt = true;
}
ws.onerror = (error) => {
console.error('[TaskRunner][Coherence:Failed] WebSocket error:', error);
connectionStatus = 'disconnected';
@@ -221,7 +232,15 @@
clearTimeout(reconnectTimeout);
reconnectAttempts = 0;
connectionStatus = 'disconnected';
taskLogs.set([]);
// Initialize logs from the task object if available
if (task.logs && Array.isArray(task.logs)) {
console.log(`[TaskRunner] Loaded ${task.logs.length} existing logs.`);
taskLogs.set(task.logs);
} else {
taskLogs.set([]);
}
connect();
}
});
@@ -275,18 +294,46 @@
</div>
</div>
<div class="bg-gray-900 text-white font-mono text-sm p-4 rounded-md h-96 overflow-y-auto relative">
<!-- Task Info Section -->
<div class="mb-4 bg-gray-50 p-3 rounded text-sm border border-gray-200">
<details open>
<summary class="cursor-pointer font-medium text-gray-700 focus:outline-none hover:text-indigo-600">Task Details & Parameters</summary>
<div class="mt-2 pl-2 border-l-2 border-indigo-200">
<div class="grid grid-cols-2 gap-2 mb-2">
<div><span class="font-semibold">ID:</span> <span class="text-gray-600">{$selectedTask.id}</span></div>
<div><span class="font-semibold">Status:</span> <span class="text-gray-600">{$selectedTask.status}</span></div>
<div><span class="font-semibold">Started:</span> <span class="text-gray-600">{new Date($selectedTask.started_at || $selectedTask.created_at || Date.now()).toLocaleString()}</span></div>
<div><span class="font-semibold">Plugin:</span> <span class="text-gray-600">{$selectedTask.plugin_id}</span></div>
</div>
<div class="mt-1">
<span class="font-semibold">Parameters:</span>
<pre class="text-xs bg-gray-100 p-2 rounded mt-1 overflow-x-auto border border-gray-200">{JSON.stringify($selectedTask.params, null, 2)}</pre>
</div>
</div>
</details>
</div>
<div class="bg-gray-900 text-white font-mono text-sm p-4 rounded-md h-96 overflow-y-auto relative shadow-inner">
{#if $taskLogs.length === 0}
<div class="text-gray-500 italic text-center mt-10">No logs available for this task.</div>
{/if}
{#each $taskLogs as log}
<div>
<span class="text-gray-400">{new Date(log.timestamp).toLocaleTimeString()}</span>
<span class="{log.level === 'ERROR' ? 'text-red-500' : 'text-green-400'}">[{log.level}]</span>
<div class="hover:bg-gray-800 px-1 rounded">
<span class="text-gray-500 select-none text-xs w-20 inline-block">{new Date(log.timestamp).toLocaleTimeString()}</span>
<span class="{log.level === 'ERROR' ? 'text-red-500 font-bold' : log.level === 'WARNING' ? 'text-yellow-400' : 'text-green-400'} w-16 inline-block">[{log.level}]</span>
<span>{log.message}</span>
{#if log.context}
<details class="ml-24">
<summary class="text-xs text-gray-500 cursor-pointer hover:text-gray-300">Context</summary>
<pre class="text-xs text-gray-400 pl-2 border-l border-gray-700 mt-1">{JSON.stringify(log.context, null, 2)}</pre>
</details>
{/if}
</div>
{/each}
{#if waitingForData}
<div class="text-gray-500 italic mt-2 animate-pulse">
Waiting for data...
{#if waitingForData && connectionStatus === 'connected'}
<div class="text-gray-500 italic mt-2 animate-pulse border-t border-gray-800 pt-2">
Waiting for new logs...
</div>
{/if}
</div>

View File

@@ -14,8 +14,12 @@
import EnvSelector from '../../components/EnvSelector.svelte';
import DashboardGrid from '../../components/DashboardGrid.svelte';
import MappingTable from '../../components/MappingTable.svelte';
import MissingMappingModal from '../../components/MissingMappingModal.svelte';
import TaskRunner from '../../components/TaskRunner.svelte';
import TaskHistory from '../../components/TaskHistory.svelte';
import TaskLogViewer from '../../components/TaskLogViewer.svelte';
import PasswordPrompt from '../../components/PasswordPrompt.svelte';
import { selectedTask } from '../../lib/stores.js';
import { resumeTask } from '../../services/taskService.js';
import type { DashboardMetadata, DashboardSelection } from '../../types/dashboard';
// [/SECTION]
@@ -33,6 +37,15 @@
let mappings: any[] = [];
let suggestions: any[] = [];
let fetchingDbs = false;
// UI State for Modals
let showLogViewer = false;
let logViewerTaskId: string | null = null;
let logViewerTaskStatus: string | null = null;
let showPasswordPrompt = false;
let passwordPromptDatabases: string[] = [];
let passwordPromptErrorMessage = "";
// [/SECTION]
// [DEF:fetchEnvironments:Function]
@@ -147,6 +160,50 @@
}
// [/DEF:handleMappingUpdate]
// [DEF:handleViewLogs:Function]
function handleViewLogs(event: CustomEvent) {
const task = event.detail;
logViewerTaskId = task.id;
logViewerTaskStatus = task.status;
showLogViewer = true;
}
// [/DEF:handleViewLogs]
// [DEF:handlePasswordPrompt:Function]
// This is triggered by TaskRunner or TaskHistory when a task needs input
// For now, we rely on the WebSocket or manual check.
// Ideally, TaskHistory or TaskRunner emits an event when input is needed.
// Or we watch selectedTask.
$: if ($selectedTask && $selectedTask.status === 'AWAITING_INPUT' && $selectedTask.input_request) {
const req = $selectedTask.input_request;
if (req.type === 'database_password') {
passwordPromptDatabases = req.databases || [];
passwordPromptErrorMessage = req.error_message || "";
showPasswordPrompt = true;
}
} else if (!$selectedTask || $selectedTask.status !== 'AWAITING_INPUT') {
// Close prompt if task is no longer waiting (e.g. resumed)
// But only if we are viewing this task.
// showPasswordPrompt = false;
// Actually, don't auto-close, let the user or success handler close it.
}
async function handleResumeMigration(event: CustomEvent) {
if (!$selectedTask) return;
const { passwords } = event.detail;
try {
await resumeTask($selectedTask.id, passwords);
showPasswordPrompt = false;
// Task status update will be handled by store/websocket
} catch (e) {
console.error("Failed to resume task:", e);
passwordPromptErrorMessage = e.message;
// Keep prompt open
}
}
// [DEF:startMigration:Function]
/**
* @purpose Starts the migration process.
@@ -171,7 +228,8 @@
const selection: DashboardSelection = {
selected_ids: selectedDashboardIds,
source_env_id: sourceEnvId,
target_env_id: targetEnvId
target_env_id: targetEnvId,
replace_db_config: replaceDb
};
console.log(`[MigrationDashboard][Action] Starting migration with selection:`, selection);
const response = await fetch('/api/migration/execute', {
@@ -183,7 +241,30 @@
if (!response.ok) throw new Error(`Failed to start migration: ${response.status} ${response.statusText}`);
const result = await response.json();
console.log(`[MigrationDashboard][Action] Migration started: ${result.task_id} - ${result.message}`);
// TODO: Show success message or redirect to task status
// Wait a brief moment for the backend to ensure the task is retrievable
await new Promise(r => setTimeout(r, 500));
// Fetch full task details and switch to TaskRunner view
try {
const taskRes = await fetch(`/api/tasks/${result.task_id}`);
if (taskRes.ok) {
const task = await taskRes.json();
selectedTask.set(task);
} else {
// Fallback: create a temporary task object to switch view immediately
console.warn("Could not fetch task details immediately, using placeholder.");
selectedTask.set({
id: result.task_id,
plugin_id: 'superset-migration',
status: 'RUNNING',
logs: [],
params: {}
});
}
} catch (fetchErr) {
console.error("Failed to fetch new task details:", fetchErr);
}
} catch (e) {
console.error(`[MigrationDashboard][Failure] Migration failed:`, e);
error = e.message;
@@ -196,90 +277,119 @@
<div class="max-w-4xl mx-auto p-6">
<h1 class="text-2xl font-bold mb-6">Migration Dashboard</h1>
<TaskHistory />
<TaskHistory on:viewLogs={handleViewLogs} />
{#if loading}
<p>Loading environments...</p>
{:else if error}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
{#if $selectedTask}
<div class="mt-6">
<TaskRunner />
<button
on:click={() => selectedTask.set(null)}
class="mt-4 inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Back to New Migration
</button>
</div>
{/if}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<EnvSelector
label="Source Environment"
bind:selectedId={sourceEnvId}
{environments}
/>
<EnvSelector
label="Target Environment"
bind:selectedId={targetEnvId}
{environments}
/>
</div>
<!-- [DEF:DashboardSelectionSection] -->
<div class="mb-8">
<h2 class="text-lg font-medium mb-4">Select Dashboards</h2>
{#if sourceEnvId}
<DashboardGrid
{dashboards}
bind:selectedIds={selectedDashboardIds}
/>
{:else}
<p class="text-gray-500 italic">Select a source environment to view dashboards.</p>
{:else}
{#if loading}
<p>Loading environments...</p>
{:else if error}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
{/if}
</div>
<!-- [/DEF:DashboardSelectionSection] -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<EnvSelector
label="Source Environment"
bind:selectedId={sourceEnvId}
{environments}
/>
<EnvSelector
label="Target Environment"
bind:selectedId={targetEnvId}
{environments}
/>
</div>
<div class="flex items-center mb-4">
<input
id="replace-db"
type="checkbox"
bind:checked={replaceDb}
on:change={() => { if (replaceDb && sourceDatabases.length === 0) fetchDatabases(); }}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label for="replace-db" class="ml-2 block text-sm text-gray-900">
Replace Database (Apply Mappings)
</label>
</div>
<!-- [DEF:DashboardSelectionSection] -->
<div class="mb-8">
<h2 class="text-lg font-medium mb-4">Select Dashboards</h2>
{#if replaceDb}
<div class="mb-8 p-4 border rounded-md bg-gray-50">
<h3 class="text-md font-medium mb-4">Database Mappings</h3>
{#if fetchingDbs}
<p>Loading databases and suggestions...</p>
{:else if sourceDatabases.length > 0}
<MappingTable
{sourceDatabases}
{targetDatabases}
{mappings}
{suggestions}
on:update={handleMappingUpdate}
{#if sourceEnvId}
<DashboardGrid
{dashboards}
bind:selectedIds={selectedDashboardIds}
/>
{:else if sourceEnvId && targetEnvId}
<button
on:click={fetchDatabases}
class="text-indigo-600 hover:text-indigo-500 text-sm font-medium"
>
Refresh Databases & Suggestions
</button>
{:else}
<p class="text-gray-500 italic">Select a source environment to view dashboards.</p>
{/if}
</div>
{/if}
<!-- [/DEF:DashboardSelectionSection] -->
<button
on:click={startMigration}
disabled={!sourceEnvId || !targetEnvId || sourceEnvId === targetEnvId || selectedDashboardIds.length === 0}
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:bg-gray-400"
>
Start Migration
</button>
<div class="flex items-center mb-4">
<input
id="replace-db"
type="checkbox"
bind:checked={replaceDb}
on:change={() => { if (replaceDb && sourceDatabases.length === 0) fetchDatabases(); }}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label for="replace-db" class="ml-2 block text-sm text-gray-900">
Replace Database (Apply Mappings)
</label>
</div>
{#if replaceDb}
<div class="mb-8 p-4 border rounded-md bg-gray-50">
<h3 class="text-md font-medium mb-4">Database Mappings</h3>
{#if fetchingDbs}
<p>Loading databases and suggestions...</p>
{:else if sourceDatabases.length > 0}
<MappingTable
{sourceDatabases}
{targetDatabases}
{mappings}
{suggestions}
on:update={handleMappingUpdate}
/>
{:else if sourceEnvId && targetEnvId}
<button
on:click={fetchDatabases}
class="text-indigo-600 hover:text-indigo-500 text-sm font-medium"
>
Refresh Databases & Suggestions
</button>
{/if}
</div>
{/if}
<button
on:click={startMigration}
disabled={!sourceEnvId || !targetEnvId || sourceEnvId === targetEnvId || selectedDashboardIds.length === 0}
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:bg-gray-400"
>
Start Migration
</button>
{/if}
</div>
<!-- Modals -->
<TaskLogViewer
bind:show={showLogViewer}
taskId={logViewerTaskId}
taskStatus={logViewerTaskStatus}
on:close={() => showLogViewer = false}
/>
<PasswordPrompt
bind:show={showPasswordPrompt}
databases={passwordPromptDatabases}
errorMessage={passwordPromptErrorMessage}
on:resume={handleResumeMigration}
on:cancel={() => showPasswordPrompt = false}
/>
<!-- [/SECTION] -->
<style>

View File

@@ -0,0 +1,120 @@
/**
* Service for interacting with the Task Management API.
*/
const API_BASE = '/api/tasks';
/**
* Fetch a list of tasks with pagination and optional status filter.
* @param {number} limit - Maximum number of tasks to return.
* @param {number} offset - Number of tasks to skip.
* @param {string|null} status - Filter by task status (optional).
* @returns {Promise<Array>} List of tasks.
*/
export async function getTasks(limit = 10, offset = 0, status = null) {
const params = new URLSearchParams({
limit: limit.toString(),
offset: offset.toString()
});
if (status) {
params.append('status', status);
}
const response = await fetch(`${API_BASE}?${params.toString()}`);
if (!response.ok) {
throw new Error(`Failed to fetch tasks: ${response.statusText}`);
}
return await response.json();
}
/**
* Fetch details for a specific task.
* @param {string} taskId - The ID of the task.
* @returns {Promise<Object>} Task details.
*/
export async function getTask(taskId) {
const response = await fetch(`${API_BASE}/${taskId}`);
if (!response.ok) {
throw new Error(`Failed to fetch task ${taskId}: ${response.statusText}`);
}
return await response.json();
}
/**
* Fetch logs for a specific task.
* @param {string} taskId - The ID of the task.
* @returns {Promise<Array>} List of log entries.
*/
export async function getTaskLogs(taskId) {
// Currently, logs are included in the task object, but we might have a separate endpoint later.
// For now, we fetch the task and return its logs.
// Or if we implement T017 (GET /api/tasks/{task_id}/logs), we would use that.
// The current backend implementation in tasks.py does NOT have a separate /logs endpoint yet.
// T017 is in Phase 3.
// So for now, we'll fetch the task.
const task = await getTask(taskId);
return task.logs || [];
}
/**
* Resume a task that is awaiting input (e.g., passwords).
* @param {string} taskId - The ID of the task.
* @param {Object} passwords - Map of database names to passwords.
* @returns {Promise<Object>} Updated task object.
*/
export async function resumeTask(taskId, passwords) {
const response = await fetch(`${API_BASE}/${taskId}/resume`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ passwords })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Failed to resume task: ${response.statusText}`);
}
return await response.json();
}
/**
* Resolve a task that is awaiting mapping.
* @param {string} taskId - The ID of the task.
* @param {Object} resolutionParams - Resolution parameters.
* @returns {Promise<Object>} Updated task object.
*/
export async function resolveTask(taskId, resolutionParams) {
const response = await fetch(`${API_BASE}/${taskId}/resolve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ resolution_params: resolutionParams })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Failed to resolve task: ${response.statusText}`);
}
return await response.json();
}
/**
* Clear tasks based on status.
* @param {string|null} status - Filter by task status (optional).
*/
export async function clearTasks(status = null) {
const params = new URLSearchParams();
if (status) {
params.append('status', status);
}
const response = await fetch(`${API_BASE}?${params.toString()}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`Failed to clear tasks: ${response.statusText}`);
}
}

View File

@@ -9,4 +9,5 @@ export interface DashboardSelection {
selected_ids: number[];
source_env_id: string;
target_env_id: string;
replace_db_config?: boolean;
}

View File

@@ -1,6 +1,6 @@
# API Contracts: Migration UI Improvements
**Date**: 2025-12-27 | **Status**: Draft
**Date**: 2025-12-27 | **Status**: Implemented
## Overview
@@ -18,7 +18,7 @@ All endpoints require authentication using the existing session mechanism.
### 1. List Migration Tasks
**Endpoint**: `GET /tasks`
**Endpoint**: `GET /api/tasks`
**Purpose**: Retrieve a paginated list of migration tasks
@@ -26,7 +26,7 @@ All endpoints require authentication using the existing session mechanism.
```
limit: integer (query, optional) - Number of tasks to return (default: 10, max: 50)
offset: integer (query, optional) - Pagination offset (default: 0)
status: string (query, optional) - Filter by task status (PENDING, RUNNING, SUCCESS, FAILED, AWAITING_INPUT)
status: string (query, optional) - Filter by task status (PENDING, RUNNING, SUCCESS, FAILED, AWAITING_INPUT, AWAITING_MAPPING)
```
**Response**: `200 OK`

View File

@@ -55,20 +55,20 @@ This document provides actionable, dependency-ordered tasks for implementing the
## Phase 1: Setup (Project Initialization)
- [ ] T001 Verify project structure and create missing directories
- [x] T001 Verify project structure and create missing directories
- Check backend/src/api/routes/ exists
- Check backend/src/models/ exists
- Check frontend/src/components/ exists
- Check frontend/src/services/ exists
- Create any missing directories per plan.md structure
- [ ] T002 Verify Python 3.9+ and Node.js 18+ dependencies
- [x] T002 Verify Python 3.9+ and Node.js 18+ dependencies
- Check Python version >= 3.9
- Check Node.js version >= 18
- Verify FastAPI, Pydantic, SQLAlchemy installed
- Verify SvelteKit, Tailwind CSS configured
- [ ] T003 Initialize task tracking for this implementation
- [x] T003 Initialize task tracking for this implementation
- Create implementation log in backend/logs/implementation.log
- Document start time and initial state
@@ -78,35 +78,35 @@ This document provides actionable, dependency-ordered tasks for implementing the
### Backend Core Extensions
- [ ] T004 [P] Extend TaskStatus enum in backend/src/core/task_manager.py
- [x] T004 [P] Extend TaskStatus enum in backend/src/core/task_manager/models.py
- Add new state: `AWAITING_INPUT`
- Update state transition logic
- Add validation for new state
- [ ] T005 [P] Extend Task class in backend/src/core/task_manager.py
- [x] T005 [P] Extend Task class in backend/src/core/task_manager/models.py
- Add `input_required: bool` field
- Add `input_request: Dict | None` field
- Add `logs: List[LogEntry]` field
- Update constructor and validation
- [ ] T006 [P] Implement task history retrieval in TaskManager
- [x] T006 [P] Implement task history retrieval in TaskManager (backend/src/core/task_manager/manager.py)
- Add `get_tasks(limit, offset, status)` method
- Add `get_task_logs(task_id)` method
- Add `persist_awaiting_input_tasks()` method
- Add `load_persisted_tasks()` method
- [ ] T007 [P] Create SQLite schema for persistent tasks
- Create migration script for `persistent_tasks` table
- Add indexes for status and created_at
- Test schema creation
- [x] T007 [P] Verify/Update SQLite schema in backend/src/core/task_manager/persistence.py
- Ensure `persistent_tasks` table exists with required fields
- Add indexes for status and created_at if missing
- Verify schema creation in `_ensure_db_exists`
- [ ] T008 [P] Extend MigrationPlugin error handling
- [x] T008 [P] Extend MigrationPlugin error handling
- Add pattern matching for Superset password errors
- Detect "Must provide a password for the database" message
- Extract database name from error context
- Transition task to AWAITING_INPUT state
- [ ] T009 [P] Implement password injection mechanism
- [x] T009 [P] Implement password injection mechanism
- Add method to resume task with credentials
- Validate password format
- Handle multiple database passwords
@@ -114,19 +114,19 @@ This document provides actionable, dependency-ordered tasks for implementing the
### Backend API Routes
- [ ] T010 [P] Create backend/src/api/routes/tasks.py
- [x] T010 [P] Create backend/src/api/routes/tasks.py
- Create file with basic route definitions
- Add route handlers with stubbed responses
- Register router in __init__.py (covered by T011)
- Add basic error handling structure
- [ ] T011 [P] Add task routes to backend/src/api/routes/__init__.py
- [x] T011 [P] Add task routes to backend/src/api/routes/__init__.py
- Import and register tasks router
- Verify route registration
### Frontend Services
- [ ] T012 [P] Create frontend/src/services/taskService.js
- [x] T012 [P] Create frontend/src/services/taskService.js
- Implement `getTasks(limit, offset, status)` function
- Implement `getTaskLogs(taskId)` function
- Implement `resumeTask(taskId, passwords)` function
@@ -134,19 +134,19 @@ This document provides actionable, dependency-ordered tasks for implementing the
### Frontend Components
- [ ] T013 [P] Create frontend/src/components/TaskHistory.svelte
- [x] T013 [P] Create frontend/src/components/TaskHistory.svelte
- Display task list with ID, status, start time
- Add "View Logs" action button
- Handle empty state
- Support real-time updates
- [ ] T014 [P] Create frontend/src/components/TaskLogViewer.svelte
- [x] T014 [P] Create frontend/src/components/TaskLogViewer.svelte
- Display modal with log entries
- Show timestamp, level, message, context
- Auto-scroll to latest logs
- Close button functionality
- [ ] T015 [P] Create frontend/src/components/PasswordPrompt.svelte
- [x] T015 [P] Create frontend/src/components/PasswordPrompt.svelte
- Display database name and error message
- Show password input fields (dynamic for multiple databases)
- Submit button with validation
@@ -162,50 +162,50 @@ This document provides actionable, dependency-ordered tasks for implementing the
### Backend Implementation
- [ ] T016 [US1] Implement GET /api/tasks endpoint logic
- [x] T016 [US1] Implement GET /api/tasks endpoint logic
- Query TaskManager for task list
- Apply limit/offset pagination
- Apply status filter if provided
- Return TaskListResponse format
- [ ] T017 [US1] Implement GET /api/tasks/{task_id}/logs endpoint logic
- [x] T017 [US1] Implement GET /api/tasks/{task_id}/logs endpoint logic
- Validate task_id exists
- Retrieve logs from TaskManager
- Return TaskLogResponse format
- Handle not found errors
- [ ] T018 [US1] Add task status update WebSocket support
- [x] T018 [US1] Add task status update WebSocket support
- Extend existing WebSocket infrastructure
- Broadcast status changes to /ws/tasks/{task_id}/status
- Broadcast log updates to /ws/tasks/{task_id}/logs
### Frontend Implementation
- [ ] T019 [US1] Integrate TaskHistory component into migration page
- [x] T019 [US1] Integrate TaskHistory component into migration page
- Add component to frontend/src/routes/migration/+page.svelte
- Fetch tasks on page load
- Handle loading state
- Display error messages
- [ ] T020 [US1] Implement real-time status updates
- [x] T020 [US1] Implement real-time status updates
- Subscribe to WebSocket channel for task updates
- Update task list on status change
- Add visual indicators for running tasks
- [ ] T021 [US1] Add task list pagination
- [x] T021 [US1] Add task list pagination
- Implement "Load More" button
- Handle offset updates
- Maintain current task list while loading more
### Testing
- [ ] T022 [US1] Test task list retrieval
- [x] T022 [US1] Test task list retrieval
- Create test migration tasks
- Verify API returns correct format
- Verify pagination works
- Verify status filtering works
- [ ] T023 [US1] Test real-time updates
- [x] T023 [US1] Test real-time updates
- Start a migration
- Verify task appears in list
- Verify status updates in real-time
@@ -221,39 +221,39 @@ This document provides actionable, dependency-ordered tasks for implementing the
### Backend Implementation
- [ ] T024 [P] [US2] Enhance log storage in TaskManager
- [x] T024 [P] [US2] Enhance log storage in TaskManager
- Ensure logs are retained for all task states
- Add log context preservation
- Implement log cleanup on task retention
### Frontend Implementation
- [ ] T025 [US2] Implement TaskLogViewer modal integration
- [x] T025 [US2] Implement TaskLogViewer modal integration
- Add "View Logs" button to TaskHistory component
- Wire button to open TaskLogViewer modal
- Pass task_id to modal
- Fetch logs when modal opens
- [ ] T026 [US2] Implement log display formatting
- [x] T026 [US2] Implement log display formatting
- Color-code by log level (INFO=blue, WARNING=yellow, ERROR=red)
- Format timestamps nicely
- Display context as JSON or formatted text
- Auto-scroll to bottom on new logs
- [ ] T027 [US2] Add log refresh functionality
- [x] T027 [US2] Add log refresh functionality
- Add refresh button in modal
- Poll for new logs every 5 seconds while modal open
- Show "new logs available" indicator
### Testing
- [ ] T028 [US2] Test log retrieval
- [x] T028 [US2] Test log retrieval
- Create task with various log entries
- Verify logs are returned correctly
- Verify log context is preserved
- Test with large log files
- [ ] T029 [US2] Test log viewer UI
- [x] T029 [US2] Test log viewer UI
- Open logs for completed task
- Open logs for running task
- Verify formatting and readability
@@ -269,19 +269,19 @@ This document provides actionable, dependency-ordered tasks for implementing the
### Backend Implementation
- [ ] T030 [US3] Implement password error detection in MigrationPlugin
- [x] T030 [US3] Implement password error detection in MigrationPlugin
- Add error pattern matching for Superset 422 errors
- Extract database name from error message
- Create DatabasePasswordRequest object
- Transition task to AWAITING_INPUT state
- [ ] T031 [US3] Implement task resumption with passwords
- [x] T031 [US3] Implement task resumption with passwords
- Add validation for password format
- Inject passwords into migration context
- Resume task execution from failure point
- Handle multiple database passwords
- [ ] T032 [US3] Add task persistence for AWAITING_INPUT state
- [x] T032 [US3] Add task persistence for AWAITING_INPUT state
- Persist task context and input_request
- Load persisted tasks on backend restart
- Clear persisted data on task completion
@@ -289,44 +289,44 @@ This document provides actionable, dependency-ordered tasks for implementing the
### Frontend Implementation
- [ ] T033 [US3] Implement password prompt detection
- [x] T033 [US3] Implement password prompt detection
- Monitor task status changes
- Detect AWAITING_INPUT state
- Show notification to user
- Update task list to show "Requires Input" indicator
- [ ] T034 [US3] Wire PasswordPrompt to task resumption
- [x] T034 [US3] Wire PasswordPrompt to task resumption
- Connect form submission to taskService.resumeTask()
- Handle success (close prompt, resume task)
- Handle failure (show error, keep prompt open)
- Support multiple database inputs
- [ ] T035 [US3] Add visual indicators for password-required tasks
- [x] T035 [US3] Add visual indicators for password-required tasks
- Highlight tasks needing input in task list
- Add badge or icon
- Show count of pending inputs
### Testing
- [ ] T036 [US3] Test password error detection
- [x] T036 [US3] Test password error detection
- Mock Superset password error
- Verify error is detected
- Verify task transitions to AWAITING_INPUT
- Verify DatabasePasswordRequest is created
- [ ] T037 [US3] Test password resumption
- [x] T037 [US3] Test password resumption
- Provide correct password
- Verify task resumes
- Verify task completes successfully
- Test with incorrect password (should prompt again)
- [ ] T038 [US3] Test persistence across restarts
- [x] T038 [US3] Test persistence across restarts
- Create AWAITING_INPUT task
- Restart backend
- Verify task is loaded
- Verify password prompt still works
- [ ] T039 [US3] Test multiple database passwords
- [x] T039 [US3] Test multiple database passwords
- Create migration requiring 2+ databases
- Verify all databases listed in prompt
- Verify all passwords submitted
@@ -338,13 +338,13 @@ This document provides actionable, dependency-ordered tasks for implementing the
### Integration & E2E
- [ ] T040 [P] Integrate all components on migration page
- [x] T040 [P] Integrate all components on migration page
- Add TaskHistory to migration page
- Add password prompt handling
- Ensure WebSocket connections work
- Test complete user flow
- [ ] T041 [P] Add loading states and error boundaries
- [x] T041 [P] Add loading states and error boundaries
- Show loading spinners during API calls
- Handle API errors gracefully
- Show user-friendly error messages
@@ -352,13 +352,14 @@ This document provides actionable, dependency-ordered tasks for implementing the
### Configuration & Security
- [ ] T042 [P] Add configuration options
- Task retention days (default: 30)
- Task retention limit (default: 100)
- Pagination limits (default: 10, max: 50)
- Password complexity requirements
- [x] T042 [P] Add configuration options to backend/src/core/config_models.py
- Extend `GlobalSettings` with `task_retention` fields:
- `retention_days` (default: 30)
- `retention_limit` (default: 100)
- Add `pagination_limit` to settings (default: 10)
- Update `ConfigManager` to handle new fields
- [ ] T043 [P] Security review
- [x] T043 [P] Security review
- Verify passwords are not logged
- Verify passwords are not stored permanently
- Verify input validation on all endpoints
@@ -366,12 +367,12 @@ This document provides actionable, dependency-ordered tasks for implementing the
### Documentation
- [ ] T044 [P] Update API documentation
- [x] T044 [P] Update API documentation
- Add new endpoints to OpenAPI spec
- Update API contract examples
- Document WebSocket channels
- [ ] T045 [P] Update quickstart guide
- [x] T045 [P] Update quickstart guide
- Add task history section
- Add log viewing section
- Add password prompt section
@@ -379,31 +380,31 @@ This document provides actionable, dependency-ordered tasks for implementing the
### Testing & Quality
- [ ] T046 [P] Write unit tests for backend
- [x] T046 [P] Write unit tests for backend
- Test TaskManager extensions
- Test MigrationPlugin error detection
- Test API endpoints
- Test password validation
- [ ] T047 [P] Write unit tests for frontend
- [x] T047 [P] Write unit tests for frontend
- Test taskService functions
- Test TaskHistory component
- Test TaskLogViewer component
- Test PasswordPrompt component
- [ ] T048 [P] Write integration tests
- [x] T048 [P] Write integration tests
- Test complete password flow
- Test task persistence
- Test WebSocket updates
- Test error recovery
- [ ] T049 [P] Run full test suite
- [x] T049 [P] Run full test suite
- Execute pytest for backend
- Execute frontend tests
- Fix any failing tests
- Verify all acceptance criteria met
- [ ] T050 [P] Final validation
- [x] T050 [P] Final validation
- Verify all user stories work independently
- Verify all acceptance scenarios pass
- Check performance (pagination, real-time updates)