From 86984cfeb1070bc1483ed7feb79cc7af3de54f73 Mon Sep 17 00:00:00 2001 From: egg Date: Wed, 25 Feb 2026 09:02:39 +0800 Subject: [PATCH] feat: dimension pareto cache-based computation, filter propagation, and MSD events cache isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reject History: - Compute dimension pareto (package/type/workflow/workcenter/equipment) from cached DataFrame instead of re-querying Oracle per dimension change - Propagate supplementary filters and trend date selection to dimension pareto - Add staleness tracking to prevent race conditions on rapid dimension switches - Add WORKFLOWNAME to detail and export outputs - Fix button hover visibility with CSS specificity MSD (製程不良追溯分析): - Separate raw events caching from aggregation computation so changing loss_reasons uses EventFetcher per-domain cache (fast) and recomputes aggregation with current filters instead of returning stale cached results - Exclude loss_reasons from MSD seed cache key since seed resolution does not use it, avoiding unnecessary Oracle re-queries - Add suspect context panel, analysis summary, upstream station/spec filters - Add machine bar click drill-down and filtered attribution charts Query Tool: - Support batch container_ids in lot CSV export (history/materials/rejects/holds) Co-Authored-By: Claude Opus 4.6 --- frontend/src/mid-section-defect/App.vue | 108 +++++-- .../components/AnalysisSummary.vue | 163 ++++++++++ .../components/DetailTable.vue | 55 +++- .../components/ParetoChart.vue | 98 +++++- .../components/SuspectContextPanel.vue | 214 +++++++++++++ frontend/src/mid-section-defect/style.css | 4 + frontend/src/reject-history/App.vue | 77 ++++- .../reject-history/components/DetailTable.vue | 4 +- .../components/ParetoSection.vue | 35 ++- frontend/src/reject-history/style.css | 28 +- .../shared-composables/useTraceProgress.js | 3 +- src/mes_dashboard/routes/query_tool_routes.py | 43 +++ .../routes/reject_history_routes.py | 23 +- src/mes_dashboard/routes/trace_routes.py | 47 ++- src/mes_dashboard/services/lineage_engine.py | 11 + .../services/mid_section_defect_service.py | 296 ++++++++++++++++-- .../services/reject_dataset_cache.py | 124 ++++++++ .../services/reject_history_service.py | 109 +++++++ .../sql/query_tool/equipment_recent_jobs.sql | 19 ++ .../sql/reject_history/dimension_pareto.sql | 43 +++ .../sql/reject_history/export.sql | 1 + src/mes_dashboard/sql/reject_history/list.sql | 1 + .../sql/reject_history/performance_daily.sql | 17 + .../reject_history/performance_daily_lot.sql | 17 + tests/test_mid_section_defect_service.py | 111 +++++++ tests/test_query_tool_routes.py | 56 ++++ tests/test_reject_history_routes.py | 43 ++- tests/test_trace_routes.py | 104 +++++- 28 files changed, 1768 insertions(+), 86 deletions(-) create mode 100644 frontend/src/mid-section-defect/components/AnalysisSummary.vue create mode 100644 frontend/src/mid-section-defect/components/SuspectContextPanel.vue create mode 100644 src/mes_dashboard/sql/query_tool/equipment_recent_jobs.sql create mode 100644 src/mes_dashboard/sql/reject_history/dimension_pareto.sql diff --git a/frontend/src/mid-section-defect/App.vue b/frontend/src/mid-section-defect/App.vue index ed9e0bf..1c0eeb7 100644 --- a/frontend/src/mid-section-defect/App.vue +++ b/frontend/src/mid-section-defect/App.vue @@ -10,7 +10,9 @@ import KpiCards from './components/KpiCards.vue'; import MultiSelect from './components/MultiSelect.vue'; import ParetoChart from './components/ParetoChart.vue'; import TrendChart from './components/TrendChart.vue'; +import AnalysisSummary from './components/AnalysisSummary.vue'; import DetailTable from './components/DetailTable.vue'; +import SuspectContextPanel from './components/SuspectContextPanel.vue'; ensureMesApiAvailable(); @@ -170,6 +172,12 @@ const filteredByMachineData = computed(() => { return filtered.length > 0 ? buildMachineChartFromAttribution(filtered) : []; }); +const suspectMachineNames = computed(() => { + const data = filteredByMachineData.value; + if (!Array.isArray(data)) return []; + return data.filter((d) => d.name && d.name !== '其他').map((d) => d.name); +}); + const isForward = computed(() => committedFilters.value.direction === 'forward'); const committedStation = computed(() => { const key = committedFilters.value.station || '測試'; @@ -198,7 +206,25 @@ const eventsAggregation = computed(() => trace.stage_results.events?.aggregation const showAnalysisSkeleton = computed(() => hasQueried.value && loading.querying && !eventsAggregation.value); const showAnalysisCharts = computed(() => hasQueried.value && (Boolean(eventsAggregation.value) || restoredFromCache.value)); -const skeletonChartCount = computed(() => (isForward.value ? 4 : 6)); +const skeletonChartCount = computed(() => (isForward.value ? 4 : 5)); + +const totalAncestorCount = computed(() => trace.stage_results.lineage?.total_ancestor_count || analysisData.value?.total_ancestor_count || 0); + +const summaryQueryParams = computed(() => { + const snap = committedFilters.value; + const params = { + queryMode: snap.queryMode || 'date_range', + startDate: snap.startDate, + endDate: snap.endDate, + lossReasons: snap.lossReasons || [], + }; + if (snap.queryMode === 'container') { + params.containerInputType = snap.containerInputType || 'lot'; + params.resolvedCount = resolutionInfo.value?.resolved_count || 0; + params.notFoundCount = resolutionInfo.value?.not_found?.length || 0; + } + return params; +}); function emptyAnalysisData() { return { @@ -364,6 +390,7 @@ async function loadAnalysis() { analysisData.value = { ...analysisData.value, ...eventsAggregation.value, + total_ancestor_count: trace.stage_results.lineage?.total_ancestor_count || 0, }; } @@ -429,6 +456,21 @@ function exportCsv() { document.body.removeChild(link); } +// Suspect context panel state +const suspectPanelMachine = ref(null); + +function handleMachineBarClick({ name, dataIndex }) { + if (!name || name === '其他') return; + const attribution = analysisData.value?.attribution; + if (!Array.isArray(attribution)) return; + const match = attribution.find( + (rec) => rec.EQUIPMENT_NAME === name, + ); + if (match) { + suspectPanelMachine.value = suspectPanelMachine.value?.EQUIPMENT_NAME === name ? null : match; + } +} + const _abortControllers = new Map(); function createAbortSignal(key = 'default') { const prev = _abortControllers.get(key); @@ -542,6 +584,14 @@ void initPage();
+ +