The app-scoped Tasks tab never sorted. It rendered straight from tasksManager.tasks and relied on loadTasks() having ordered it, so any path that appends after the load — a task arriving from the event bus, a retry, a queue merge — put that task wherever it happened to land rather than at the top. Sort where the list is rendered instead of trusting it from three callers away. Honest note on the reported symptom: a list_users task appearing mid-list could not be reproduced from the stored records — replaying the sort over all 96 task files puts the newest tool tasks first. What is demonstrably wrong is the missing sort above, and a second latent fault it would mask: 8 of those 96 records carry a null createdAt (cron-created backups), and `new Date(null)` is the epoch, so they sort as if from 1970 rather than as unknown. Adds window.taskSortTime for that: createdAt when it parses, otherwise the timestamp already embedded in the task id — the WebUI mints task_<epoch_ms>_<rand> and the backend task_<epoch_s>_<hex>, distinguishable by digit count. All three sorts now use it, so the global list, the app list and the loader agree. The filter bar is client-side over the already-loaded per-app array, so it is instant and needs no reload: status chips (built from the statuses actually present, with counts, so a chip can never return zero) plus a search over the command and the task id — the id being what a deep link and a log URL both carry, so pasting one finds it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
211 lines
6.7 KiB
JavaScript
211 lines
6.7 KiB
JavaScript
// Auto-extracted from tasks-manager.js (verbatim) — augments TasksManager.prototype. Loaded after the base.
|
|
Object.assign(TasksManager.prototype, {
|
|
async refreshTasks() {
|
|
// Show refresh notification
|
|
const refreshNotification = window.notificationSystem.info(
|
|
'🔄 Refreshing tasks...',
|
|
'Tasks',
|
|
null,
|
|
null
|
|
);
|
|
|
|
try {
|
|
await this.loadTasks();
|
|
|
|
// Remove refresh notification and show success
|
|
if (refreshNotification && refreshNotification.remove) {
|
|
refreshNotification.remove();
|
|
}
|
|
|
|
if (window.notificationSystem) {
|
|
window.notificationSystem.success(
|
|
'🔄 Tasks refreshed successfully',
|
|
'Tasks',
|
|
null,
|
|
null
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error refreshing tasks:', error);
|
|
|
|
// Remove refresh notification and show error
|
|
if (refreshNotification && refreshNotification.remove) {
|
|
refreshNotification.remove();
|
|
}
|
|
|
|
if (window.notificationSystem) {
|
|
window.notificationSystem.error(
|
|
`⚠️ Failed to refresh tasks: ${error.message}`,
|
|
'Tasks',
|
|
null,
|
|
null
|
|
);
|
|
}
|
|
}
|
|
},
|
|
async loadTasks() {
|
|
try {
|
|
//// // console.log('🔄 Loading tasks from file system...');
|
|
|
|
// Check if task system is available
|
|
if (!this.taskManager) {
|
|
console.warn('⚠️ Task system not yet initialized, skipping task loading');
|
|
this.tasks = [];
|
|
return;
|
|
}
|
|
|
|
// Get tasks using new system
|
|
|
|
// Get queue and current status
|
|
let queue = [];
|
|
let current = {};
|
|
|
|
try {
|
|
const queueResponse = await fetch('/read-file?path=tasks/queue.json');
|
|
if (queueResponse.ok) {
|
|
const queueText = await queueResponse.text();
|
|
if (queueText.trim()) { // Only parse if not empty
|
|
try {
|
|
queue = JSON.parse(queueText);
|
|
} catch (parseError) {
|
|
console.warn('⚠️ Invalid queue.json format, starting with empty queue');
|
|
queue = [];
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
}
|
|
|
|
try {
|
|
const currentResponse = await fetch('/read-file?path=tasks/current.json');
|
|
if (currentResponse.ok) {
|
|
const currentText = await currentResponse.text();
|
|
if (currentText.trim()) { // Only parse if not empty
|
|
try {
|
|
current = JSON.parse(currentText);
|
|
} catch (parseError) {
|
|
console.warn('⚠️ Invalid current.json format, treating as empty');
|
|
current = {};
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
}
|
|
|
|
// Load individual task files
|
|
const allTasks = [];
|
|
|
|
// Add queued tasks
|
|
for (const taskId of queue) {
|
|
try {
|
|
const task = await this.taskManager.getTask(taskId);
|
|
if (task) allTasks.push(task);
|
|
} catch (error) {
|
|
console.warn(`⚠️ Failed to load queued task ${taskId}:`, error);
|
|
}
|
|
}
|
|
|
|
// Add current task if different from queue
|
|
if (current.id && !queue.includes(current.id)) {
|
|
try {
|
|
const task = await this.taskManager.getTask(current.id);
|
|
if (task) allTasks.push(task);
|
|
} catch (error) {
|
|
console.warn(`⚠️ Failed to load current task ${current.id}:`, error);
|
|
}
|
|
}
|
|
|
|
// Scan tasks folder for all task files (including completed ones) - OPTIMIZED
|
|
try {
|
|
const tasksResponse = await fetch('/read-directory?path=tasks');
|
|
if (tasksResponse.ok) {
|
|
const files = await tasksResponse.json();
|
|
const taskFiles = files.filter(file =>
|
|
file.endsWith('.json') &&
|
|
file !== 'queue.json' &&
|
|
file !== 'current.json'
|
|
);
|
|
|
|
|
|
// OPTIMIZATION: Batch load tasks instead of individual calls
|
|
const missingTaskIds = taskFiles
|
|
.map(file => file.replace('.json', ''))
|
|
.filter(taskId => !allTasks.find(task => task.id === taskId));
|
|
|
|
if (missingTaskIds.length > 0) {
|
|
try {
|
|
const batchResponse = await fetch('/read-tasks-batch', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ taskIds: missingTaskIds })
|
|
});
|
|
|
|
if (batchResponse.ok) {
|
|
const batchTasks = await batchResponse.json();
|
|
batchTasks.forEach(task => {
|
|
if (task) {
|
|
allTasks.push(task);
|
|
}
|
|
});
|
|
} else {
|
|
// Fallback to individual loading if batch endpoint not available
|
|
await this.loadTasksIndividually(missingTaskIds, allTasks);
|
|
}
|
|
} catch (error) {
|
|
console.warn('⚠️ Batch loading failed, falling back to individual loading:', error);
|
|
await this.loadTasksIndividually(missingTaskIds, allTasks);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('⚠️ Failed to scan tasks folder:', error);
|
|
}
|
|
|
|
//// // console.log('📊 Task counts:', {
|
|
//queued: queuedTasks.length,
|
|
//processing: processingTasks.length,
|
|
//completed: completedTasks.length
|
|
//});
|
|
|
|
// Combine all tasks
|
|
this.tasks = allTasks;
|
|
|
|
// Sort by creation time (newest first)
|
|
// Newest first, tolerant of a missing createdAt (see taskSortTime).
|
|
this.tasks.sort((a, b) =>
|
|
(window.taskSortTime ? window.taskSortTime(b) - window.taskSortTime(a)
|
|
: new Date(b.createdAt) - new Date(a.createdAt)));
|
|
|
|
//// // console.log('📋 All tasks:', this.tasks);
|
|
|
|
this.renderTasks();
|
|
this.updateStats();
|
|
this.updateSidebarCounts();
|
|
this.generateAppCategories();
|
|
} catch (error) {
|
|
console.error('❌ Failed to load tasks:', error);
|
|
if (window.notificationSystem) {
|
|
window.notificationSystem.error(`Failed to load tasks: ${error.message}`);
|
|
}
|
|
this.tasks = [];
|
|
this.renderTasks();
|
|
this.updateStats();
|
|
this.updateSidebarCounts();
|
|
this.generateAppCategories();
|
|
}
|
|
},
|
|
async loadTasksIndividually(taskIds, allTasks) {
|
|
// Fallback method for individual task loading
|
|
for (const taskId of taskIds) {
|
|
try {
|
|
const task = await this.taskManager.getTask(taskId);
|
|
if (task) {
|
|
allTasks.push(task);
|
|
}
|
|
} catch (error) {
|
|
console.warn(`⚠️ Failed to load task ${taskId}:`, error);
|
|
}
|
|
}
|
|
},
|
|
});
|