Files
sciagent/docs/research-project-cockpit-feature.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

7.3 KiB
Raw Permalink Blame History

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_adminResearchReviewPage.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.py7 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).