7.3 KiB
7.3 KiB
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 JSONBholds 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 usestart_period/end_period(the JSON keysstart/endmap 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.dapply_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_fieldsmaps 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_versionis enforced by the globalmain.pymiddleware (routers use baredecode_access_token_user_id). - Seed-on-approve —
_seed_cockpit_from_proposalpopulates members (chủ nhiệm + thư ký + thành viên) + milestones (tiến độ) fromcontentso the cockpit opens "according to the proposal". Best-effort + idempotent.
- Proposals:
- Auth model: a PI = any authenticated user who owns a project (no new system role). Admin via the
existing allow-list mechanism (
AUTH_ADMIN_EMAILSenv, else the built-in default list inauth_api.py).
Frontend
frontend_investigator/— new Vite/React/TS app cloned fromfrontend_user(same design system by construction:index.css+ tailwind tokens, Inter/Merriweather, shared shadcn primitives, auth flow, dashboard shell). Dev:8083→container5175(static IP10.5.0.7); prodFE_INV_PORT. Pages:ProjectsListPage(Đề tài của tôi),ProposalFormPage(schema-driven form),CockpitPage. Monorepo wiring: rootworkspaces+=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 bothfrontend_investigatorandfrontend_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)
- Migration COMMENT with semicolons (016 v1) → the naive SQL splitter in
apply_initiative_migrations.pysplits 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. - 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.
- regression test
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, adminlocalhost:5174(host vite servers, proxy/api→be0:4402). - Not deployed (push ≠ deploy; no CI). Prod needs
scripts/deploy-prod.sh+FE_INV_PORTset, and a deploy of the newfrontend_investigatorservice.
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
proposalSchemainto@ump/sharedwould 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).