---
phase: "06.2"
plan: "04"
type: execute
wave: 2
depends_on:
  - "06.2-02"
  - "06.2-03"
files_modified:
  - backend/api/audit.py
  - frontend/src/components/admin/AuditLogTab.vue
  - frontend/src/api/client.js
  - backend/tests/test_audit.py
autonomous: true
requirements:
  - ADMIN-06
must_haves:
  truths:
    - "Audit log JSON viewer returns user_handle and actor_handle alongside user_id and actor_id"
    - "GET /api/admin/audit-log?user_handle=X filters to entries for that user"
    - "GET /api/admin/audit-log?user_handle=nonexistent returns empty items list, not 422"
    - "CSV export button in AuditLogTab downloads a file via fetch+Blob (not window.location.href)"
    - "GET /api/admin/audit-log/daily-exports returns sorted list of available export dates"
    - "GET /api/admin/audit-log/daily-exports/{date} streams the CSV for that date"
    - "Daily exports section in AuditLogTab shows date dropdown + Download button"
    - "Date path parameter validated against YYYY-MM-DD regex before MinIO key construction"
  artifacts:
    - path: "backend/api/audit.py"
      provides: "handle-enriched query; user_handle filter; two daily-export endpoints"
      contains: "_audit_to_dict_with_handles"
    - path: "frontend/src/api/client.js"
      provides: "adminExportAuditLogCsv(), adminListDailyExports(), adminDownloadDailyExport()"
      contains: "adminExportAuditLogCsv"
    - path: "frontend/src/components/admin/AuditLogTab.vue"
      provides: "fixed exportCsv(), daily exports section, user_handle filter label"
      contains: "Daily exports"
  key_links:
    - from: "backend/api/audit.py list_audit_log"
      to: "User table (aliased twice)"
      via: "outerjoin on user_id and actor_id FKs"
      pattern: "outerjoin.*UserSubject|outerjoin.*UserActor"
    - from: "backend/api/audit.py list_daily_exports"
      to: "MinIO audit-logs bucket"
      via: "asyncio.to_thread(_list)"
      pattern: "asyncio.to_thread"
    - from: "frontend/src/components/admin/AuditLogTab.vue:exportCsv"
      to: "adminExportAuditLogCsv() in client.js"
      via: "fetch() + Blob URL — no window.location.href"
      pattern: "adminExportAuditLogCsv"
---

<objective>
Close the ADMIN-06 gaps in a single vertical slice: user handles in audit log responses, handle-based filter, fixed CSV export download, and a new daily-export listing + download UI.

Purpose: Admins can now see who performed actions by name (not UUID), filter by handle without 422 errors, download exports that actually arrive (not a 401 from window.location.href), and access the Celery-generated daily export files from the admin panel.

Output: Modified audit.py (handle JOIN, user_handle filter, two new endpoints), modified AuditLogTab.vue (filter label, fetch+Blob exportCsv, daily-export section), new client.js functions, five promoted test stubs.
</objective>

<execution_context>
@/Users/nik/Documents/Progamming/document_scanner/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-RESEARCH.md
@/Users/nik/Documents/Progamming/document_scanner/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-UI-SPEC.md
</execution_context>

<context>
@/Users/nik/Documents/Progamming/document_scanner/.planning/ROADMAP.md
@/Users/nik/Documents/Progamming/document_scanner/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-CONTEXT.md

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->

From backend/api/audit.py (current state):
  def _audit_to_dict(entry: AuditLog) -> dict:
      # Returns: id, event_type, user_id, actor_id, resource_id, ip_address, metadata_, created_at
      # Does NOT return user_handle or actor_handle

  def _build_filtered_query(start, end, user_id: Optional[uuid.UUID], event_type):
      # Accepts user_id as UUID type — FastAPI validates this via Query(Optional[uuid.UUID])
      # This type annotation causes FastAPI to 422 on non-UUID strings

  @router.get("/audit-log")
  async def list_audit_log(
      user_id: Optional[uuid.UUID] = Query(default=None),  # BUG: must change to Optional[str]
      ...
  )

  @router.get("/audit-log/export")
  async def export_audit_log(
      user_id: Optional[uuid.UUID] = Query(default=None),  # BUG: same fix needed
      ...
  )
  # Both endpoints must be updated to accept user_handle: Optional[str]

