Password promt
This commit is contained in:
25
.kilocodemodes
Normal file
25
.kilocodemodes
Normal 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.
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
@@ -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}")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
153
frontend/src/components/TaskLogViewer.svelte
Normal file
153
frontend/src/components/TaskLogViewer.svelte
Normal 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">​</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] -->
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
120
frontend/src/services/taskService.js
Normal file
120
frontend/src/services/taskService.js
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@ export interface DashboardSelection {
|
||||
selected_ids: number[];
|
||||
source_env_id: string;
|
||||
target_env_id: string;
|
||||
replace_db_config?: boolean;
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user