# Audit Log Manager — Implementation notes (review & debug)

This document describes the **Audit Log Manager** delivered in this repo: database schema, backend recording, admin API, frontend layout, and how to verify end-to-end (Postgres, API, MinIO).

## 1. Database

- **Migration:** `be0/migrations/008_audit_events.sql`
  - Creates PostgreSQL enum **`audit_action`** and table **`audit_events`** (append-only by convention).
  - Indexes: actor+time, entity+time, action+time, GIN on `metadata`.

**Apply** (example against local Docker Postgres — adjust connection):

```bash
psql "$INITIATIVE_DATABASE_URL" -f be0/migrations/008_audit_events.sql
```

(`INITIATIVE_DATABASE_URL` is typically `postgresql://…` for `psql`; the app uses SQLAlchemy async URL like `postgresql+asyncpg://…`.)

Docker Compose mounts `008_audit_events.sql` into `docker-entrypoint-initdb.d` for **new** databases only. **Existing** `initiative_pg_data` volumes still need the `psql -f …/008_audit_events.sql` step once.

## 2. Backend model & helpers

| Piece | Path |
|-------|------|
| ORM model | `be0/src/initiative_db/models.py` — `AuditEvent` |
| Helpers | `be0/src/audit.py` — `record_audit`, `persist_audit_standalone`, `resolve_actor_fields` |

- **`await record_audit(session, …)`** — insert via a **SAVEPOINT**: if `audit_events` is missing (migration not applied), logs a warning and **does not roll back** the parent transaction (so login/register still succeed).
- **`await persist_audit_standalone(…)`** — own session + `commit`; same missing-table handling without raising.

## 3. Where events are written

| Area | Actions / entity_type | Notes |
|------|----------------------|--------|
| Auth | `register` → `create` / `user` | Same transaction as user + roles |
| Auth | OK login → `login` / `auth` | |
| Auth | Bad login → `login_failed` / `auth` | **Standalone** insert after failed credentials |
| Auth | Valid Bearer logout → `logout` / `auth` | **Standalone**; skipped if JWT does not decode |
| Auth | Profile patch → `update` / `user` | Before/after: `fullName`, `phone` |
| Auth | Password change → `update` / `user` | Snapshots `{ password: "[redacted|changed]" }` only |
| Drafts autosave | `update` / `application_draft` | When `owner_id` is known (authenticated saves) |
| MinIO evidence | `create`/`update`/`delete` / `application_evidence` | After DB + object write/delete in `main.py` |
| Staff evidence review | `update` / `application_evidence_review` | |

Admin adjudication (`application_admin_results.py`):

- `create` / `application_admin_result`
- `update` / …
- `upsert` → `create` or `update` depending on prior row
- `delete` — requires **`actor_user_id`**; `delete_admin_result(..., actor_user_id=…)` called from `main.py`

Legacy table **`audit_log`** (draft telemetry) remains unchanged.

## 4. Admin HTTP API

- **JWT decode** for admin routes (`decode_bearer_token`, `decode_access_token_user_id`) lives in **`be0/src/auth_jwt.py`** so audit routes do not pull in Argon2 at import time.
- **Router:** `be0/src/admin_audit_routes.py`, mounted in `main.py` as **`/api/v1/admin`**.
- **`GET /api/v1/admin/audit`** — list (admin JWT only).
  - Default window: **now − 7 days** → **now** if `from` / `to` omitted.
  - Query params: `from`, `to`, `actor_user_id`, `actor_email` **(exact, lowercased)** `entity_type`, `entity_id`, `action` (comma-separated), `request_id`, `page`, `page_size` (≤ 100), `sort` (`occurred_at:asc|desc`).
- **`GET /api/v1/admin/audit/{id}`** — full row incl. `before` / `after` JSON.

Non-admin receives **403**.

## 5. Frontend

| Layer | Path |
|-------|------|
| Shared types + API client | `fe0/src/audit/` |
| Admin UI | `fe0/src/admin/audit/` — `AuditLogManagerPage`, filters, table, detail **Sheet** |
| Applicant-side copy (reuse) | `fe0/src/applicant/audit/actionLabels.ts` — Vietnamese labels for actions |
| Nav | **`fe0/src/components/admin/DashboardSidebar.tsx`** (+ duplicate `DashboardSidebar.tsx`) — **« Nhật ký Audit »** → `/dashboard/admin/audit` |
| Routes | `fe0/src/App.tsx` — **`admin/audit` before `admin`** nested under dashboard |

Detail panel loads **`/api/v1/admin/audit/{id}`** and runs **`microdiff`** on the client only (see `audit/jsonMicrodiffLines.ts`), per architecture doc.

Dependency: **`microdiff`** (added to `fe0/package.json`).

## 6. Debugging checklist

1. **500 on `/auth/login` after adding audit** — Postgres log / SQLAlchemy showed `relation "audit_events" does not exist`: apply migration `008`. Until then, **`record_audit`** skips the insert and auth still works.
2. **`ECONNREFUSED` from `fe0` to `:4402`** — API not listening yet (`be0` still on NLTK pip, or old entrypoint failed on `ollama: command not found`). Rebuild `be0`; entrypoint now **skips Ollama** when the binary is absent. Wait for `Uvicorn running on http://0.0.0.0:4402` before logging in.
3. **Table missing / Admin audit 503** — run migration `008_audit_events.sql`.
4. **Empty list but events expected** — check `from`/`to`; default is last **7 days** on the API.
5. **403 on `/api/v1/admin/audit`** — JWT must include **`admin`** in `roles` (same as other admin APIs).
6. **Failed-login rows missing** — must use **`persist_audit_standalone`** path; if Postgres URL wrong, standalone insert is skipped (`is_postgres_enabled()`).
7. **MinIO uploads without audit rows** — evidence handler must reach `upsert_evidence_artifact` **after** successful `s3.upload`; failures before that do not audit.
8. **Comment typo broke build once** — `formatAuditTime.ts` JSDoc used invalid `**/”` sequence; stick to **` */`** closing block comments.

## 7. Suggested verification flow

1. Apply migration → restart API.
2. Register / login → see `create`/`login` rows (filter `action=login,create`).
3. Save applicant draft tab → `application_draft`.
4. Upload evidence with MinIO configured → `application_evidence` with `metadata.minioBucket`.
5. Open **`/dashboard/admin/audit`** as admin → pagination + row click opens JSON + microdiff.

## 8. References

- Spec: `assets/docs/audit-log-implementation.md`
- Product plan: `docs/audit-log-manager-plan.md`