From backend/db/models.py (User model — key fields):
  User.id: UUID
  User.handle: str (unique, indexed)

From backend/tasks/audit_tasks.py line 79:
  key = f"audit-logs/{yesterday.isoformat()}.csv"
  # MinIO bucket: "audit-logs"
  # Key pattern: "audit-logs/YYYY-MM-DD.csv"

From backend/storage/__init__.py:
  def get_storage_backend() -> StorageBackend:
      # Returns MinIOBackend; has ._client attribute (Minio SDK instance)

From backend/storage/minio_backend.py:
  # _client: Minio SDK instance
  # _client.list_objects(bucket, prefix, recursive) → synchronous iterator
  # _client.get_object(bucket, key) → response with .read() and .release_conn()

From frontend/src/api/client.js (existing patterns):
  # request() wrapper: always calls res.json() — NOT for CSV responses
  # fetchDocumentContent() at lines 399-428: raw fetch() pattern with Authorization header
  # export async function fetchDocumentContent(docId, options = {}) { ... }

From frontend/src/components/admin/AuditLogTab.vue (current state):
  # filters reactive object: { start, end, user_id, event_type }
  # exportCsv() at lines 185-192: uses window.location.href (broken)
  # fetchLog() sends user_id: filters.user_id to adminListAuditLog()
  # Table renders: entry.user_handle || entry.user_id || '—'  (line 89 — already expects handle)

From .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-UI-SPEC.md:
  C-4: Daily Exports Section — below pagination block, border-t separator
  C-5: User filter label change from "User" to "User handle"
  Copywriting: section label "Daily exports", dropdown label "Select date", button "Download"
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Backend — handle enrichment, user_handle filter, two daily-export endpoints</name>
  <files>backend/api/audit.py, backend/tests/test_audit.py</files>
  <read_first>
    - backend/api/audit.py — read the full file; understand _audit_to_dict(), _build_filtered_query(), both existing endpoints and their exact Query parameter signatures; understand how both endpoints share _build_filtered_query
    - backend/db/models.py — search for "class User" and "class AuditLog" to confirm handle field and user_id/actor_id FK field names
    - backend/storage/__init__.py — read lines 32-50 (get_storage_backend factory) to understand how to get the MinIOBackend instance for the daily-export endpoints; confirm _client attribute
    - backend/tasks/audit_tasks.py — read lines 78-86 to confirm the MinIO bucket name ("audit-logs") and key pattern ("audit-logs/YYYY-MM-DD.csv")
    - backend/tests/test_audit.py — read the full file to understand _seed_audit helper, admin_user fixture, and existing test patterns before promoting stubs
    - .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-RESEARCH.md — Pattern 3 (aliased double-JOIN), Pattern 4 (handle-to-UUID resolution), Pattern 6 (list_objects), Pattern 7 (daily export streaming), Pitfall 4 (COUNT query breaks after JOIN), Pitfall 6 (date regex), Pitfall 7 (both endpoints must use enriched function)
  </read_first>
  <behavior>
    - test_audit_log_includes_user_handle: Seed an audit entry for admin_user. GET /api/admin/audit-log. Assert each item in items has keys "user_handle" and "actor_handle". Assert the first item's user_handle matches admin_user["user"].handle (not None for a seeded entry).
    - test_audit_log_filter_by_handle: Seed one entry for admin_user. Seed one entry for a second distinct user. GET /api/admin/audit-log?user_handle={admin_user.handle}. Assert items contains only entries matching admin_user (user_handle == admin_user.handle). Seeded second entry must not appear.
    - test_audit_log_filter_unknown_handle: GET /api/admin/audit-log?user_handle=definitely_does_not_exist. Assert status 200. Assert response body items == []. Assert total == 0. Assert no 422 error.
    - test_daily_exports_list: Mock MinIOBackend._client.list_objects to return fake objects (or patch get_storage_backend and its _client). GET /api/admin/audit-log/daily-exports. Assert status 200. Assert response has "items" key. Items sorted descending by date.
    - test_daily_export_download: Mock MinIOBackend._client.get_object to return fake CSV bytes. GET /api/admin/audit-log/daily-exports/2026-05-30. Assert status 200. Assert Content-Type: text/csv. Assert Content-Disposition header contains "2026-05-30". Also test GET /api/admin/audit-log/daily-exports/invalid-date returns 404.
  </behavior>
  <action>
