Files
service-finder/plans/finance_module_recovery_blueprint_2026-07-26.md
2026-07-27 08:39:18 +00:00

214 lines
10 KiB
Markdown

# 🔴 Finance Module Recovery Blueprint — Root Cause Analysis & Recovery Plan
**Date:** 2026-07-26
**Author:** Architect Mode
**Status:** Pending Code Mode Execution
**Priority:** CRITICAL — Multiple systemic bugs across Frontend & Backend
---
## STEP 1: ARCHITECTURAL COMPARISON — Dashboard vs. Finance
### 1.1 Layout Wrapper Comparison
| Aspect | DashboardView (Working) | FinanceMainView (Broken) |
|--------|-------------------------|--------------------------|
| **Parent Layout** | PrivateLayout — provides BaseHeader with hamburger menu | FinanceLayout — provides same BaseHeader but with HeaderBackButton |
| **Background** | Self-contained: `fixed inset-0` + `bg-[url('@/assets/garage-bg.png')]` in the component itself | Duplicated in layout: same `fixed inset-0` background in FinanceLayout |
| **Header Teleport** | NO Teleport usage — DashboardView owns its own header via the background div | USES Teleport: FinanceMainView teleports `<span>` into `#header-teleport-target` |
| **Router Nesting** | Dash is a child of PrivateLayout | Finance is a child of FinanceLayout |
### 1.2 Card Component Comparison
| Aspect | Working Dashboard Cards | Broken Finance Cards |
|--------|------------------------|---------------------|
| **Root Element** | Single `<div>` with CSS classes directly on the card (e.g., MyVehiclesCard.vue:2-4) | Same pattern: plain `<div>` with identical CSS ✅ |
| **Click Handler** | Invisible overlay `<div>` with `@click` (e.g., MyVehiclesCard.vue:6-9) | Same pattern: invisible overlay with `@click` router push ✅ |
| **Data Binding** | Uses Pinia stores (financeStore, vehicleStore, authStore) | Same Pinia stores used ✅ |
| **Component Import** | Imports card components from dashboard/ | Standalone views — no card components imported, HTML inline |
### 1.3 Key Architectural Difference
**The only structural difference is the Teleport usage.** The working `DashboardView` uses `<Teleport to="body">` ONLY for its Flip Modal overlay, which is always available in the DOM. The broken `FinanceMainView` uses `<Teleport to="#header-teleport-target">` — a target that is conditionally rendered inside `BaseHeader`, which itself is nested inside `FinanceLayout`.
---
## STEP 2: ROOT CAUSE ANALYSIS OF 4 KNOWN BUGS
### 🔴 BUG 1: Teleport Freeze — `Failed to locate Teleport target with selector "#header-teleport-target"`
**Root Cause:** Vue Router lifecycle timing mismatch between Teleport mount/unmount and DOM availability.
**Evidence:**
- `FinanceMainView.vue:38-40` uses `<Teleport to="#header-teleport-target">` to inject a page title
- The target `#header-teleport-target` is defined in `BaseHeader.vue:15`
- `BaseHeader` is rendered inside `FinanceLayout.vue:35`
**Failure scenario:**
1. User navigates FROM `/finance` TO `/dashboard`
2. Vue Router starts unmounting the `/finance` route tree: FinanceMainView → FinanceLayout → BaseHeader
3. FinanceMainView tries to unmount its Teleported `<span>` from `#header-teleport-target`
4. BUT BaseHeader (which contains the target) has already been destroyed or is in the process of being destroyed
5. Vue throws: `Failed to locate Teleport target with selector "#header-teleport-target"`
6. The app freezes because the Teleport cleanup throws an unhandled error that halts the transition
**The same issue can happen on MOUNT:** if FinanceMainView renders before BaseHeader finishes mounting, the Teleport target doesn't exist yet.
**Contrast with DashboardView:** The working `DashboardView:101` teleports to `"body"` — which **always** exists in the DOM, regardless of component lifecycle.
### 🔴 BUG 2: Component Resolution Error — `Failed to resolve component: TechDataTab`
**Root Cause:** Missing import statement in `VehicleDetailModal.vue`.
**Evidence:**
- `VehicleDetailModal.vue:284` uses `<TechDataTab />` in its template
- The `<script setup>` section (lines 892-903) imports: `VehiclePlateBadge`, `ProviderAutocomplete`, `ProviderQuickAddModal` — but **NOT** `TechDataTab`
- `VehicleDetailsView.vue:112` correctly imports `TechDataTab from '../../components/vehicles/tabs/TechDataTab.vue'`
- This is a copy-paste bug: the `<TechDataTab />` usage was copied from VehicleDetailsView but the import was not
**Failure scenario:**
1. User opens VehicleDetailModal and clicks the "Műszaki adatok" tab
2. Vue tries to render `<TechDataTab />` but cannot resolve the component
3. Vue emits warning: `[Vue warn]: Failed to resolve component: TechDataTab`
4. The tab shows blank/empty content
**Note:** This bug is NOT caused by the finance module changes — it's a pre-existing bug in VehicleDetailModal.vue.
### 🟡 BUG 3: Fragment/Emit Warning — `Extraneous non-emits event listeners (deleted)`
**Root Cause:** `DashboardView.vue:169` listens for `@deleted` but the current `VehicleFormModal.vue` does not declare or emit `deleted`.
**Evidence:**
- `DashboardView.vue:164-170`: `<VehicleFormModal @deleted="onVehicleDeleted" />`
- `VehicleFormModal.vue:1778-1782`:
```ts
const emit = defineEmits<{
(e: 'close'): void
(e: 'saved'): void
// NO "deleted" event declared!
}>()
```
**Failure scenario:**
1. Vue 3 sees `@deleted` on the component but no matching emit declaration
2. Vue tries to fall through and treat it as a native DOM event on the root element
3. Since VehicleFormModal's root is `<Teleport>` (a fragment-like component), Vue cannot attach native DOM listeners
4. Vue warns: `Extraneous non-emits event listeners (deleted) were passed to component`
### 🔴 BUG 4: Backend API Crash — HTTP 500 on `GET /api/v1/providers/categories`
**Root Cause:** The backend endpoint fails with an unhandled exception during ExpertiseTag query or ExpertiseCategoryOut serialization.
**Evidence:**
- `providers.py:149-183`: The `/categories` endpoint queries ExpertiseTag and constructs ExpertiseCategoryOut objects
- The endpoint wraps everything in try/except and returns HTTP 500 on any exception
- Git status shows modified schema files — possible schema mismatch
**Required diagnostic:** Run `docker compose logs --tail 100 sf_api` after reproducing the 500 error.
---
## STEP 3: THE RECOVERY BLUEPRINT
### Phase 1: Stabilize the Frontend
#### Fix 1.1: Teleport Target Lifecycle (BUG 1)
- **File:** `frontend_app/src/views/FinanceMainView.vue`
- **Solution:** Wrap Teleport in a v-if guard tied to onMounted/onBeforeUnmount lifecycle:
```html
<Teleport v-if="teleportReady" to="#header-teleport-target">
```
- **Also apply to:** FinanceWalletsView, FinancePackagesView, FinanceTransactionsView
#### Fix 1.2: Missing TechDataTab Import (BUG 2)
- **File:** `frontend_app/src/components/vehicle/VehicleDetailModal.vue`
- **Action:** Add `import TechDataTab from '../vehicles/tabs/TechDataTab.vue'` after line 903
#### Fix 1.3: Fragment Emit Warning (BUG 3)
- **File:** `frontend_app/src/components/dashboard/VehicleFormModal.vue`
- **Action:** Add `(e: 'deleted'): void` to defineEmits and emit on deletion
### Phase 2: Diagnose & Fix the Backend
#### Fix 2.1: Backend 500 on providers/categories (BUG 4)
- Restart sf_api container and capture live logs
- Trigger the endpoint and analyze the Python traceback
- Fix based on actual error (model/schema/DB issue)
---
## STEP 4: CODE MODE HAND-OFF INSTRUCTIONS
### Task 1: Fix Teleport Lifecycle (BUG 1) — CRITICAL
1. Open: `frontend_app/src/views/FinanceMainView.vue`
2. Add `ref` and `onBeforeUnmount` to vue imports (line 237)
3. Add: `const teleportReady = ref(false)`
4. In onMounted, add: `teleportReady.value = true`
5. Add: `onBeforeUnmount(() => { teleportReady.value = false })`
6. Change line 38: `<Teleport v-if="teleportReady" to="#header-teleport-target">`
7. Apply same pattern to: `FinanceWalletsView.vue`, `FinancePackagesView.vue`, `FinanceTransactionsView.vue`
### Task 2: Fix Missing TechDataTab Import (BUG 2) — HIGH
1. Open: `frontend_app/src/components/vehicle/VehicleDetailModal.vue`
2. After line 903, add:
```ts
import TechDataTab from '../vehicles/tabs/TechDataTab.vue'
```
### Task 3: Fix Fragment Emit Warning (BUG 3) — MEDIUM
1. Open: `frontend_app/src/components/dashboard/VehicleFormModal.vue`
2. At line 1778-1782, add `(e: 'deleted'): void` to defineEmits
3. Find delete/archive handler, add `emit('deleted')` after successful operation
### Task 4: Diagnose Backend 500 (BUG 4) — HIGH
1. Run: `docker compose restart sf_api`
2. Run diagnostic query:
```bash
docker compose exec sf_api python3 -c "
import asyncio
from app.database import async_session
from sqlalchemy import select, text
from app.models.data.expertise_tag import ExpertiseTag
async def main():
async with async_session() as db:
stmt = select(ExpertiseTag).order_by(ExpertiseTag.level, ExpertiseTag.id).limit(10)
result = await db.execute(stmt)
tags = result.scalars().all()
print(f'Query OK: {len(tags)} tags')
asyncio.run(main())
"
```
3. Based on output, fix providers.py or the ExpertiseTag model
### Task 5: Verify All Fixes — MANDATORY
1. Frontend build: `cd frontend_app && npm run build 2>&1 | tail -20`
2. Backend imports: `docker compose exec sf_api python3 -c "from app.main import app; print('OK')"`
3. Navigate `/finance` → all 4 cards → `/dashboard` — verify no freeze
4. VehicleDetailModal → "Műszaki adatok" tab — verify renders
---
## 📋 Summary Table
| # | Bug | Severity | Root Cause | File(s) to Fix |
|---|-----|----------|------------|-----------------|
| 1 | Teleport Freeze | CRITICAL | Teleport target lifecycle mismatch | FinanceMainView.vue:38 + 3 sub-views |
| 2 | TechDataTab not found | HIGH | Missing import | VehicleDetailModal.vue:284 |
| 3 | Fragment emit warning | MEDIUM | Missing emit declaration | VehicleFormModal.vue:1778 |
| 4 | Backend 500 error | HIGH | Unhandled exception in /providers/categories | providers.py:149-183 (needs live diagnosis) |
## ⚠️ DO NOT MODIFY
The following files are working correctly and MUST NOT be changed:
- `DashboardView.vue` — working dashboard
- `PrivateLayout.vue` — working layout
- `BaseHeader.vue` — working header
- `FinancialCard.vue` — working card
- `MyVehiclesCard.vue` — working card
- `router/index.ts` — correct route configuration