Files
sciagent/docs/ADMIN_APPLICATIONS_ADMIN_RESULT_RADICAL_FIX_PLAN.md
Thinh Lam 688fac73e9
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped
sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:38:30 +07:00

9.0 KiB

Radical fix plan: admin applications, admin results, and dashboard consistency

This document captures how the current stack is wired, what went wrong with “save decision → see it in Kết quả đăng ký”, and a phased, radical plan to make the behavior reliable by design—not only by patching one screen.


1. Problem statement (what users experience)

  1. An admin uses Mẫu hồ sơ và Minh chứngDuyệt (xem trước)Xác nhận (AdminStaffReadonlyReviewDialog + upsertAdminApplicationResult).
  2. They expect the submission to appear under Kết quả đăng ký (ConsideredInitiativesListApprovedApplicationsList with lifecycle=decided).
  3. Sometimes nothing changes, or behaviour is confusing, because several independent subsystems must stay aligned: HTTP client semantics, API routes, PostgreSQL initiatives.status, optional file fallback, React Query keys, and optional MinIO (evidence—not the same as admin result).

2. Implementation map (how it fits together)

2.1 Frontend

Piece Role
ConsideredInitiativesList Renders ApprovedApplicationsList with lifecycle="decided" — only rows whose list status is approved or rejected.
ApprovedApplicationsList useQuery(["applications", filters])GET /api/applications with filters including lifecycle.
AdminDocxTemplatePreview Opens AdminStaffReadonlyReviewDialog; confirm calls upsertAdminApplicationResult.
applicationAdminResultApi fetch + create / update; upsert = GET then POST or PUT.
shared/api/client.ts Axios validateStatus: (s) => s < 5004xx responses do not throw unless callers pass an override.

2.2 Backend

Piece Role
POST/PUT/GET/DELETE …/admin-result Persists application_admin_results and sets initiatives.status to approved / rejected (PostgreSQL).
GET /api/applications Primary: list_submitted_applications from Postgres. Fallback: _load_submitted_items() file index if Postgres fails or is disabled.
GET /api/applications/{id} Same pattern: Postgres first, then file index.
MinIO (S3-compatible) Evidence / attachments buckets; not where admin “decision rows” live. Decisions are Postgres; MinIO only matters for evidence flows.

2.3 Data flow (intended)

sequenceDiagram
  participant UI as Admin UI
  participant API as be0 FastAPI
  participant PG as Postgres
  participant List as GET /api/applications

  UI->>API: upsert admin-result (POST or PUT)
  API->>PG: application_admin_results + initiatives.status
  UI->>API: invalidate + refetch applications
  List->>PG: list initiatives + drafts → status approved/rejected
  List-->>UI: decided list

Breakage happens when any step returns “success” without real data, reads from a different backend than writes, or treats HTTP 404 as a valid JSON body.


3. Root causes (systemic, not one-line bugs)

A. Global Axios: 4xx treated as success (validateStatus < 500)

  • For GET /api/.../admin-result with no row, the server returns 404 + { detail: "…" }.
  • With the default client, that resolves instead of rejects.
  • Any code that checks if (!data) fails → truthy object { detail } looks like “existing result”.
  • upsert then chose PUT instead of POST on first save → no row created, silent wrong success possible.

Partial fix already applied: pass axiosSuccessStatusOnly (2xx-only) on all applicationAdminResultApi calls.

Radical fix: see §5.1 — stop relying on opt-in overrides.

B. Client upsert = GET + mutate (race + footguns)

  • Two round trips; duplicated server rules; easy to get wrong if GET semantics change.
  • Prefer idempotent server upsert (PUT with “create or replace” semantics) or POST-only with clear 409 handling.

C. Dual source of truth for application lists (Postgres vs file index)

  • Listing can silently fall back to _load_submitted_items() when Postgres throws.
  • Admin-result writes only hit Postgres.
  • Result: UI can show stale or empty “decided” data while DB was actually updated (or the reverse in dev).

D. React Query key ["applications", filters]

  • Invalidate ["applications"] is correct for TanStack Query partial matching, but any cache/subscription edge case should be covered by tests.