Make these changes to backend/api/audit.py:

CHANGE 1 — Add SQLAlchemy aliased imports and User import check:
Add `from sqlalchemy.orm import aliased` to the imports if not already present. Confirm `User` is already imported from `db.models`.

CHANGE 2 — New helper _audit_to_dict_with_handles():
Add a new function `_audit_to_dict_with_handles(entry: AuditLog, user_handle: Optional[str], actor_handle: Optional[str]) -> dict` that returns the same dict as `_audit_to_dict(entry)` PLUS two additional keys: `"user_handle": user_handle or None` and `"actor_handle": actor_handle or None`. Do NOT remove or rename `_audit_to_dict` — preserve it as a fallback.

CHANGE 3 — New query builder _build_filtered_query_with_handles():
Add function `_build_filtered_query_with_handles(start, end, user_uuid, event_type)` that builds a multi-column select:
```
UserSubject = aliased(User)
UserActor   = aliased(User)
stmt = (
    select(AuditLog, UserSubject.handle.label("user_handle"), UserActor.handle.label("actor_handle"))
    .outerjoin(UserSubject, UserSubject.id == AuditLog.user_id)
    .outerjoin(UserActor,   UserActor.id   == AuditLog.actor_id)
    .order_by(AuditLog.created_at.desc())
)
```
Apply the same start/end/user_uuid/event_type filters as the original `_build_filtered_query`. Return the statement. This is a standalone function, NOT replacing `_build_filtered_query` (the old function stays for the count query — see Pitfall 4).

CHANGE 4 — Update list_audit_log endpoint:
Change `user_id: Optional[uuid.UUID] = Query(default=None)` to `user_handle: Optional[str] = Query(default=None)`.

Add handle-to-UUID resolution logic before executing the main query (Pattern 4 from RESEARCH.md):
```python
user_uuid: Optional[uuid.UUID] = None
if user_handle:
    result = await session.execute(select(User.id).where(User.handle == user_handle))
    uid = result.scalar_one_or_none()
    if uid is None:
        return {"items": [], "total": 0, "page": page, "per_page": per_page}
    user_uuid = uid
```

For the count query, use the ORIGINAL `_build_filtered_query(start, end, user_uuid, event_type)` to avoid the COUNT subquery problem (Pitfall 4). Count query is unchanged.

For the data query, use `_build_filtered_query_with_handles(start, end, user_uuid, event_type)`. Add `.limit(per_page).offset((page - 1) * per_page)`. Execute. Iterate `result.all()` as tuples: `for row in rows: entry, user_handle_val, actor_handle_val = row[0], row[1], row[2]`. Build each item with `_audit_to_dict_with_handles(entry, user_handle_val, actor_handle_val)`.

CHANGE 5 — Update export_audit_log endpoint:
Apply the same user_handle→user_uuid resolution (identical block as above). Use `_build_filtered_query_with_handles` for the data query. Iterate rows as tuples. Use `_audit_to_dict_with_handles` for CSV serialization. Add `"user_handle"` and `"actor_handle"` to the `fields` list for the CSV DictWriter. This satisfies Pitfall 7 (both endpoints must use enriched function).

CHANGE 6 — Add two new endpoints for daily exports:
Before the existing endpoints, add necessary imports: `import asyncio`, `import re`. The `StreamingResponse` import should already be present.

Add endpoint `@router.get("/audit-log/daily-exports")`:
- Auth: `_admin: User = Depends(get_current_admin)`
- No session param needed (MinIO call only)
- Body: get the MinIO backend via `from storage import get_storage_backend; from storage.minio_backend import MinIOBackend; backend = get_storage_backend()`. If not MinIOBackend, return `{"items": []}`.
- Define inner `_list() -> list[dict]` function (synchronous) that calls `backend._client.list_objects("audit-logs", prefix="audit-logs/", recursive=False)`, iterates objects, filters `.endswith(".csv")`, extracts date from `obj.object_name.removeprefix("audit-logs/").removesuffix(".csv")`, builds `{"date": date_str, "key": obj.object_name}`, sorts by date descending.
- Execute: `items = await asyncio.to_thread(_list)`
- Return `{"items": items}`

