690419:1329 feat: update CI/CD to use SSH key authentication #04
This commit is contained in:
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
SOURCE_DIR="/share/np-dms/app/source/lcbp3"
|
SOURCE_DIR="/share/np-dms/app/source/lcbp3"
|
||||||
COMPOSE_FILE="$SOURCE_DIR/specs/04-Infrastructure-OPS/04-00-docker-compose/docker-compose-app.yml"
|
COMPOSE_FILE="$SOURCE_DIR/specs/04-Infrastructure-OPS/04-00-docker-compose/QNAP/app/docker-compose-app.yml"
|
||||||
ENV_FILE="/share/np-dms/app/.env"
|
ENV_FILE="/share/np-dms/app/.env"
|
||||||
|
|
||||||
API_URL="https://backend.np-dms.work/api"
|
API_URL="https://backend.np-dms.work/api"
|
||||||
|
|||||||
@@ -18,6 +18,13 @@
|
|||||||
- **Q:** What is the cache TTL for Workflow History data to reduce join query overhead? → **A:** 1 hour — balanced cache duration for workflow history data
|
- **Q:** What is the cache TTL for Workflow History data to reduce join query overhead? → **A:** 1 hour — balanced cache duration for workflow history data
|
||||||
- **Q:** Who is authorized to upload step-specific attachments during a workflow transition? → **A:** Only assigned handler can upload; superadmin and organization admin can upload on behalf (impersonation)
|
- **Q:** Who is authorized to upload step-specific attachments during a workflow transition? → **A:** Only assigned handler can upload; superadmin and organization admin can upload on behalf (impersonation)
|
||||||
|
|
||||||
|
### Session 2026-04-19
|
||||||
|
- **Q:** สถานะ Workflow ใดบ้างที่อนุญาตให้อัปโหลด Step-specific Attachment ได้? → **A:** เฉพาะสถานะ Active-decision เท่านั้น (`PENDING_REVIEW`, `PENDING_APPROVAL`) — ห้ามอัปโหลดในสถานะ Terminal (`APPROVED`, `REJECTED`, `CLOSED`)
|
||||||
|
- **Q:** หาก Redis Redlock ล้มเหลวระหว่าง Transition ระบบควรทำอย่างไร? → **A:** Fail-closed — Retry 3 ครั้ง (500ms exponential backoff) แล้ว throw HTTP 503 "Service temporarily unavailable" เพื่อรักษาความถูกต้องของข้อมูล
|
||||||
|
- **Q:** Module ใดบ้างที่ต้องรองรับ Step-specific Attachments ใน v1.8.6? → **A:** เฉพาะ Module ที่ผ่าน Workflow Engine: **RFA, Transmittal, Circulation** — ไม่รวม Correspondence (ใช้ Circulation เป็น vehicle อยู่แล้ว หลีกเลี่ยงซ้ำซ้อน)
|
||||||
|
- **Q:** Performance target ของ Upload + Transition API คืออะไร? → **A:** P95 ≤ 5 วินาที สำหรับ file ≤ 10MB (ClamAV scan + Redlock + DB transaction included)
|
||||||
|
- **Q:** Definition of Done สำหรับ REQ-01 ถึง REQ-06 คืออะไร? → **A:** กำหนด Observable Outcome 1 ประโยคต่อ REQ ที่ตรวจสอบได้โดย QA/Product Owner โดยไม่ต้องอ่าน code
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. บริบทและความสำคัญ (Context & Problem Statement)
|
## 2. บริบทและความสำคัญ (Context & Problem Statement)
|
||||||
@@ -54,7 +61,7 @@
|
|||||||
| REQ-01 | **Integrated Banner** | แสดง ID, Status, Priority (`URGENT`/`HIGH`/`MEDIUM`/`LOW` พร้อม visual indicators) และปุ่ม Approve/Reject ไว้ในแถบด้านบนสุด |
|
| REQ-01 | **Integrated Banner** | แสดง ID, Status, Priority (`URGENT`/`HIGH`/`MEDIUM`/`LOW` พร้อม visual indicators) และปุ่ม Approve/Reject ไว้ในแถบด้านบนสุด |
|
||||||
| REQ-02 | **Step Detail View** | ใน Tab Workflow Engine ต้องแสดงรายละเอียด Role, Handler และ Description ของทุกขั้นตอน |
|
| REQ-02 | **Step Detail View** | ใน Tab Workflow Engine ต้องแสดงรายละเอียด Role, Handler และ Description ของทุกขั้นตอน |
|
||||||
| REQ-03 | **Active Step Color** | ขั้นตอนปัจจุบันใช้สี Indigo (#6366f1) พร้อม Pulse effect |
|
| REQ-03 | **Active Step Color** | ขั้นตอนปัจจุบันใช้สี Indigo (#6366f1) พร้อม Pulse effect |
|
||||||
| REQ-04 | **Step-safe Upload** | รองรับการลากวางไฟล์ (Drag & Drop) เพื่อแนบหลักฐานประจำขั้นตอนนั้นๆ |
|
| REQ-04 | **Step-safe Upload** | รองรับการลากวางไฟล์ (Drag & Drop) เพื่อแนบหลักฐานประจำขั้นตอนนั้นๆ — **อนุญาตเฉพาะสถานะ `PENDING_REVIEW` และ `PENDING_APPROVAL` เท่านั้น** สถานะ Terminal (`APPROVED`, `REJECTED`, `CLOSED`) ไม่อนุญาต |
|
||||||
| REQ-05 | **Internal Preview** | คลิกดูไฟล์ PDF/Images ได้ทันทีผ่าน Modal ภายในระบบ |
|
| REQ-05 | **Internal Preview** | คลิกดูไฟล์ PDF/Images ได้ทันทีผ่าน Modal ภายในระบบ |
|
||||||
| REQ-06 | **i18n Support** | ทุก UI text ต้องใช้ i18n keys ตาม `05-08-i18n-guidelines.md` |
|
| REQ-06 | **i18n Support** | ทุก UI text ต้องใช้ i18n keys ตาม `05-08-i18n-guidelines.md` |
|
||||||
|
|
||||||
@@ -64,7 +71,7 @@
|
|||||||
|
|
||||||
1. **Database Consistency:** ต้องเพิ่ม `workflow_history_id` ในตาราง `attachments` โดยรักษาค่าเป็น Nullable สำหรับไฟล์หลัก (Main Docs)
|
1. **Database Consistency:** ต้องเพิ่ม `workflow_history_id` ในตาราง `attachments` โดยรักษาค่าเป็น Nullable สำหรับไฟล์หลัก (Main Docs)
|
||||||
2. **Concurrency Control:**
|
2. **Concurrency Control:**
|
||||||
- **Transition Lock:** ใช้ Redis Redlock เมื่อมีการเปลี่ยนสถานะพร้อมอัปโหลดไฟล์ (Two-Phase: Lock → Upload + Transition → Unlock)
|
- **Transition Lock:** ใช้ Redis Redlock เมื่อมีการเปลี่ยนสถานะพร้อมอัปโหลดไฟล์ (Two-Phase: Lock → Upload + Transition → Unlock) — หาก Lock ไม่สำเร็จ: Retry 3 ครั้ง (500ms exponential backoff) แล้ว Fail-closed (HTTP 503)
|
||||||
- **File Upload:** ใช้ Temp Storage ก่อน (ADR-016 Two-Phase Upload) แล้วย้ายไป Permanent หลัง Transition สำเร็จ
|
- **File Upload:** ใช้ Temp Storage ก่อน (ADR-016 Two-Phase Upload) แล้วย้ายไป Permanent หลัง Transition สำเร็จ
|
||||||
3. **Security Audit:** ทุกไฟล์แนบราย Step ต้องผ่านการสแกน **ClamAV** และบันทึกข้อมูล Who/When ลงใน Audit Log (ADR-016)
|
3. **Security Audit:** ทุกไฟล์แนบราย Step ต้องผ่านการสแกน **ClamAV** และบันทึกข้อมูล Who/When ลงใน Audit Log (ADR-016)
|
||||||
4. **Identifier Strategy:** อ้างอิงไฟล์ผ่าน **UUIDv7** (publicId) เท่านั้น ห้ามใช้ ID ที่เป็น Integer ใน API (ADR-019)
|
4. **Identifier Strategy:** อ้างอิงไฟล์ผ่าน **UUIDv7** (publicId) เท่านั้น ห้ามใช้ ID ที่เป็น Integer ใน API (ADR-019)
|
||||||
@@ -73,6 +80,7 @@
|
|||||||
7. **File Size Limits:** ไม่กำหนด Limit ที่ Application Layer — ควบคุมโดย Infrastructure (Reverse Proxy / Storage Config) เท่านั้น
|
7. **File Size Limits:** ไม่กำหนด Limit ที่ Application Layer — ควบคุมโดย Infrastructure (Reverse Proxy / Storage Config) เท่านั้น
|
||||||
8. **Idempotency Control:** ทุกการอัปโหลดไฟล์พร้อม Transition ต้องมีการตรวจสอบ `Idempotency-Key` header (per .windsurfrules Security Rule #1)
|
8. **Idempotency Control:** ทุกการอัปโหลดไฟล์พร้อม Transition ต้องมีการตรวจสอบ `Idempotency-Key` header (per .windsurfrules Security Rule #1)
|
||||||
9. **Async Event Processing:** Notifications และ Events จาก workflow transitions ต้องใช้ **BullMQ** queue (ADR-008) — ห้าม inline ใน Service
|
9. **Async Event Processing:** Notifications และ Events จาก workflow transitions ต้องใช้ **BullMQ** queue (ADR-008) — ห้าม inline ใน Service
|
||||||
|
10. **Performance SLA:** `POST /workflow/:uuid/transition` (พร้อม file) ต้องตอบสนองภายใน **P95 ≤ 5 วินาที** สำหรับไฟล์ขนาด ≤ 10MB (รวม ClamAV scan + Redlock + DB transaction)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -94,8 +102,10 @@
|
|||||||
| **Upload ไฟล์ระหว่าง Transition ล้มเหลว** | Rollback transaction ทั้งหมด, ลบ temp files, คงสถานะ workflow เดิม |
|
| **Upload ไฟล์ระหว่าง Transition ล้มเหลว** | Rollback transaction ทั้งหมด, ลบ temp files, คงสถานะ workflow เดิม |
|
||||||
| **ไฟล์ถูกลบจาก Storage หลัง Attach** | แสดง "File unavailable" ใน UI, บันทึก metadata ไว้ตามเดิม |
|
| **ไฟล์ถูกลบจาก Storage หลัง Attach** | แสดง "File unavailable" ใน UI, บันทึก metadata ไว้ตามเดิม |
|
||||||
| **Concurrent Transition พร้อม Upload** | Redis Redlock จัดการ queue, อนุญาตเพียง 1 transition พร้อมกัน |
|
| **Concurrent Transition พร้อม Upload** | Redis Redlock จัดการ queue, อนุญาตเพียง 1 transition พร้อมกัน |
|
||||||
|
| **Redis Redlock ล้มเหลว / Redis ล่ม** | Retry 3 ครั้ง (500ms exponential backoff) — หากยังล้มเหลว throw HTTP 503 "Service temporarily unavailable" (Fail-closed, ห้าม Fail-open) |
|
||||||
| **Invalid File Type** | Reject ตั้งแต่ client-side (whitelist: PDF, DOCX, XLSX, DWG, ZIP) |
|
| **Invalid File Type** | Reject ตั้งแต่ client-side (whitelist: PDF, DOCX, XLSX, DWG, ZIP) |
|
||||||
| **Unauthorized Upload Attempt** | ตรวจสอบสิทธิ์: เฉพาะ assigned handler, superadmin, หรือ org admin เท่านั้น |
|
| **Unauthorized Upload Attempt** | ตรวจสอบสิทธิ์: เฉพาะ assigned handler, superadmin, หรือ org admin เท่านั้น |
|
||||||
|
| **Upload ในสถานะ Terminal** | Reject ทันทีด้วย HTTP 409 — ไม่อนุญาตอัปโหลดเมื่อสถานะเป็น `APPROVED`, `REJECTED`, หรือ `CLOSED` |
|
||||||
| **Duplicate Upload (Network Retry)** | ตรวจสอบ `Idempotency-Key` — reject duplicate พร้อม return existing result |
|
| **Duplicate Upload (Network Retry)** | ตรวจสอบ `Idempotency-Key` — reject duplicate พร้อม return existing result |
|
||||||
| **Cache Stale Data** | Invalidate Redis Cache ทันทีเมื่อ workflow state เปลี่ยน (TTL override)
|
| **Cache Stale Data** | Invalidate Redis Cache ทันทีเมื่อ workflow state เปลี่ยน (TTL override)
|
||||||
|
|
||||||
@@ -118,8 +128,13 @@
|
|||||||
- *Action:* Implement **CASL Guard** สำหรับ 4-Level RBAC: Superadmin > Org Admin > Assigned Handler > Read-only
|
- *Action:* Implement **CASL Guard** สำหรับ 4-Level RBAC: Superadmin > Org Admin > Assigned Handler > Read-only
|
||||||
|
|
||||||
### 🟡 Frontend Layer (UI & Interaction)
|
### 🟡 Frontend Layer (UI & Interaction)
|
||||||
- `frontend/app/(dashboard)/rfas/[uuid]/page.tsx` (และโมดูลอื่นที่เกี่ยวข้อง เช่น correspondences, circulations)
|
- `frontend/app/(dashboard)/rfas/[uuid]/page.tsx`
|
||||||
- *Action:* Refactor Header เป็น Integrated Banner และเพิ่ม Tab Workflow Lifecycle
|
- *Action:* Refactor Header เป็น Integrated Banner และเพิ่ม Tab Workflow Lifecycle
|
||||||
|
- `frontend/app/(dashboard)/transmittals/[uuid]/page.tsx`
|
||||||
|
- *Action:* Refactor Header เป็น Integrated Banner และเพิ่ม Tab Workflow Lifecycle
|
||||||
|
- `frontend/app/(dashboard)/circulation/[uuid]/page.tsx`
|
||||||
|
- *Action:* Refactor Header เป็น Integrated Banner และเพิ่ม Tab Workflow Lifecycle
|
||||||
|
- **หมายเหตุ (Out of Scope v1.8.6):** `correspondences/[uuid]/page.tsx` — ไม่รวมใน Scope นี้ Correspondence ใช้ Circulation เป็น Routing Vehicle อยู่แล้ว
|
||||||
- `frontend/components/workflow/workflow-visualizer.tsx` (สร้างใหม่)
|
- `frontend/components/workflow/workflow-visualizer.tsx` (สร้างใหม่)
|
||||||
- *Action:* พัฒนาการแสดงผล Vertical Timeline พร้อมระบบสี Active/Inactive
|
- *Action:* พัฒนาการแสดงผล Vertical Timeline พร้อมระบบสี Active/Inactive
|
||||||
- `frontend/components/common/file-preview-modal.tsx` (สร้างใหม่)
|
- `frontend/components/common/file-preview-modal.tsx` (สร้างใหม่)
|
||||||
@@ -165,6 +180,17 @@
|
|||||||
- **Frontend Component Tests:** สำหรับ Integrated Banner, Workflow Visualizer, File Preview Modal
|
- **Frontend Component Tests:** สำหรับ Integrated Banner, Workflow Visualizer, File Preview Modal
|
||||||
- **E2E Tests:** สำหรับ complete workflow พร้อม file upload
|
- **E2E Tests:** สำหรับ complete workflow พร้อม file upload
|
||||||
|
|
||||||
|
### Definition of Done (Observable Outcomes)
|
||||||
|
|
||||||
|
| REQ | Done When |
|
||||||
|
|-----|-----------|
|
||||||
|
| **REQ-01** | Banner แสดง Doc No, Status, Priority badge และปุ่ม Approve/Reject ก่อนเนื้อหาเอกสารในทุก Module (RFA, Transmittal, Circulation) |
|
||||||
|
| **REQ-02** | Tab "Workflow Engine" แสดง Role + Handler + Description ครบทุก Step โดยไม่ต้อง reload หน้า |
|
||||||
|
| **REQ-03** | Step ปัจจุบันใช้สี Indigo (#6366f1) พร้อม CSS Pulse animation ที่มองเห็นได้ชัดเจน; Step อื่นๆ ไม่มี animation |
|
||||||
|
| **REQ-04** | Drag & Drop ทำงานเฉพาะเมื่อ Status = `PENDING_REVIEW`/`PENDING_APPROVAL`; ปุ่มถูก disable เมื่อ Status = `APPROVED`/`REJECTED`/`CLOSED` |
|
||||||
|
| **REQ-05** | คลิกไฟล์ PDF/Image ใดก็ตามเปิด Preview Modal แบบ In-browser โดยไม่ navigate ออกจากหน้าปัจจุบัน |
|
||||||
|
| **REQ-06** | ไม่มี Hardcoded text ใน Component ใหม่; ทุก label ต้องเปลี่ยนภาษาได้เมื่อ switch EN/TH |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Compliance
|
## 10. Compliance
|
||||||
|
|||||||
@@ -214,19 +214,25 @@ export interface WorkflowTransitionWithAttachmentsDto {
|
|||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
[User clicks Approve/Reject/Return]
|
[User clicks Approve/Reject/Return]
|
||||||
→ use-workflow-action hook:
|
→ use-workflow-action hook (client-side guard):
|
||||||
1. Generates Idempotency-Key (UUIDv7)
|
1. ❗️ ตรวจสอบ currentState ∈ {PENDING_REVIEW, PENDING_APPROVAL}
|
||||||
2. POST /workflow-engine/instances/:id/transition
|
└ ถ้าไม่ใช่ → ไม่ส่ง API (ปุ่ม disabled ไว้แล้ว)
|
||||||
|
2. Generates Idempotency-Key (UUIDv7)
|
||||||
|
3. POST /workflow-engine/instances/:id/transition
|
||||||
Header: Idempotency-Key
|
Header: Idempotency-Key
|
||||||
Body: { action, comment, attachmentPublicIds: [uuid1, uuid2] }
|
Body: { action, comment, attachmentPublicIds: [uuid1, uuid2] }
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
[WorkflowTransitionGuard] — RBAC check (4.5-Level: Superadmin / Org Admin / Level 2.5 Contract Membership / Assigned Handler)
|
[WorkflowTransitionGuard] — RBAC check (4-Level: Superadmin / Org Admin / Contract Member / Assigned Handler)
|
||||||
│ pass
|
│ pass
|
||||||
▼
|
▼
|
||||||
[WorkflowEngineService.processTransition()]
|
[WorkflowEngineService.processTransition()]
|
||||||
→ Check Redis idempotency key (return cached if duplicate)
|
→ Check Redis idempotency key (return cached if duplicate)
|
||||||
|
→ ❗️ Server-side state check: currentState ∈ {PENDING_REVIEW, PENDING_APPROVAL}
|
||||||
|
└ ถ้าไม่ใช่ → throw HTTP 409 Conflict "ไม่สามารถอัปโหลดในสถานะนี้" (Clarify Q1)
|
||||||
→ Acquire Redis Redlock on instanceId
|
→ Acquire Redis Redlock on instanceId
|
||||||
|
└ Retry 3x (500ms exponential backoff)
|
||||||
|
└ ถ้า Redis ล่ม / Lock ไม่ได้ → throw HTTP 503 "Service temporarily unavailable" (Clarify Q2 Fail-closed)
|
||||||
→ Begin DB Transaction:
|
→ Begin DB Transaction:
|
||||||
1. Lock WorkflowInstance (pessimistic_write)
|
1. Lock WorkflowInstance (pessimistic_write)
|
||||||
2. Evaluate DSL transition
|
2. Evaluate DSL transition
|
||||||
@@ -246,6 +252,7 @@ export interface WorkflowTransitionWithAttachmentsDto {
|
|||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
[Frontend: invalidate TanStack Query cache → reload document + timeline]
|
[Frontend: invalidate TanStack Query cache → reload document + timeline]
|
||||||
|
→ HTTP 503 → แสดง toast "ระบบยุ่ง กรุณาลองใหม่" (user may retry)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
**Testing**: Jest (backend unit + e2e), Vitest (frontend)
|
**Testing**: Jest (backend unit + e2e), Vitest (frontend)
|
||||||
**Target Platform**: QNAP Container Station (Docker), Browser (Chrome/Edge latest)
|
**Target Platform**: QNAP Container Station (Docker), Browser (Chrome/Edge latest)
|
||||||
**Project Type**: Web application (backend/ + frontend/ monorepo)
|
**Project Type**: Web application (backend/ + frontend/ monorepo)
|
||||||
**Performance Goals**: Workflow history + attachment join query < 200ms p95 (mitigated by Redis Cache TTL 1h)
|
**Performance Goals**: (1) Workflow history + attachment join query < 200ms p95 (mitigated by Redis Cache TTL 1h); (2) `POST /instances/:id/transition` (พร้อม file) P95 ≤ 5 วินาที สำหรับ file ≤ 10MB (รวม ClamAV + Redlock + DB transaction) — Clarify Q4
|
||||||
**Constraints**: No TypeORM migrations (ADR-009); UUID via `publicId` only (ADR-019); ClamAV scan mandatory (ADR-016); BullMQ for all async jobs (ADR-008)
|
**Constraints**: No TypeORM migrations (ADR-009); UUID via `publicId` only (ADR-019); ClamAV scan mandatory (ADR-016); BullMQ for all async jobs (ADR-008)
|
||||||
**Scale/Scope**: ~50 concurrent users, documents in hundreds per project
|
**Scale/Scope**: ~50 concurrent users, documents in hundreds per project
|
||||||
|
|
||||||
@@ -45,7 +45,8 @@ _GATE: Checked against `.windsurfrules` before Phase 0. Re-verified after Phase
|
|||||||
| **🔴 No `any` types** | ✅ PASS | All new types fully typed — see data-model.md |
|
| **🔴 No `any` types** | ✅ PASS | All new types fully typed — see data-model.md |
|
||||||
| **🟡 Thin Controller** | ✅ PASS | Controller delegates to Service; Guard handles RBAC |
|
| **🟡 Thin Controller** | ✅ PASS | Controller delegates to Service; Guard handles RBAC |
|
||||||
| **🟡 Test Coverage 80% business logic** | ⚠️ REQUIRED | See testing plan in Phase 3 |
|
| **🟡 Test Coverage 80% business logic** | ⚠️ REQUIRED | See testing plan in Phase 3 |
|
||||||
| **🔴 Redis Redlock (ADR-002)** | ✅ PASS | Redlock applied to `instanceId` during `processTransition()` — existing pattern extended |
|
| **🔴 Redis Redlock (ADR-002)** | ✅ PASS | Redlock applied to `instanceId` during `processTransition()` — Fail-closed: Retry 3x (500ms exponential backoff) → HTTP 503 if Redis unavailable (Clarify Q2) |
|
||||||
|
| **🔴 Upload State Restriction** | ✅ PASS | Step-attachment upload permitted only in `PENDING_REVIEW`/`PENDING_APPROVAL`; Terminal states (`APPROVED`,`REJECTED`,`CLOSED`) → HTTP 409 (Clarify Q1) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -110,9 +111,9 @@ frontend/hooks/
|
|||||||
|
|
||||||
# 🟡 Frontend — Page Refactors (use new components)
|
# 🟡 Frontend — Page Refactors (use new components)
|
||||||
frontend/app/(dashboard)/rfas/[uuid]/page.tsx [MODIFY — integrate IntegratedBanner + WorkflowLifecycle]
|
frontend/app/(dashboard)/rfas/[uuid]/page.tsx [MODIFY — integrate IntegratedBanner + WorkflowLifecycle]
|
||||||
frontend/app/(dashboard)/correspondences/[uuid]/page.tsx [MODIFY — same]
|
frontend/app/(dashboard)/transmittals/[uuid]/page.tsx [MODIFY — same as RFA]
|
||||||
frontend/app/(dashboard)/transmittals/[uuid]/page.tsx [MODIFY — same as RFA/Correspondence]
|
frontend/app/(dashboard)/circulation/[uuid]/page.tsx [MODIFY — same as RFA]
|
||||||
frontend/app/(dashboard)/circulation/[uuid]/page.tsx [MODIFY — same as RFA/Correspondence]
|
# ⛔ OUT OF SCOPE (v1.8.6): correspondences/[uuid]/page.tsx — Correspondence ใช้ Circulation เป็น Routing Vehicle (Clarify Q3)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -178,10 +179,12 @@ Response: WorkflowHistoryItem[] with nested attachments[] per step
|
|||||||
```
|
```
|
||||||
<IntegratedBanner document={doc} workflowInstance={instance} onAction={...} />
|
<IntegratedBanner document={doc} workflowInstance={instance} onAction={...} />
|
||||||
└── uses: <PriorityBadge />, <StatusBadge />, <WorkflowActionButtons />
|
└── uses: <PriorityBadge />, <StatusBadge />, <WorkflowActionButtons />
|
||||||
|
└── WorkflowActionButtons: disabled เมื่อ currentState ∈ {APPROVED, REJECTED, CLOSED}
|
||||||
|
|
||||||
<WorkflowLifecycle instance={instance} onFileClick={openPreview} />
|
<WorkflowLifecycle instance={instance} onFileClick={openPreview} />
|
||||||
└── vertical timeline, Indigo active step (pulse animation)
|
└── vertical timeline, Indigo active step (pulse animation)
|
||||||
└── each step: StepCard with date, actor, comment, attachments[]
|
└── each step: StepCard with date, actor, comment, attachments[]
|
||||||
|
└── Drag & Drop zone: แสดงเฉพาะเมื่อ currentState ∈ {PENDING_REVIEW, PENDING_APPROVAL}
|
||||||
|
|
||||||
<FilePreviewModal file={attachment} onClose={...} />
|
<FilePreviewModal file={attachment} onClose={...} />
|
||||||
└── PDF: <iframe src="/api/files/preview/:publicId" />
|
└── PDF: <iframe src="/api/files/preview/:publicId" />
|
||||||
@@ -190,9 +193,13 @@ Response: WorkflowHistoryItem[] with nested attachments[] per step
|
|||||||
|
|
||||||
**`use-workflow-action` hook responsibilities:**
|
**`use-workflow-action` hook responsibilities:**
|
||||||
1. Validate `Idempotency-Key` (generate UUIDv7 once per action intent)
|
1. Validate `Idempotency-Key` (generate UUIDv7 once per action intent)
|
||||||
2. Ensure all `attachmentPublicIds` are committed (not temp) before transition
|
2. Guard: ตรวจสอบว่า `currentState ∈ {PENDING_REVIEW, PENDING_APPROVAL}` ก่อน transition (client-side guard)
|
||||||
3. Call `POST /instances/:id/transition` with `Idempotency-Key` header
|
3. Ensure all `attachmentPublicIds` are committed (not temp) before transition
|
||||||
4. Invalidate TanStack Query cache for the document + workflow instance
|
4. Call `POST /instances/:id/transition` with `Idempotency-Key` header
|
||||||
|
5. Handle HTTP 503 (Redlock unavailable) → แสดง toast "ระบบยุ่ง กรุณาลองใหม่"
|
||||||
|
6. Invalidate TanStack Query cache for the document + workflow instance
|
||||||
|
|
||||||
|
**Modules in scope (v1.8.6):** RFA, Transmittal, Circulation — ไม่รวม Correspondence (Clarify Q3)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -222,9 +229,9 @@ Response: WorkflowHistoryItem[] with nested attachments[] per step
|
|||||||
| F5 | Create `WorkflowLifecycle` component (vertical timeline) | `components/workflow/workflow-lifecycle.tsx` | F1 |
|
| F5 | Create `WorkflowLifecycle` component (vertical timeline) | `components/workflow/workflow-lifecycle.tsx` | F1 |
|
||||||
| F6 | Create `FilePreviewModal` component | `components/common/file-preview-modal.tsx` | F1 |
|
| F6 | Create `FilePreviewModal` component | `components/common/file-preview-modal.tsx` | F1 |
|
||||||
| F7 | Refactor RFA detail page — integrate new components | `rfas/[uuid]/page.tsx` | F3–F6 |
|
| F7 | Refactor RFA detail page — integrate new components | `rfas/[uuid]/page.tsx` | F3–F6 |
|
||||||
| F8 | Refactor Correspondence detail page — integrate new components | `correspondences/[uuid]/page.tsx` | F3–F6 |
|
| F8 | Refactor Transmittal detail page — integrate new components | `transmittals/[uuid]/page.tsx` | F3–F6 |
|
||||||
| F9 | Refactor Transmittal detail page — integrate new components | `transmittals/[uuid]/page.tsx` | F3–F6 |
|
| F9 | Refactor Circulation detail page — integrate new components | `circulation/[uuid]/page.tsx` | F3–F6 |
|
||||||
| F10 | Refactor Circulation detail page — integrate new components | `circulation/[uuid]/page.tsx` | F3–F6 |
|
| ~~F10~~ | ~~Correspondence~~ | **OUT OF SCOPE v1.8.6** — Clarify Q3 | — |
|
||||||
|
|
||||||
### 🟢 GUIDELINES (after F7/F8)
|
### 🟢 GUIDELINES (after F7/F8)
|
||||||
|
|
||||||
@@ -278,6 +285,11 @@ cd frontend && pnpm test --run # Vitest
|
|||||||
- [ ] Unauthorized user (not handler, not admin) → `403 Forbidden`
|
- [ ] Unauthorized user (not handler, not admin) → `403 Forbidden`
|
||||||
- [ ] ClamAV test file (EICAR) upload → blocked before transition
|
- [ ] ClamAV test file (EICAR) upload → blocked before transition
|
||||||
- [ ] `attachmentPublicIds` with non-temp (already-committed) UUID → rejected
|
- [ ] `attachmentPublicIds` with non-temp (already-committed) UUID → rejected
|
||||||
|
- [ ] Upload attempt when `currentState = APPROVED/REJECTED/CLOSED` → `409 Conflict` (Clarify Q1)
|
||||||
|
- [ ] Transition when Redis unavailable (mock Redis down) → retry 3x then `503 Service Unavailable` (Clarify Q2)
|
||||||
|
|
||||||
|
### Definition of Done Reference
|
||||||
|
ดู DoD Observable Outcomes ต่อ REQ ใน [ADR-021 §9.1](../../06-Decision-Records/ADR-021-integrated-workflow-context.md%20.md#definition-of-done-observable-outcomes)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,61 @@ The existing component is **horizontal** layout. ADR-021 §3.2 requires **Vertic
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Research Question 10: Redis Redlock Failure Mode During Transition
|
||||||
|
|
||||||
|
**Question:** If Redis is unreachable or Redlock cannot be acquired, should the system fail-open (allow transition) or fail-closed (block transition)?
|
||||||
|
|
||||||
|
**Context (Clarify Q2):** In DMS, a race condition that creates duplicate approvals is worse than temporary service unavailability. Incorrect document state corrupts audit trails and may have legal/contractual consequences.
|
||||||
|
|
||||||
|
### Option A: Fail-open (Rejected ❌)
|
||||||
|
Allow transition even without a lock. Accepts Race Condition risk.
|
||||||
|
- **Con:** Two concurrent approvals possible — violates document integrity.
|
||||||
|
|
||||||
|
### Option B: Retry then fail-open (Rejected ❌)
|
||||||
|
Retry 3x then allow anyway.
|
||||||
|
- **Con:** Still permits race condition under high load.
|
||||||
|
|
||||||
|
### Option C: Fail-closed — Retry then HTTP 503 (Selected ✅)
|
||||||
|
Retry 3x with 500ms exponential backoff → if still unavailable → throw HTTP 503.
|
||||||
|
- **Pro:** Data integrity preserved at all times.
|
||||||
|
- **Pro:** HTTP 503 is a retryable error — client can display toast and user can retry manually.
|
||||||
|
- **Con:** Temporary service degradation during Redis outage (~50 concurrent users; outage rare).
|
||||||
|
|
||||||
|
**Decision:** Option C — Fail-closed.
|
||||||
|
**Implementation:**
|
||||||
|
```typescript
|
||||||
|
// ใน WorkflowEngineService.processTransition()
|
||||||
|
// ลอง Acquire Redlock สูงสุด 3 ครั้ง
|
||||||
|
const MAX_LOCK_RETRIES = 3;
|
||||||
|
const LOCK_BACKOFF_MS = 500;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= MAX_LOCK_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
lock = await this.redlock.acquire([`lock:wf:${instanceId}`], 10000);
|
||||||
|
break; // สำเร็จ
|
||||||
|
} catch {
|
||||||
|
if (attempt === MAX_LOCK_RETRIES) {
|
||||||
|
throw new ServiceUnavailableException('ระบบยุ่งชั่วคราว กรุณาลองใหม่ภายหลัง');
|
||||||
|
}
|
||||||
|
await sleep(LOCK_BACKOFF_MS * attempt); // exponential backoff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Research Question 11: Upload-Permitted States
|
||||||
|
|
||||||
|
**Question:** Which workflow states allow step-attachment upload? (Clarify Q1)
|
||||||
|
|
||||||
|
**Decision:** Upload permitted ONLY in `PENDING_REVIEW` and `PENDING_APPROVAL`.
|
||||||
|
|
||||||
|
**Rationale:** Attachments are decision-support evidence. After a terminal decision (`APPROVED`, `REJECTED`, `CLOSED`), adding attachments retroactively would contaminate the audit trail.
|
||||||
|
|
||||||
|
**Implementation:** Both client-side guard (disable upload UI) AND server-side validation in `processTransition()` before acquiring Redlock.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Summary of All Decisions
|
## Summary of All Decisions
|
||||||
|
|
||||||
| # | Decision | Chosen | Rejected |
|
| # | Decision | Chosen | Rejected |
|
||||||
@@ -214,3 +269,6 @@ The existing component is **horizontal** layout. ADR-021 §3.2 requires **Vertic
|
|||||||
| 7 | Transition RBAC | New `WorkflowTransitionGuard` 4-Level | Extend existing `RbacGuard` |
|
| 7 | Transition RBAC | New `WorkflowTransitionGuard` 4-Level | Extend existing `RbacGuard` |
|
||||||
| 8 | Priority display | Tailwind badges from `instance.context.priority` | Hardcoded status |
|
| 8 | Priority display | Tailwind badges from `instance.context.priority` | Hardcoded status |
|
||||||
| 9 | File preview security | `Content-Disposition: inline` + permission check | Direct storage URL |
|
| 9 | File preview security | `Content-Disposition: inline` + permission check | Direct storage URL |
|
||||||
|
| 10 | Redlock failure mode | Fail-closed: Retry 3x (500ms backoff) → HTTP 503 | Fail-open |
|
||||||
|
| 11 | Upload-permitted states | `PENDING_REVIEW`, `PENDING_APPROVAL` only | All non-terminal states |
|
||||||
|
| 12 | Module scope (v1.8.6) | RFA, Transmittal, Circulation | Including Correspondence |
|
||||||
|
|||||||
Reference in New Issue
Block a user