${this.esc(task.title || BugReportCore.buildBugTaskTitle(report))}
${this.esc(report.actual_result || 'Описание не указано')}
${prompt ? `Показать prompt для Codex
${this.esc(prompt)}
const BugReports = { bundle: null, employees: [], isLoading: false, filters: { search: '', severity: '', section: '', status: 'open', }, quickDraft: null, _overlayOpen: false, submittingPrefixes: new Set(), _draftSaveTimer: null, _draftRestored: false, async load() { this.isLoading = true; this.render(); try { await this.refreshData(); } finally { this.isLoading = false; this.render(); } }, async refreshData() { const [bundle, employees] = await Promise.all([ loadWorkBundle(), loadEmployees(), ]); this.bundle = this._hydrateBundle(bundle); this.employees = employees || []; }, _hydrateBundle(bundle) { const next = bundle && typeof bundle === 'object' ? { ...bundle } : {}; next.tasks = Array.isArray(next.tasks) ? next.tasks.slice() : []; const reports = Array.isArray(next.bugReports) ? next.bugReports.map(report => ({ ...report })) : []; const reportTaskIds = new Set( reports .map(report => String(report?.task_id || '').trim()) .filter(Boolean) ); next.tasks.forEach(task => { if (!this._isBugLikeTask(task)) return; const taskId = String(task?.id || '').trim(); if (!taskId || reportTaskIds.has(taskId)) return; const synthetic = this._buildSyntheticBugReport(task); if (!synthetic) return; reports.push(synthetic); reportTaskIds.add(taskId); }); next.bugReports = reports.sort((a, b) => String(b?.created_at || '').localeCompare(String(a?.created_at || ''))); return next; }, _isBugLikeTask(task) { const title = String(task?.title || '').trim(); if (/^\[баг\]/i.test(title)) return true; return String(task?.type || '').trim().toLowerCase() === 'bug'; }, _extractBugTaskDescriptionFields(description) { const fields = { actual_result: '', expected_result: '', steps_to_reproduce: '', page_route: '', page_url: '', browser: '', os: '', viewport: '', severity: '', submitted_by_name: '', }; String(description || '') .split(/\n\s*\n/) .map(block => String(block || '').trim()) .filter(Boolean) .forEach(block => { if (/^проблема:/i.test(block)) fields.actual_result = block.replace(/^проблема:\s*/i, '').trim(); else if (/^ожидалось:/i.test(block)) fields.expected_result = block.replace(/^ожидалось:\s*/i, '').trim(); else if (/^шаги:/i.test(block)) fields.steps_to_reproduce = block.replace(/^шаги:\s*/i, '').trim(); else if (/^маршрут\s*\/\s*hash:/i.test(block)) fields.page_route = block.replace(/^маршрут\s*\/\s*hash:\s*/i, '').trim(); else if (/^url:/i.test(block)) fields.page_url = block.replace(/^url:\s*/i, '').trim(); else if (/^браузер:/i.test(block)) fields.browser = block.replace(/^браузер:\s*/i, '').trim(); else if (/^ос:/i.test(block)) fields.os = block.replace(/^ос:\s*/i, '').trim(); else if (/^viewport:/i.test(block)) fields.viewport = block.replace(/^viewport:\s*/i, '').trim(); else if (/^серьезность:/i.test(block)) fields.severity = block.replace(/^серьезность:\s*/i, '').trim().toLowerCase(); else if (/^сообщил:/i.test(block)) fields.submitted_by_name = block.replace(/^сообщил:\s*/i, '').trim(); }); return fields; }, _findSectionByLabel(label) { const normalized = BugReportCore.normalizeText(label); if (!normalized) return null; return (BugReportCore.getSectionCatalog() || []).find(section => BugReportCore.normalizeText(section?.label) === normalized ) || null; }, _findSubsectionByLabel(sectionKey, label) { const normalized = BugReportCore.normalizeText(label); if (!normalized) return null; const section = BugReportCore.getSectionByKey(sectionKey); return (section?.subsections || []).find(item => BugReportCore.normalizeText(item?.label) === normalized ) || null; }, _parseBugTaskTitle(task) { const rawTitle = String(task?.title || '').trim(); const stripped = rawTitle.replace(/^\[баг\]\s*/i, '').trim(); const match = stripped.match(/^(.+?)\s+—\s+(.+)$/); const context = match ? String(match[1] || '').trim() : ''; const title = match ? String(match[2] || '').trim() : stripped; const contextParts = context ? context.split(/\s*\/\s*/).map(part => String(part || '').trim()).filter(Boolean) : []; const section = contextParts.length > 0 ? this._findSectionByLabel(contextParts[0]) : null; const subsection = section && contextParts.length > 1 ? this._findSubsectionByLabel(section.key, contextParts[1]) : null; return { title: title || stripped || rawTitle, section, subsection, }; }, _severityFromTask(task, parsedSeverity) { const explicit = String(parsedSeverity || '').trim().toLowerCase(); if (['low', 'medium', 'high', 'critical'].includes(explicit)) return explicit; const priority = String(task?.priority || '').trim().toLowerCase(); if (priority === 'urgent') return 'critical'; if (priority === 'high') return 'high'; if (priority === 'low') return 'low'; return 'medium'; }, _buildSyntheticBugReport(task) { if (!task || !task.id) return null; const parsedTitle = this._parseBugTaskTitle(task); const parsedDescription = this._extractBugTaskDescriptionFields(task.description); const inferredSection = parsedTitle.section || BugReportCore.inferSectionFromRoute(parsedDescription.page_route); const section = inferredSection || BugReportCore.getSectionByKey('general'); const subsection = parsedTitle.subsection || BugReportCore.getSubsectionByKey(section?.key, BugReportCore.inferSubsectionKey(section?.key, parsedDescription.page_route)) || BugReportCore.getSubsectionByKey(section?.key, 'other'); return { id: `task:${task.id}`, task_id: Number(task.id), title: parsedTitle.title, section_key: section?.key || 'general', section_name: section?.label || 'Другое', subsection_key: subsection?.key || 'other', subsection_name: subsection?.label || 'Другое', page_route: parsedDescription.page_route || '', page_url: parsedDescription.page_url || '', app_version: '', browser: parsedDescription.browser || '', os: parsedDescription.os || '', viewport: parsedDescription.viewport || '', steps_to_reproduce: parsedDescription.steps_to_reproduce || '', expected_result: parsedDescription.expected_result || '', actual_result: parsedDescription.actual_result || String(task.description || '').trim(), severity: this._severityFromTask(task, parsedDescription.severity), codex_prompt: '', codex_status: '', codex_result: '', codex_error: '', submitted_by: task.created_by || null, submitted_by_name: parsedDescription.submitted_by_name || task.created_by_name || task.author_name || '', created_at: task.created_at || new Date().toISOString(), updated_at: task.updated_at || task.created_at || new Date().toISOString(), synthetic: true, }; }, esc(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }, currentRoute() { const raw = window.location.hash || ''; return raw === '#bugs' ? '' : raw; }, currentPageUrl() { try { return window.location.href || ''; } catch (error) { return ''; } }, draftStorageKey() { return `ro_bug_report_draft_v1:${App?.currentEmployeeId || 'guest'}`; }, draftTtlMs() { return 6 * 60 * 60 * 1000; }, meaningfulDraft(draft) { if (!draft || typeof draft !== 'object') return false; return [ draft.title, draft.actual_result, draft.expected_result, draft.steps_to_reproduce, draft.extra_link, ].some(value => String(value || '').trim()); }, readStoredDraft() { try { const raw = localStorage.getItem(this.draftStorageKey()); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed?.saved_at || (Date.now() - Number(parsed.saved_at)) > this.draftTtlMs()) { localStorage.removeItem(this.draftStorageKey()); return null; } return parsed.draft || null; } catch (error) { return null; } }, persistStoredDraft(draft) { try { if (!this.meaningfulDraft(draft)) { localStorage.removeItem(this.draftStorageKey()); return; } localStorage.setItem(this.draftStorageKey(), JSON.stringify({ saved_at: Date.now(), draft, })); } catch (error) { console.warn('[BugReports] draft persist failed:', error); } }, clearStoredDraft() { clearTimeout(this._draftSaveTimer); this._draftSaveTimer = null; this._draftRestored = false; try { localStorage.removeItem(this.draftStorageKey()); } catch (error) { console.warn('[BugReports] draft clear failed:', error); } }, currentContextDraft(preset = {}) { const storedDraft = Object.keys(preset || {}).length === 0 ? this.readStoredDraft() : null; this._draftRestored = !!storedDraft; const baseDraft = storedDraft || {}; const route = preset.page_route != null ? preset.page_route : (baseDraft.page_route || this.currentRoute()); const inferredSection = preset.section_key ? BugReportCore.getSectionByKey(preset.section_key) : BugReportCore.inferSectionFromRoute(route); const sectionKey = preset.section_key || baseDraft.section_key || inferredSection?.key || 'general'; const subsectionKey = preset.subsection_key || baseDraft.subsection_key || BugReportCore.inferSubsectionKey(sectionKey, route); return { title: preset.title || baseDraft.title || '', section_key: sectionKey, section_name: preset.section_name || baseDraft.section_name || BugReportCore.getSectionByKey(sectionKey)?.label || '', subsection_key: subsectionKey, subsection_name: preset.subsection_name || baseDraft.subsection_name || BugReportCore.getSubsectionByKey(sectionKey, subsectionKey)?.label || '', page_route: route, page_url: preset.page_url || baseDraft.page_url || this.currentPageUrl(), browser: preset.browser || baseDraft.browser || BugReportCore.summarizeBrowser(navigator.userAgent), os: preset.os || baseDraft.os || BugReportCore.summarizeOs(navigator.userAgent), viewport: preset.viewport || baseDraft.viewport || `${window.innerWidth || 0}x${window.innerHeight || 0}`, app_version: preset.app_version || baseDraft.app_version || (typeof APP_VERSION !== 'undefined' ? APP_VERSION : ''), severity: preset.severity || baseDraft.severity || 'medium', actual_result: preset.actual_result || baseDraft.actual_result || '', expected_result: preset.expected_result || baseDraft.expected_result || '', steps_to_reproduce: preset.steps_to_reproduce || baseDraft.steps_to_reproduce || '', extra_link: preset.extra_link || baseDraft.extra_link || '', }; }, pageReports() { const tasksById = new Map((this.bundle?.tasks || []).map(task => [String(task.id), task])); const assetsByTask = new Map(); (this.bundle?.assets || []).forEach(asset => { const key = String(asset.task_id || ''); if (!key) return; const bucket = assetsByTask.get(key) || []; bucket.push(asset); assetsByTask.set(key, bucket); }); return (this.bundle?.bugReports || []) .map(report => ({ report, task: tasksById.get(String(report.task_id || '')) || null, assets: assetsByTask.get(String(report.task_id || '')) || [], })) .sort((a, b) => String(b.report?.created_at || '').localeCompare(String(a.report?.created_at || ''))); }, filteredReports() { const search = BugReportCore.normalizeText(this.filters.search); return this.pageReports().filter(entry => { const task = entry.task || {}; const report = entry.report || {}; if (this.filters.severity && report.severity !== this.filters.severity) return false; if (this.filters.section && report.section_key !== this.filters.section) return false; if (this.filters.status === 'open' && this.isTaskClosed(task)) return false; if (this.filters.status === 'closed' && !this.isTaskClosed(task)) return false; if (!search) return true; const haystack = [ task.title, report.title, report.actual_result, report.expected_result, report.steps_to_reproduce, report.section_name, report.subsection_name, report.page_route, ].map(BugReportCore.normalizeText).join(' '); return haystack.includes(search); }); }, isTaskClosed(task) { return task && (task.status === 'done' || task.status === 'cancelled'); }, openStats() { const reports = this.pageReports(); return { total: reports.length, open: reports.filter(entry => !this.isTaskClosed(entry.task)).length, high: reports.filter(entry => ['high', 'critical'].includes(entry.report?.severity)).length, promptReady: reports.filter(entry => this.promptText(entry.report)).length, }; }, statusLabel(task) { return task ? WorkManagementCore.getTaskStatusLabel(task.status) : 'Без задачи'; }, promptText(report) { if (!report) return ''; return String( report.codex_prompt || report.prompt || report.codex_result || '' ).trim(); }, priorityFromSeverity(severity) { if (severity === 'critical') return 'urgent'; if (severity === 'high') return 'high'; if (severity === 'low') return 'low'; return 'normal'; }, dueDateFromSeverity(severity) { const base = new Date(); const shiftDays = severity === 'critical' ? 0 : severity === 'high' ? 1 : severity === 'low' ? 5 : 3; base.setDate(base.getDate() + shiftDays); return base.toISOString().slice(0, 10); }, findAreaId(slug) { return (this.bundle?.areas || []).find(item => String(item.slug || '') === slug)?.id || null; }, defaultBugAssigneeId() { const configured = Number(App?.settings?.bug_report_default_assignee_id || 0); if (Number.isFinite(configured) && configured > 0) return configured; const currentEmployeeId = Number(App?.currentEmployeeId || 0); if (Number.isFinite(currentEmployeeId) && currentEmployeeId > 0) return currentEmployeeId; const polina = (this.employees || []).find(item => Number(item.id) === 5); if (polina) return polina.id; return App.currentEmployeeId || null; }, employeeName(employeeId) { return (this.employees || []).find(item => String(item.id) === String(employeeId))?.name || ''; }, render() { const container = document.getElementById('page-bugs'); if (!container) return; if (this.isLoading && !this.bundle) { container.innerHTML = `
Единая точка входа для команды: баг попадает в задачу, получает prompt для Codex и не теряется в чатах.
По выбранным фильтрам багов нет.
${this.esc(report.actual_result || 'Описание не указано')}
${prompt ? `${this.esc(prompt)}