Add endpoint `@router.get("/audit-log/daily-exports/{date}")`:
- Auth: `_admin: User = Depends(get_current_admin)`
- Path param: `date: str`
- Date validation (Pitfall 6 / D-16): `if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", date): raise HTTPException(404, "Invalid date format")`
- Get backend, construct key = `f"audit-logs/{date}.csv"`
- Define inner `_get() -> bytes` (synchronous): `response = backend._client.get_object("audit-logs", key); try: return response.read(); finally: response.close(); response.release_conn()`
- Execute: wrap in `try: csv_bytes = await asyncio.to_thread(_get); except Exception: raise HTTPException(404, "Export not found")`
- Return `StreamingResponse(iter([csv_bytes]), media_type="text/csv", headers={"Content-Disposition": f'attachment; filename="audit-{date}.csv"'})`

CRITICAL: The two new endpoints must be placed BEFORE the existing `@router.get("/audit-log/export")` and `@router.get("/audit-log")` in the file, because FastAPI routes are matched in registration order. The path `/audit-log/daily-exports` is more specific than `/audit-log` and must be registered first. Or, at minimum, place them before the `@router.get("/audit-log")` GET handler.

Then in backend/tests/test_audit.py: promote all five xfail stubs. Use `unittest.mock.patch` to mock `storage.get_storage_backend` for the daily-export endpoint tests, returning a mock MinIOBackend with a `_client` mock.
  </action>
  <verify>
    <automated>cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest tests/test_audit.py::test_audit_log_includes_user_handle tests/test_audit.py::test_audit_log_filter_by_handle tests/test_audit.py::test_audit_log_filter_unknown_handle tests/test_audit.py::test_daily_exports_list tests/test_audit.py::test_daily_export_download -x -v 2>&1 | tail -25</automated>
  </verify>
  <done>
    - All five promoted tests pass
    - `grep "_audit_to_dict_with_handles" backend/api/audit.py` returns at least 2 matches (definition + both endpoint usages — Pitfall 7)
    - `grep "user_handle" backend/api/audit.py` returns at least 4 matches
    - `grep "daily-exports" backend/api/audit.py` returns 2 matches (two new endpoints)
    - `grep "fullmatch" backend/api/audit.py` returns a match (date regex validation)
    - `pytest tests/test_audit.py -x -q` exits 0
  </done>
</task>

<task type="auto">
  <name>Task 2: Frontend — user_handle filter, fetch+Blob export, daily-export section</name>
  <files>frontend/src/components/admin/AuditLogTab.vue, frontend/src/api/client.js</files>
  <read_first>
    - frontend/src/components/admin/AuditLogTab.vue — read the full file; understand filters reactive object (filters.user_id must become filters.user_handle), fetchLog() which passes params to adminListAuditLog(), exportCsv() (broken window.location.href on lines 185-192), pagination block location (where to add the new daily-export section below it)
    - frontend/src/api/client.js — read lines 395-435 (fetchDocumentContent — the fetch+Blob reference pattern); search for "adminListAuditLog" to find its current implementation; note that request() wrapper always calls res.json() and must NOT be used for CSV responses
    - .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-RESEARCH.md — Pattern 5 (fetch+Blob URL for CSV), Pattern 6 (adminListDailyExports signature), Pattern 7 (adminDownloadDailyExport)
    - .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-UI-SPEC.md — C-4 (daily exports section markup), C-5 (user filter label), Copywriting Contract (section copy), State Inventory (loading/empty/populated states)
  </read_first>
  <action>
Make two file changes:

CHANGE 1 — frontend/src/api/client.js: add three new functions
Follow the exact fetch+Blob pattern from fetchDocumentContent (lines 399-428) — NOT using the request() wrapper.

Add `adminExportAuditLogCsv(params = {})`:
- Import useAuthStore lazily (same pattern as fetchDocumentContent)
- Build URLSearchParams with format=csv; add start, end, event_type if provided; add user_handle if provided (NOT user_id — the backend param is now user_handle)
- Raw fetch to `/api/admin/audit-log/export?${searchParams}` with Authorization Bearer header and credentials: 'include'
- On !res.ok: throw Error(`Export failed: ${res.status}`)
- `const text = await res.text()` (NOT res.json())
- Create Blob([text], { type: 'text/csv' }), URL.createObjectURL, create `<a>` element, set href + download='audit-export.csv', click, URL.revokeObjectURL

