94 lines
7.3 KiB
Markdown
94 lines
7.3 KiB
Markdown
# 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).
|