# Research-project proposals + PI cockpit (frontend_investigator) _Added 2026-06-14. A new parallel domain (independent of the sáng-kiến / initiative flow): a Principal Investigator submits a research-project proposal (Thuyết minh đề tài, Mẫu III.06-TM.ĐTUD), an admin approves it, then the PI manages the project via a "cockpit" (members / datasets / models / assets / tiến độ + audit)._ ## Shape (one-line) PI app `frontend_investigator` → fill proposal → submit → **admin approves in `frontend_admin`** → cockpit unlocks (auto-seeded from the proposal) → PI manages entities. Owner + admin authz; every mutation writes an append-only audit row. ## Backend (`be0/`) - **Migration `016_research_projects.sql`** — 7 tables: - `research_projects` — the aggregate root. **The proposal row IS the project** across its lifecycle (`status`: `draft → submitted → approved | rejected`). `content JSONB` holds the whole proposal form blob; a few **extracted scalars** (`title/level/pi_name/period_months/budget_total`) are columns for listing/overview. `code` (mã số) is null until approved. Review fields: `reviewed_by/at/note`. - `research_project_{members,datasets,models,assets,milestones}` — normalized child entity tables (FK → project, `ON DELETE CASCADE`), columns mirror the cockpit artifact's fields. Milestones use `start_period`/`end_period` (the JSON keys `start`/`end` map to them). - `research_project_audit` — append-only (BIGSERIAL; actor + action + subject + detail). - Registered in **4 places** (the stage-together rule): both compose files' `docker-entrypoint-initdb.d` + `apply_initiative_migrations.py` (`_needs_research_projects_migration` + guarded block) + the `.sql`. - **`src/research_routes.py`** — router mounted at `/api/v1/research/*` (now the **5th** extracted router: auth · admin-audit · admin-user-profiles · templates · **research**). Endpoints: - Proposals: `POST/GET /projects`, `GET/PUT/DELETE /projects/{id}`, `POST /projects/{id}/submit`, `POST /projects/{id}/approve` (admin), `POST /projects/{id}/reject` (admin), `GET /projects/{id}/audit`. - Cockpit entities: `GET/POST /projects/{id}/entities/{entity}`, `PUT/DELETE …/{item_id}`, `GET /projects/{id}/cockpit` (one-shot bundle: project + 5 entity lists + recent audit). - **Generic config-driven CRUD** (`_ENTITY_CONFIG`): one whitelist `_apply_fields` maps json_key→column for all 5 entities (no column injection — sensitive cols absent from configs). - **Authz (v1) = owner OR platform admin.** Read hides others' rows with 404; submit/update are owner-only + draft-only; approve/reject are admin-only + submitted-only; entity mutations require the project to be **approved** (409 otherwise = "cockpit unlocks on approval"). `credential_version` is enforced by the global `main.py` middleware (routers use bare `decode_access_token_user_id`). - **Seed-on-approve** — `_seed_cockpit_from_proposal` populates members (chủ nhiệm + thư ký + thành viên) + milestones (tiến độ) from `content` so the cockpit opens "according to the proposal". Best-effort + idempotent. - Auth model: a **PI = any authenticated user who owns a project** (no new system role). Admin via the existing allow-list mechanism (`AUTH_ADMIN_EMAILS` env, else the built-in default list in `auth_api.py`). ## Frontend - **`frontend_investigator/`** — new Vite/React/TS app cloned from `frontend_user` (same design system by construction: `index.css` + tailwind tokens, Inter/Merriweather, shared shadcn primitives, auth flow, dashboard shell). Dev `:8083`→container `5175` (static IP `10.5.0.7`); prod `FE_INV_PORT`. Pages: `ProjectsListPage` (Đề tài của tôi), `ProposalFormPage` (schema-driven form), `CockpitPage`. Monorepo wiring: root `workspaces` += `frontend_investigator`; **all 4 Dockerfiles** COPY the new manifest (npm ci/install resolves); both compose files mount it into the FE services. - **`shared/src/lib/researchApi.ts`** — typed client for `/api/v1/research/*` (+ barrel export); used by both `frontend_investigator` and `frontend_admin`. - **Proposal form** (`components/proposal/`): `proposalSchema.ts` (the Mẫu III.06-TM.ĐTUD schema + buildInitial/shouldShow/collectMissing) + `ProposalFormFields.tsx` (renderer on shared shadcn) + `ProposalFormPage.tsx` (load/save-draft/submit; read-only for non-draft; approved → cockpit). - **Cockpit** (`components/cockpit/`): `cockpitConfig.ts` (ENTITIES field defs + status tones) + `CockpitWidgets.tsx` (Badge/Stat/Bar/EntityCard/EntityDrawer) + `CockpitPage.tsx` (bundle via TanStack Query, tabbed Overview/entities/audit, CRUD mutations + cache invalidation, owner/admin-gated). - **`frontend_admin`** — `ResearchReviewPage.tsx` (route `/research`, nav "Thẩm định đề tài", admin-gated): submitted-proposals queue + detail dialog (content read-only) + approve (assign mã số) / reject (note). ## Tests - `be0/tests/test_research_routes.py` — **7 tests** (3 pure-unit scalar extraction + 4 DB integration: lifecycle, reject/authz, entity CRUD+coercion+seeding+approved-gate+audit+bundle, malformed-content seeding). Run: `docker exec be0 sh -lc 'cd /app && python -m unittest tests.test_research_routes'`. - FE: `npm run typecheck` (×4 workspaces clean) + `npm run build -w frontend_investigator|frontend_admin`. ## Gotchas hit this session (blameless RCA) 1. **Migration COMMENT with semicolons** (016 v1) → the naive SQL splitter in `apply_initiative_migrations.py` splits on `;` even inside string literals → `unterminated quoted string`, the COMMENT failed (tables still created, since they commit before it). _5-why:_ the COMMENT body contained `; ` separators. _Fix:_ rewrote without semicolons (periods) + stripped accents. _Guard:_ already a documented rule (CLAUDE.md + reviewer memory) — keep COMMENT bodies semicolon/accent-free. 2. **Seeder crash on malformed content** (P2, reviewer-caught P1) — `for x in c.get(key) or []` only guards falsy; a truthy non-list (PI-controlled JSONB, e.g. `{"tienDoThucHien": 5}`) → `TypeError` → 500 on approve. _Fix (double-fault: code + test):_ `v = c.get(key); rows = v if isinstance(v, list) else []` + regression test `test_seeding_survives_malformed_content`. ## Commits (local `main`) `63e8bec` P1 schema+lifecycle · `c10ce1b` P2 entity APIs+seeding · `b561db4` P3 scaffold · `d3e7daf` P4 form · `8d186a6` P5 cockpit · `93cf6bf` P6 admin review · `b80cb64` admin allow-list. ## Eval / run - Admin account provisioned for E2E: `ththinh@ump.edu.vn` (added to the default policy-admin list). - Dev: PI app `localhost:5175`, admin `localhost:5174` (host vite servers, proxy `/api`→be0:4402). - **Not deployed** (push ≠ deploy; no CI). Prod needs `scripts/deploy-prod.sh` + `FE_INV_PORT` set, and a deploy of the new `frontend_investigator` service. ## Follow-ups (not done) - Bundle code-split (frontend_investigator ~1.6 MB — shared-deps bloat, same as frontend_user). - Richer admin proposal view (currently a humanized key:value dump — moving `proposalSchema` into `@ump/shared` would let the admin reuse the labeled renderer). - Per-member RBAC (v2) — link cockpit members to real accounts + enforce the 5 project roles server-side. - A `file-replace`/attachments story if proposals need evidence files (none yet).