rate limit queue, content
This commit is contained in:
		
							parent
							
								
									6cdac6ec7c
								
							
						
					
					
						commit
						e90da3b2fb
					
				@ -998,10 +998,10 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Reset smart prompting when submitting
 | 
			
		||||
      resetSmartPrompting();
 | 
			
		||||
 | 
			
		||||
      const taskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
 | 
			
		||||
      console.log(`[FRONTEND] Starting AI request with taskId: ${taskId}`);
 | 
			
		||||
 | 
			
		||||
      aiResults.style.display = 'none';
 | 
			
		||||
      aiError.style.display = 'none';
 | 
			
		||||
@ -1011,7 +1011,7 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
      const taskIdDisplay = document.getElementById('current-task-id');
 | 
			
		||||
      if (queueStatus && taskIdDisplay) {
 | 
			
		||||
        queueStatus.style.display = 'block';
 | 
			
		||||
        taskIdDisplay.textContent = taskId;
 | 
			
		||||
        taskIdDisplay.textContent = taskId.slice(-8);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      aiSubmitBtn.disabled = true;
 | 
			
		||||
@ -1023,64 +1023,90 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
      const updateQueueStatus = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
          const response = await fetch(`/api/ai/queue-status?taskId=${taskId}`);
 | 
			
		||||
          
 | 
			
		||||
          if (!response.ok) {
 | 
			
		||||
            console.error(`[FRONTEND] Queue status HTTP error: ${response.status}`);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          const data = await response.json();
 | 
			
		||||
          
 | 
			
		||||
          if (data.success) {
 | 
			
		||||
            const queueLength = document.getElementById('queue-length');
 | 
			
		||||
            const estimatedTime = document.getElementById('estimated-time');
 | 
			
		||||
            const positionBadge = document.getElementById('queue-position-badge');
 | 
			
		||||
            const progressBar = document.getElementById('queue-progress');
 | 
			
		||||
            
 | 
			
		||||
            if (queueLength) queueLength.textContent = data.queueLength;
 | 
			
		||||
            
 | 
			
		||||
            if (estimatedTime) {
 | 
			
		||||
              if (data.estimatedWaitTime > 0) {
 | 
			
		||||
                estimatedTime.textContent = formatDuration(data.estimatedWaitTime);
 | 
			
		||||
              } else {
 | 
			
		||||
                estimatedTime.textContent = 'Verarbeitung läuft...';
 | 
			
		||||
              }
 | 
			
		||||
          const queueLength = document.getElementById('queue-length');
 | 
			
		||||
          const estimatedTime = document.getElementById('estimated-time');
 | 
			
		||||
          const positionBadge = document.getElementById('queue-position-badge');
 | 
			
		||||
          const progressBar = document.getElementById('queue-progress');
 | 
			
		||||
          
 | 
			
		||||
          if (queueLength) {
 | 
			
		||||
            queueLength.textContent = data.queueLength || 0;
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          if (estimatedTime) {
 | 
			
		||||
            if (data.estimatedWaitTime > 0) {
 | 
			
		||||
              estimatedTime.textContent = formatDuration(data.estimatedWaitTime);
 | 
			
		||||
            } else {
 | 
			
		||||
              estimatedTime.textContent = 'Verarbeitung läuft...';
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (positionBadge && data.currentPosition) {
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          if (positionBadge) {
 | 
			
		||||
            if (data.currentPosition) {
 | 
			
		||||
              positionBadge.textContent = data.currentPosition;
 | 
			
		||||
              
 | 
			
		||||
              if (progressBar && data.queueLength > 0) {
 | 
			
		||||
                const progress = Math.max(0, ((data.queueLength - data.currentPosition + 1) / data.queueLength) * 100);
 | 
			
		||||
                progressBar.style.width = `${progress}%`;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (data.isProcessing && !data.currentPosition) {
 | 
			
		||||
              if (positionBadge) positionBadge.textContent = '⚡';
 | 
			
		||||
              if (progressBar) progressBar.style.width = '100%';
 | 
			
		||||
              if (estimatedTime) estimatedTime.textContent = 'Verarbeitung läuft...';
 | 
			
		||||
            } else {
 | 
			
		||||
              if (data.taskStatus === 'processing') {
 | 
			
		||||
                positionBadge.textContent = '⚡';
 | 
			
		||||
                if (progressBar) progressBar.style.width = '100%';
 | 
			
		||||
                console.log(`[FRONTEND] Task ${taskId.slice(-6)} is processing but no position returned`);
 | 
			
		||||
              } else if (data.taskStatus === 'completed') {
 | 
			
		||||
                positionBadge.textContent = '✅';
 | 
			
		||||
                if (progressBar) progressBar.style.width = '100%';
 | 
			
		||||
                console.log(`[FRONTEND] Task ${taskId.slice(-6)} completed`);
 | 
			
		||||
              } else if (data.taskStatus === 'failed') {
 | 
			
		||||
                positionBadge.textContent = '❌';
 | 
			
		||||
                if (progressBar) progressBar.style.width = '100%';
 | 
			
		||||
                console.log(`[FRONTEND] Task ${taskId.slice(-6)} failed`);
 | 
			
		||||
              } else {
 | 
			
		||||
                positionBadge.textContent = '?';
 | 
			
		||||
                if (progressBar) progressBar.style.width = '0%';
 | 
			
		||||
                console.log(`[FRONTEND] Task ${taskId.slice(-6)} status unknown:`, data.taskStatus);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
                    
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          console.warn('Queue status update failed:', error);
 | 
			
		||||
          console.error('[FRONTEND] Queue status update failed:', error);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      };  
 | 
			
		||||
 | 
			
		||||
      const aiRequestPromise = fetch('/api/ai/query', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json',
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({ 
 | 
			
		||||
          query,
 | 
			
		||||
          mode: currentMode,
 | 
			
		||||
          taskId
 | 
			
		||||
        })
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      updateQueueStatus();
 | 
			
		||||
      
 | 
			
		||||
      statusInterval = setInterval(updateQueueStatus, 500);
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        updateQueueStatus(); 
 | 
			
		||||
        statusInterval = setInterval(updateQueueStatus, 1000); // Poll every 1 second for better responsiveness
 | 
			
		||||
      }, 500); 
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await fetch('/api/ai/query', {
 | 
			
		||||
          method: 'POST',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json',
 | 
			
		||||
          },
 | 
			
		||||
          body: JSON.stringify({ 
 | 
			
		||||
            query,
 | 
			
		||||
            mode: currentMode,
 | 
			
		||||
            taskId
 | 
			
		||||
          })
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const response = await aiRequestPromise;
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
        if (statusInterval) clearInterval(statusInterval);
 | 
			
		||||
        if (statusInterval) {
 | 
			
		||||
          clearInterval(statusInterval);
 | 
			
		||||
          console.log(`[FRONTEND] AI request completed for ${taskId.slice(-6)}, stopping status polling`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!response.ok) {
 | 
			
		||||
          throw new Error(data.error || `HTTP ${response.status}`);
 | 
			
		||||
@ -1102,7 +1128,7 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
        aiResults.style.display = 'block';
 | 
			
		||||
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('AI query failed:', error);
 | 
			
		||||
        console.error(`[FRONTEND] AI query failed for ${taskId.slice(-6)}:`, error);
 | 
			
		||||
        
 | 
			
		||||
        if (statusInterval) clearInterval(statusInterval);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -287,16 +287,22 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
    const body = await request.json();
 | 
			
		||||
    const { query, mode = 'workflow', taskId: clientTaskId } = body;
 | 
			
		||||
 | 
			
		||||
    // ADD THIS DEBUG LOGGING
 | 
			
		||||
    console.log(`[AI API] Received request - TaskId: ${clientTaskId}, Mode: ${mode}, Query length: ${query?.length || 0}`);
 | 
			
		||||
 | 
			
		||||
    if (!query || typeof query !== 'string') {
 | 
			
		||||
      console.log(`[AI API] Invalid query for task ${clientTaskId}`);
 | 
			
		||||
      return apiError.badRequest('Query required');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!['workflow', 'tool'].includes(mode)) {
 | 
			
		||||
      console.log(`[AI API] Invalid mode for task ${clientTaskId}: ${mode}`);
 | 
			
		||||
      return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const sanitizedQuery = sanitizeInput(query);
 | 
			
		||||
    if (sanitizedQuery.includes('[FILTERED]')) {
 | 
			
		||||
      console.log(`[AI API] Filtered input detected for task ${clientTaskId}`);
 | 
			
		||||
      return apiError.badRequest('Invalid input detected');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -308,6 +314,9 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
    
 | 
			
		||||
    const taskId = clientTaskId || `ai_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
 | 
			
		||||
    
 | 
			
		||||
    console.log(`[AI API] About to enqueue task ${taskId}`);
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    const aiResponse = await enqueueApiCall(() =>
 | 
			
		||||
      fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								src/pages/api/ai/queue-debug.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/pages/api/ai/queue-debug.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
// src/pages/api/ai/queue-debug.ts - Enhanced debug endpoint
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { getQueueDebugState, getAllTaskStatuses } from '../../../utils/rateLimitedQueue.js';
 | 
			
		||||
import { apiResponse, apiServerError } from '../../../utils/api.js';
 | 
			
		||||
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // Only allow in development or with special header
 | 
			
		||||
    const isDev = process.env.NODE_ENV === 'development';
 | 
			
		||||
    const debugHeader = request.headers.get('X-Debug-Queue');
 | 
			
		||||
    
 | 
			
		||||
    if (!isDev && debugHeader !== 'true') {
 | 
			
		||||
      return apiServerError.internal('Debug endpoint not available');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const debugState = getQueueDebugState();
 | 
			
		||||
    const allTaskStatuses = getAllTaskStatuses();
 | 
			
		||||
    
 | 
			
		||||
    return apiResponse.success({
 | 
			
		||||
      ...debugState,
 | 
			
		||||
      allTaskStatuses,
 | 
			
		||||
      timestamp: Date.now(),
 | 
			
		||||
      message: 'Enhanced queue debug state snapshot'
 | 
			
		||||
    });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Queue debug error:', error);
 | 
			
		||||
    return apiServerError.internal('Failed to get queue debug state');
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@ -12,51 +12,67 @@ interface QueuedTask {
 | 
			
		||||
  id: string;
 | 
			
		||||
  task: Task;
 | 
			
		||||
  addedAt: number;
 | 
			
		||||
  status: 'queued' | 'processing' | 'completed' | 'failed';
 | 
			
		||||
  startedAt?: number;
 | 
			
		||||
  completedAt?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface QueueStatus {
 | 
			
		||||
  queueLength: number;
 | 
			
		||||
  isProcessing: boolean;
 | 
			
		||||
  estimatedWaitTime: number; // in milliseconds
 | 
			
		||||
  currentPosition?: number; 
 | 
			
		||||
  estimatedWaitTime: number;
 | 
			
		||||
  currentPosition?: number;
 | 
			
		||||
  taskStatus?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class RateLimitedQueue {
 | 
			
		||||
  private queue: QueuedTask[] = [];
 | 
			
		||||
  private processing = false;
 | 
			
		||||
  private tasks: QueuedTask[] = [];
 | 
			
		||||
  private isProcessing = false;
 | 
			
		||||
  private delayMs = RATE_LIMIT_DELAY_MS;
 | 
			
		||||
  private lastProcessedAt = 0;
 | 
			
		||||
  private currentlyProcessingTaskId: string | null = null;
 | 
			
		||||
 | 
			
		||||
  add<T>(task: Task<T>, taskId?: string): Promise<T> {
 | 
			
		||||
    const id = taskId || this.generateTaskId();
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    return new Promise<T>((resolve, reject) => {
 | 
			
		||||
      this.queue.push({
 | 
			
		||||
      const queuedTask: QueuedTask = {
 | 
			
		||||
        id,
 | 
			
		||||
        task: async () => {
 | 
			
		||||
          try {
 | 
			
		||||
            const result = await task();
 | 
			
		||||
            resolve(result);
 | 
			
		||||
            return result;
 | 
			
		||||
          } catch (err) {
 | 
			
		||||
            reject(err);
 | 
			
		||||
            throw err;
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        addedAt: Date.now()
 | 
			
		||||
      });
 | 
			
		||||
      this.process();
 | 
			
		||||
        addedAt: Date.now(),
 | 
			
		||||
        status: 'queued'
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      this.tasks.push(queuedTask);
 | 
			
		||||
      
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.processQueue();
 | 
			
		||||
      }, 100);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStatus(taskId?: string): QueueStatus {
 | 
			
		||||
    const queueLength = this.queue.length;
 | 
			
		||||
    const queuedTasks = this.tasks.filter(t => t.status === 'queued');
 | 
			
		||||
    const processingTasks = this.tasks.filter(t => t.status === 'processing');
 | 
			
		||||
    const queueLength = queuedTasks.length + processingTasks.length;
 | 
			
		||||
    
 | 
			
		||||
    const now = Date.now();
 | 
			
		||||
    
 | 
			
		||||
    let estimatedWaitTime = 0;
 | 
			
		||||
    if (queueLength > 0) {
 | 
			
		||||
      if (this.processing) {
 | 
			
		||||
      if (this.isProcessing && this.lastProcessedAt > 0) {
 | 
			
		||||
        const timeSinceLastRequest = now - this.lastProcessedAt;
 | 
			
		||||
        const remainingDelay = Math.max(0, this.delayMs - timeSinceLastRequest);
 | 
			
		||||
        estimatedWaitTime = remainingDelay + (queueLength - 1) * this.delayMs;
 | 
			
		||||
        const remainingDelay = Math.max(0, this.delayMs * 2 - timeSinceLastRequest);
 | 
			
		||||
        estimatedWaitTime = remainingDelay + queuedTasks.length * this.delayMs;
 | 
			
		||||
      } else {
 | 
			
		||||
        estimatedWaitTime = queueLength * this.delayMs;
 | 
			
		||||
      }
 | 
			
		||||
@ -64,14 +80,41 @@ class RateLimitedQueue {
 | 
			
		||||
 | 
			
		||||
    const status: QueueStatus = {
 | 
			
		||||
      queueLength,
 | 
			
		||||
      isProcessing: this.processing,
 | 
			
		||||
      isProcessing: this.isProcessing,
 | 
			
		||||
      estimatedWaitTime
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (taskId) {
 | 
			
		||||
      const position = this.queue.findIndex(item => item.id === taskId);
 | 
			
		||||
      if (position >= 0) {
 | 
			
		||||
        status.currentPosition = position + 1; 
 | 
			
		||||
      const task = this.tasks.find(t => t.id === taskId);
 | 
			
		||||
      
 | 
			
		||||
      if (task) {
 | 
			
		||||
        status.taskStatus = task.status;
 | 
			
		||||
        
 | 
			
		||||
        if (task.status === 'processing') {
 | 
			
		||||
          status.currentPosition = 1;
 | 
			
		||||
        } else if (task.status === 'queued') {
 | 
			
		||||
          const queuedTasksInOrder = this.tasks
 | 
			
		||||
            .filter(t => t.status === 'queued')
 | 
			
		||||
            .sort((a, b) => a.addedAt - b.addedAt);
 | 
			
		||||
          
 | 
			
		||||
          const positionInQueue = queuedTasksInOrder.findIndex(t => t.id === taskId);
 | 
			
		||||
          
 | 
			
		||||
          if (positionInQueue >= 0) {
 | 
			
		||||
            const processingOffset = processingTasks.length > 0 ? 1 : 0;
 | 
			
		||||
            status.currentPosition = processingOffset + positionInQueue + 1;
 | 
			
		||||
          }
 | 
			
		||||
        } else if (task.status === 'completed' || task.status === 'failed') {
 | 
			
		||||
        }
 | 
			
		||||
      } else {        
 | 
			
		||||
        const taskTimestamp = taskId.match(/ai_(\d+)_/)?.[1];
 | 
			
		||||
        if (taskTimestamp) {
 | 
			
		||||
          const taskAge = now - parseInt(taskTimestamp);
 | 
			
		||||
          if (taskAge < 30000) { 
 | 
			
		||||
            status.taskStatus = 'starting';
 | 
			
		||||
          } else {
 | 
			
		||||
            status.taskStatus = 'unknown';
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -87,23 +130,60 @@ class RateLimitedQueue {
 | 
			
		||||
    return this.delayMs;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async process(): Promise<void> {
 | 
			
		||||
    if (this.processing) return;
 | 
			
		||||
    this.processing = true;
 | 
			
		||||
 | 
			
		||||
    while (this.queue.length > 0) {
 | 
			
		||||
      const next = this.queue.shift();
 | 
			
		||||
      if (!next) continue;
 | 
			
		||||
      
 | 
			
		||||
      this.lastProcessedAt = Date.now();
 | 
			
		||||
      await next.task();
 | 
			
		||||
      
 | 
			
		||||
      if (this.queue.length > 0) {
 | 
			
		||||
        await new Promise((r) => setTimeout(r, this.delayMs));
 | 
			
		||||
      }
 | 
			
		||||
  private async processQueue(): Promise<void> {
 | 
			
		||||
    if (this.isProcessing) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.processing = false;
 | 
			
		||||
    this.isProcessing = true;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      while (true) {
 | 
			
		||||
        const nextTask = this.tasks
 | 
			
		||||
          .filter(t => t.status === 'queued')
 | 
			
		||||
          .sort((a, b) => a.addedAt - b.addedAt)[0];
 | 
			
		||||
        
 | 
			
		||||
        if (!nextTask) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        nextTask.status = 'processing';
 | 
			
		||||
        nextTask.startedAt = Date.now();
 | 
			
		||||
        this.currentlyProcessingTaskId = nextTask.id;
 | 
			
		||||
        this.lastProcessedAt = Date.now();
 | 
			
		||||
        
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
          await nextTask.task();
 | 
			
		||||
          nextTask.status = 'completed';
 | 
			
		||||
          nextTask.completedAt = Date.now();
 | 
			
		||||
          console.log(`[QUEUE] Task ${nextTask.id} completed`);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          nextTask.status = 'failed';
 | 
			
		||||
          nextTask.completedAt = Date.now();
 | 
			
		||||
          console.error(`[QUEUE] Task ${nextTask.id} failed:`, error);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        this.currentlyProcessingTaskId = null;
 | 
			
		||||
        
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          const index = this.tasks.findIndex(t => t.id === nextTask.id);
 | 
			
		||||
          if (index >= 0) {
 | 
			
		||||
            console.log(`[QUEUE] Removing completed task ${nextTask.id}`);
 | 
			
		||||
            this.tasks.splice(index, 1);
 | 
			
		||||
          }
 | 
			
		||||
        }, 10000); 
 | 
			
		||||
        
 | 
			
		||||
        const hasMoreQueued = this.tasks.some(t => t.status === 'queued');
 | 
			
		||||
        if (hasMoreQueued) {
 | 
			
		||||
          console.log(`[QUEUE] Waiting ${this.delayMs}ms before next task`);
 | 
			
		||||
          await new Promise((r) => setTimeout(r, this.delayMs));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } finally {
 | 
			
		||||
      this.isProcessing = false;
 | 
			
		||||
      console.log(`[QUEUE] Queue processing finished`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private generateTaskId(): string {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user