E. MinIO vs admin decision (scope confusion)

  • Fixing “Kết quả đăng ký” does not require MinIO updates.
  • Evidence upload paths are separate; do not conflate in testing or plans.

4. Verification already performed (baseline)

  • Docker: postgres, minio, be0 healthy; GET /api/v1/test OK.
  • Postgres + create_admin_result: initiatives.status and application_admin_results stay in sync when using the Python layer directly.
  • Integration test test_applications_db_integration: one failing test (get_application_by_id fallback sub-… without submissionRecord.id) — suggests ID-resolution edge cases still risky; align with list/GET contract in the same plan.
  • Host Python may lack boto3; validate MinIO from be0 container or install dev deps locally for S3 tests.

5. Radical fixes (options, from smaller to larger)

5.1 Frontend: default to real HTTP semantics (highest leverage)

Goal: No API call “succeeds” with a 404/422 body unless explicitly handled.

Options:

  1. Change default validateStatus to 2xx-only in ApiClient, then globally fix call sites that depended on reading { detail } from a resolved 4xx response (likely few; grep for patterns).
  2. Two clients: apiClientStrict (2xx-only) for CRUD and apiClient legacy only where needed—migrate modules incrementally.
  3. Response interceptor: if status >= 400, reject with unified ApiError (preserves current “no throw on 4xx” idea but never returns res.data as success to .then()).

Acceptance: ESLint rule or CI script: forbid apiClient.get/post/put/delete without explicit validateStatus or wrapper.

5.2 Backend: atomic admin-result upsert

Goal: Single request, no client-side GET-before-POST.

  • Expose PUT /api/applications/{id}/admin-result as idempotent upsert (create or update in one transaction), or document POST as upsert with unique constraint handling.
  • Optionally return updated application row snippet (status, applicationId) so the client can patch cache without listing.

5.3 Backend: single listing source in production

Goal: No silent list fallback when INITIATIVE_DATABASE_URL is set.

  • On Postgres enabled: fail listing with 503 and a clear JSON error instead of falling back to files; or log + metrics + feature flag.
  • Deprecate file index for /api/applications in environments where submissions are always in PG.

5.4 Contract tests (API + FE)

  • pytest/httpx: POST admin-resultGET /api/applications?lifecycle=decided contains that id.
  • Playwright or MSW: Admin flow confirm → list row appears (requires auth fixture).

5.5 Observability

  • Structured logs: application_id, initiative_id, decision, source=postgres|file_fallback.
  • Metrics: applications_list_fallback_total, admin_result_upsert_duration_ms.

Phase Scope Outcome
P0 Keep axiosSuccessStatusOnly on admin-result API; add one e2e/API test: upsert → decided list. Regression guard.
P1 Introduce strict HTTP default or interceptor (§5.1); fix broken call sites. Class of bugs eliminated.
P2 Backend idempotent PUT upsert; simplify client to single call. Fewer races, simpler mental model.
P3 Remove or gate file fallback for /api/applications when PG is configured. Align list with admin writes.
P4 Fix failing DB test for submissionRecord.id omission; document canonical applicationId rules. Predictable IDs end-to-end.

7. Out of scope / non-goals

  • MinIO consistency for “admin approve” — wrong layer unless the feature explicitly writes objects on decision.
  • Council flow (saveCouncilReviewOutcome / local storage) — separate product path; only mention if merging with admin outcomes.

8. Decision log (fill in as you implement)

Date Decision Rationale

References (code)

  • fe0/src/shared/api/client.ts — global validateStatus.
  • fe0/src/lib/applicationReviewApi.tsaxiosSuccessStatusOnly.
  • fe0/src/lib/applicationAdminResultApi.ts — admin-result CRUD + upsert.
  • fe0/src/components/admin/result/ConsideredInitiativesList.tsx — decided list entry point.
  • be0/src/initiative_db/application_admin_results.py — DB writes + initiative.status.
  • be0/main.pylist_applications Postgres vs file fallback.
  • be0/tests/test_applications_db_integration.py — Postgres integration tests.