sciagent code + Gitea Actions CI/CD
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Thinh Lam
2026-06-30 09:38:30 +07:00
commit 688fac73e9
1167 changed files with 158244 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
# 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).