Add `adminListDailyExports()`:
- Raw fetch to `/api/admin/audit-log/daily-exports` with Authorization Bearer header
- On !res.ok: throw Error
- Return `await res.json()` — this endpoint returns JSON

Add `adminDownloadDailyExport(date)`:
- Raw fetch to `/api/admin/audit-log/daily-exports/${date}` with Authorization Bearer header and credentials: 'include'
- On !res.ok: throw Error(`Download failed: ${res.status}`)
- `const text = await res.text()`
- Blob + URL.createObjectURL + `<a>` click with download=`audit-${date}.csv` + revokeObjectURL

CHANGE 2 — frontend/src/components/admin/AuditLogTab.vue: three UI changes

CHANGE 2a — User filter label and binding (per D-12, C-5):
In the filters reactive object, rename `user_id: ''` to `user_handle: ''`.
In the fetchLog() function, change `user_id: filters.user_id || undefined` to `user_handle: filters.user_handle || undefined`.
In the template filter bar, change the label text from "User" to "User handle". Change `v-model="filters.user_id"` to `v-model="filters.user_handle"`.
Update adminListAuditLog() call to pass `user_handle` not `user_id` (check the existing call signature in fetchLog).

CHANGE 2b — Fix exportCsv() (per D-13):
Replace the entire body of `function exportCsv()` with an async call to `api.adminExportAuditLogCsv({...})`.  Change the function declaration to `async function exportCsv()`. Pass current filter values: `start: filters.start || undefined, end: filters.end || undefined, user_handle: filters.user_handle || undefined, event_type: filters.event_type || undefined`. Add a ref `exportingCsv` (boolean, default false) and set it true/false around the call. On error: show an alert or set an error ref with "Export failed. Please try again."

CHANGE 2c — Add daily exports section (per D-17, C-4 from UI-SPEC):
Add new reactive state in script setup:
- `dailyExports` ref (Array, default [])
- `loadingExports` ref (boolean, default false)
- `selectedExportDate` ref (string, default '')
- `downloadingExport` ref (boolean, default false)
- `exportsError` ref (string, default null)

Add `loadDailyExports()` async function that calls `await api.adminListDailyExports()` and populates `dailyExports.value` from `data.items`. Set `loadingExports` accordingly. Call `loadDailyExports()` inside `onMounted()` alongside the existing `fetchLog()` call.

Add `downloadDailyExport()` async function that calls `await api.adminDownloadDailyExport(selectedExportDate.value)`. Set `downloadingExport` true/false. On error: set `exportsError.value = "Download failed. Please try again."`.

In the template, add the daily-export section below the pagination block, following C-4 markup from UI-SPEC:
- Section separator: `<div class="border-t border-gray-100 mt-6 pt-6">`
- Section label: `<h3 class="text-sm font-semibold text-gray-700 mb-3">Daily exports</h3>`
- Loading state: `<p v-if="loadingExports" class="text-sm text-gray-400">Loading exports…</p>`
- Empty state: `<p v-else-if="dailyExports.length === 0" class="text-sm text-gray-400 italic">No daily exports available.</p>`
- Controls row (v-else): `<div class="flex items-end gap-3">`
  - `<select v-model="selectedExportDate" class="text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white">`
    - `<option value="" disabled>Choose a date</option>`
    - `<option v-for="exp in dailyExports" :key="exp.date" :value="exp.date">{{ exp.date }}</option>`
  - `<button @click="downloadDailyExport" :disabled="!selectedExportDate || downloadingExport" class="bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-4 py-2 rounded-lg disabled:opacity-50 transition-colors">`
    - Loading spinner inline when downloadingExport (same animate-spin pattern as ShareModal)
    - "Download" text otherwise
- Error display: `<p v-if="exportsError" class="text-xs text-red-600 mt-2">{{ exportsError }}</p>`
  </action>
  <verify>
    <automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && grep -n "adminExportAuditLogCsv\|adminListDailyExports\|adminDownloadDailyExport" src/api/client.js | head -10</automated>
  </verify>
  <done>
    - `grep "adminExportAuditLogCsv" frontend/src/api/client.js` returns a match
    - `grep "adminListDailyExports" frontend/src/api/client.js` returns a match
    - `grep "adminDownloadDailyExport" frontend/src/api/client.js` returns a match
    - `grep "window.location.href" frontend/src/components/admin/AuditLogTab.vue` returns NO match (broken export removed)
    - `grep "Daily exports" frontend/src/components/admin/AuditLogTab.vue` returns a match
    - `grep "User handle" frontend/src/components/admin/AuditLogTab.vue` returns a match
    - `grep "user_handle" frontend/src/components/admin/AuditLogTab.vue` returns at least 2 matches (filter binding + fetchLog param)
    - No build errors: `cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | grep -i "error" | grep -v "^>" | head -10` returns empty
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| browser → GET /api/admin/audit-log/daily-exports/{date} | date path param is user-supplied; must not allow MinIO key injection |
| api/audit.py → MinIO | asyncio.to_thread isolates sync SDK from the async event loop |
| AuditLogTab → /api/admin/audit-log/export | fetch() must carry Bearer header; window.location.href cannot |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-06.2-04-01 | Tampering | Date path parameter injection | mitigate | `re.fullmatch(r"\d{4}-\d{2}-\d{2}", date)` validates before `f"audit-logs/{date}.csv"` key construction — rejects any non-date string including path traversal sequences (Pitfall 6 from RESEARCH.md) |
| T-06.2-04-02 | Elevation of Privilege | Unauthenticated daily-export access | mitigate | Both new endpoints use `_admin: User = Depends(get_current_admin)` — regular users receive 403, unauthenticated receive 401 |
| T-06.2-04-03| Information Disclosure | Audit log CSV token bypass via window.location.href | mitigate | exportCsv() replaced with fetch()+Blob pattern that sends Authorization Bearer header — no unauthenticated CSV download possible |
| T-06.2-04-04 | Information Disclosure | user_handle in audit response leaks PII | accept | handle is already public within the platform (users are identified by handle in sharing UI); admin view of handles is consistent with existing admin privileges |
| T-06.2-04-05 | Denial of Service | list_objects blocking event loop | mitigate | `asyncio.to_thread(_list)` wraps synchronous Minio iterator — event loop is not blocked |
| T-06.2-SC | Tampering | npm/pip/cargo installs | accept | No new packages installed in this plan |
</threat_model>

<verification>
After both tasks complete:

```
cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest tests/test_audit.py -x -q
```

Expected: exits 0, all 9 tests pass (4 pre-existing + 5 promoted).

```
cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest -v 2>&1 | tail -20
```

Expected: zero failures.

Phase gate — full suite:
```
cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest -v 2>&1 | grep -E "passed|failed|error" | tail -5
```

Frontend:
```
cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | grep -i "error" | grep -v "^>" | head -10
```

Security spot-checks:
```
grep "window.location.href" /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/admin/AuditLogTab.vue
# Expected: no output (bug removed)

grep "fullmatch" /Users/nik/Documents/Progamming/document_scanner/backend/api/audit.py
# Expected: matches the date regex line

grep "get_current_admin" /Users/nik/Documents/Progamming/document_scanner/backend/api/audit.py
# Expected: 4 matches (2 existing endpoints + 2 new endpoints)
```
</verification>

<success_criteria>
- Audit log JSON response includes user_handle and actor_handle — confirmed by test_audit_log_includes_user_handle
- user_handle filter returns correct filtered results — confirmed by test_audit_log_filter_by_handle
- Unknown handle returns empty (not 422) — confirmed by test_audit_log_filter_unknown_handle
- Daily export list endpoint returns sorted items — confirmed by test_daily_exports_list
- Daily export download streams CSV with regex-validated date — confirmed by test_daily_export_download
- AuditLogTab exportCsv() uses fetch+Blob (window.location.href removed)
- AuditLogTab user filter labeled "User handle"
- AuditLogTab has Daily exports section with date dropdown and Download button
- All 9 test_audit.py tests pass
- Full pytest suite exits 0
</success_criteria>

<output>
Create `.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-04-SUMMARY.md` when done.
</output>
