eCourt-Doc-Edit/README.md
2026-08-19 23:06:57 +07:00

280 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# e-Court Documents Versioning (Word Add-in)
ระบบจัดการ version history ของเอกสาร Word — สร้างเอกสารจาก template ผ่านเว็บ ติดตามทุกการแก้ไขผ่าน add-in ใน Word และ sync ข้อมูลสำคัญกลับฐานข้อมูลอัตโนมัติ
## คุณสมบัติหลัก
- 📝 **สร้างเอกสารจาก template** — กรอก form บนเว็บ → ระบบ generate .docx จาก template (แทนที่ค่าใน content controls) พร้อม stamp metadata และขึ้นเป็น **version 1 อัตโนมัติ**
- 🗂️ **Template management** — upload ไฟล์ .docx ที่มี content controls แล้วระบบ **detect fields อัตโนมัติ** ตั้งชื่อ/ชนิด/required ต่อ field ผ่านหน้าเว็บ
- 📌 **ตรวจจับ tracked document** — เปิดไฟล์ใน Word (Desktop Win/Mac หรือ Online) แล้ว add-in รู้ทันทีว่าไฟล์นี้อยู่ในระบบ พร้อมแสดง version history
- 💾 **Save & Version** — ปุ่มใน task pane สร้าง version ใหม่ (stamp metadata + อัปโหลด snapshot ไป backend)
- 🔄 **Field sync** — ทุกครั้งที่สร้าง version ระบบ extract ค่าจาก content controls ของเอกสารไป update fields ใน DB ให้ sync กับเนื้อหาจริงเสมอ
- ⬇️ **ดาวน์โหลด version ย้อนหลัง** — ทุก version เก็บครบพร้อม SHA256 hash ตรวจสอบความถูกต้องได้
## สถาปัตยกรรม
| ส่วน | Stack | หน้าที่ |
|---|---|---|
| **Add-in** (`/addin`) | Office.js, TypeScript, React, Fluent UI | Task pane ใน Word — ตรวจจับ document, Save & Version, version history, field extraction |
| **Backend** (`/backend`) | ASP.NET Core 8, EF Core, PostgreSQL, MinIO, OpenXML | Version API, template system, docx generation, JWT validation |
| **Web Portal** (`/web`) | Nuxt.js, TailwindCSS | จัดการ template + สร้างเอกสารจาก form (light/dark theme) |
| **Auth** | Keycloak (OIDC + PKCE) | Public clients ทั้ง add-in และเว็บ, backend validate ผ่าน JWKS |
| **Storage** | MinIO (S3-compatible) | bucket `docx-versions` (snapshot ต่อ version) + `docx-templates` (ไฟล์ template) — private ทั้งคู่ |
**Flow หลัก:**
1. Admin upload template (.docx ที่มี content controls) → ระบบ detect fields อัตโนมัติ
2. ผู้ใช้กรอก form บนเว็บ → ได้ไฟล์ .docx ที่เป็น version 1 แล้ว → เปิดใน Word
3. Add-in รู้จักเอกสารทันที → แก้ไขเนื้อหา → กด **Save & Version** → เกิด version ใหม่ + fields ใน DB sync ตามเนื้อหาล่าสุด
## สถานะโปรเจกต์
| Phase | เนื้อหา | สถานะ |
|---|---|---|
| 0 | Scaffold 3 โปรเจกต์ (addin / backend / web) | ✅ เสร็จ |
| 1 | Local dev infra — docker-compose + Keycloak (realm, 3 clients + PKCE) + MinIO (buckets) | ✅ เสร็จ |
| 2 | Backend core — version API (FR-5–7) + JWT auth (FR-8) | ✅ เสร็จ |
| 3 | Template system + docx generation + field sync (FR-9–11) | ✅ เสร็จ |
| 4 | Add-in services layer — auth (PKCE + Dialog API), metadata, file/API, field extraction (FR-8, FR-11) | ✅ เสร็จ |
| 5 | Task pane UI — sign-in, Start Versioning, Save & Version, history (FR-1–4) | ✅ เสร็จ — ทดสอบจริงบน Word for Mac แล้ว (login/tracking/Save & Version ผ่าน, แก้ปัญหาที่เจอ 4 ประเด็น — ดู Troubleshooting) เหลือยืนยัน download + AC เต็มชุด |
| 6 | Web Portal UI (FR-9–10) | ⏳ รอ implement |
| 7–8 | Tests ครบชุด + เอกสารฉบับเต็ม | ⏳ รอ implement |
**ใช้ได้แล้วตอนนี้:** infra ทั้งชุด + backend API ครบทั้ง version API (upload / history / download) และ template system (upload template พร้อม auto-detect fields, generate เอกสารเป็น v1, field sync ตอนสร้าง version) — ลองเล่นได้ผ่าน Swagger หรือ curl (ตัวอย่างด้านล่าง) · ฝั่ง add-in ครบทั้ง services layer และ task pane UI พร้อม unit tests 45 ตัว
## เอกสาร
- [specs/spec-officejs.md](specs/spec-officejs.md) — spec เต็มรูปแบบ: functional requirements (FR-1–11), data models, acceptance criteria (1–14)
- [docs/plans/officejs-implementation-plan.md](docs/plans/officejs-implementation-plan.md) — implementation plan แบ่ง 9 phases พร้อม definition of done
สำหรับผู้เริ่มพัฒนา: อ่าน spec ก่อน แล้วตามด้วย plan (เริ่มจาก Phase 0)
## การพัฒนา (local)
### Prerequisites
- Docker Desktop
- .NET 8 SDK + `dotnet-ef` tool (สำหรับ migration):
```bash
dotnet tool install --global dotnet-ef --version 8.0.11
```
- Node.js LTS (add-in และ web)
- **Dev cert สำหรับ localhost** (ต้องทำ**ก่อน** `docker compose up` — Keycloak mount cert จากที่นี่):
```bash
cd addin && npx office-addin-dev-certs install
```
ทำครั้งเดียวต่อเครื่อง: สร้าง CA + cert ที่ trust ไว้ใน keychain ใช้ทั้ง add-in dev server (https://localhost:3000) และ Keycloak (https://localhost:8443)
### 1) รัน infrastructure (PostgreSQL + MinIO + Keycloak)
```bash
cp .env.example .env # ครั้งแรกครั้งเดียว — แก้ password ได้ตามต้องการ
docker compose up -d
```
เมื่อขึ้นครบแล้ว:
| บริการ | URL | เข้าด้วย |
|---|---|---|
| Keycloak admin console | https://localhost:8443/admin/ | `KC_BOOTSTRAP_ADMIN_USERNAME` / `KC_BOOTSTRAP_ADMIN_PASSWORD` จาก `.env` |
| MinIO console | http://localhost:9001 | `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` จาก `.env` |
| PostgreSQL | `localhost:5432` | `POSTGRES_USER` / `POSTGRES_PASSWORD` จาก `.env`, ฐานข้อมูล `wordversioning` |
> **ทำไม Keycloak ต้องเป็น HTTPS:** Office Dialog API ที่ add-in ใช้เปิดหน้า login **ปฏิเสธ http:// โดยสมบูรณ์** (error: "The HTTP protocol is not supported. Use HTTPS instead") — dev จึงให้ Keycloak เสิร์ฟ HTTPS ที่ port 8443 ด้วย cert ของ `office-addin-dev-certs` (CA ถูก trust ในเครื่องแล้ว Word/webview จึงเชื่อ) · ตัว issuer ใน token จะเป็น `https://localhost:8443/realms/word-versioning` ซึ่ง backend ตั้ง `Authentication:Authority` ตรงกัน
Keycloak จะ import realm `word-versioning` อัตโนมัติ (clients 3 ตัว + test user) จาก [keycloak/realm-export.json](keycloak/realm-export.json) — ไฟล์นี้คือ source of truth ของ realm ฝั่ง dev: แก้ realm ให้แก้ที่ไฟล์นี้แล้วรัน `docker compose rm -sf keycloak && docker compose up -d` เพื่อ import ใหม่ (การแก้ผ่าน admin console จะหายเมื่อ recreate container)
**Test user สำหรับล็อกอิน:** `testuser` / `Test1234!` (เปลี่ยนได้ใน realm-export.json)
MinIO จะมี bucket `docx-versions` และ `docx-templates` (private ทั้งคู่) สร้างให้อัตโนมัติ
### 2) Backend API
```bash
cd backend/WordVersioning.Api
dotnet ef database update # ครั้งแรก / เมื่อมี migration ใหม่
dotnet run # รันทั้ง http://localhost:5075 และ https://localhost:7232 (Swagger ใช้ https ก็ได้)
```
> **ทำไม dev ต้องมี endpoint HTTPS ด้วย:** task pane ของ add-in เป็นหน้า `https://localhost:3000` และ **webview ของ Word (WKWebView บน Mac) บล็อก fetch จากหน้า https ไป http แบบเงียบๆ** — error ที่เห็นคือ "Load failed" — ส่วน CORS ก็ไม่ช่วยเพราะ request ไม่เคยถึง server · dev จึงเปิด endpoint `https://localhost:7232` ด้วย cert ชุดเดียวกับ Keycloak (`~/.office-addin-dev-certs`, ปิดได้ผ่าน `DevHttps:Enabled=false` ใน `appsettings.Development.json`) · add-in ชี้ API ที่ https ตัวนี้อัตโนมัติ
ค่าเชื่อมต่อ (DB/MinIO/Keycloak) อยู่ใน `appsettings.json` + `appsettings.Development.json` — override ผ่าน env ได้ทั้งหมด เช่น `ConnectionStrings__Default`, `MinIO__Endpoint`, `Authentication__Authority`
**API ที่มีตอนนี้** (ทุก endpoint ต้องแนบ `Authorization: Bearer <token>`):
```
# Documents / versions (FR-5–7, 11)
POST /api/documents/{documentId}/versions # อัปโหลด version ใหม่ + field sync
GET /api/documents/{documentId}/versions # version history ใหม่ → เก่า (พร้อม snapshot fields)
GET /api/documents/{documentId}/versions/{versionId}/download # ดาวน์โหลด version (proxy stream จาก MinIO)
GET /api/documents/{documentId}/versions/{versionId}/download-link # presigned URL อายุสั้น (ให้เบราว์เซอร์โหลดตรงจาก MinIO)
# Generate จาก template (FR-10)
POST /api/documents/generate # { templateId, values } → เอกสาร v1 พร้อม metadata
# Templates (FR-9)
GET /api/templates # รายการ (?includeInactive=true สำหรับ admin)
GET /api/templates/{id} # รายละเอียด + field definitions
POST /api/templates # upload .docx → detect fields อัตโนมัติ
PUT /api/templates/{id} # แก้ metadata + field definitions (label/type/required/ลำดับ)
PUT /api/templates/{id}/file # แทนที่ไฟล์ → re-detect (บอก tag ที่เพิ่ม/หาย)
DELETE /api/templates/{id} # deactivate (soft-delete)
```
- Upload เป็น multipart/form-data: `file` (.docx เท่านั้น, จำกัด 100MB) + `clientVersionId` (GUID) + `fields` (optional, JSON map tag→text สำหรับ field sync)
- `documentId` ยังไม่มีในระบบ = สร้างใหม่อัตโนมัติ · `versionNumber` เพิ่มอัตโนมัติต่อเอกสาร · เก็บ SHA256 ของทุก version
- Field sync policy (FR-11): field ที่ส่งมาขาด (ผู้ใช้ลบ content control ใน Word) → เก็บค่าเดิม + mark missing ใน snapshot **ไม่ fail request** · ส่งเกินนิยาม → ignore
- Generate (FR-10): backend เปิด template ด้วย OpenXML → แทนที่ค่าใน SDT ที่ **run level** (รักษา formatting) → stamp custom properties (`SavedByAddIn`, `DocumentTrackingId`) → upload เป็น v1 → response `{ documentId, versionId, versionNumber, downloadUrl }` — เปิดไฟล์ใน Word แล้ว add-in (Phase 4–5) จะรู้จักทันที
### ขอ token ทดสอบ (dev)
ทุก client ใน realm บังคับ PKCE (ปิด direct grants ตาม spec) และ token ของ `admin-cli` **ไม่มี claim `aud`** จึงเรียก backend ไม่ผ่าน (401) — ใช้สคริปต์นี้เดิน PKCE flow จริงแทน (login ด้วย test user ให้อัตโนมัติ):
```bash
python3 scripts/dev-token.py # ปริ้น access token
python3 scripts/dev-token.py --clip # หรือคัดลอกเข้า clipboard
TOKEN=$(python3 scripts/dev-token.py) # เก็บใส่ตัวแปร
```
แล้วลอง upload:
```bash
DOC_ID=$(uuidgen | tr 'A-Z' 'a-z')
curl -X POST http://localhost:5075/api/documents/$DOC_ID/versions \
-H "Authorization: Bearer $TOKEN" \
-F "file=@เอกสาร.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
-F "clientVersionId=$(uuidgen)"
# → { "versionId": "...", "versionNumber": 1 }
```
### ลองเล่น template → generate → field sync (Phase 3)
**0) เตรียมไฟล์ template** — เขียน .docx ใน Word แล้วแทรก content controls ในจุดที่จะเป็น field (Developer tab → Rich Text Content Control → Properties ตั้ง **Tag** ให้ unique ต่อไฟล์ เช่น `employee_name`, `salary`)
**1) Upload template → ระบบ detect fields อัตโนมัติ:**
```bash
curl -X POST http://localhost:5075/api/templates \
-H "Authorization: Bearer $TOKEN" \
-F "name=สัญญาจ้างงาน" \
-F "file=@template.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document"
# → { "id": "...", "fields": [ { "key": "employee_name", ... }, ... ] }
```
**2) ตั้งค่า field definitions** (label ที่แสดงในฟอร์ม / ชนิด `text|number|date` / required / ลำดับ):
```bash
curl -X PUT http://localhost:5075/api/templates/$TPL_ID \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"fields": [
{ "key": "employee_name", "label": "ชื่อ-นามสกุล", "type": "text", "isRequired": true, "sortOrder": 0 },
{ "key": "salary", "label": "เงินเดือน", "type": "number", "isRequired": true, "sortOrder": 1 } ]}'
```
**3) Generate เอกสาร — ได้ .docx ที่เป็น version 1 พร้อม metadata:**
```bash
curl -X POST http://localhost:5075/api/documents/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"templateId\":\"$TPL_ID\",\"values\":{\"employee_name\":\"สมชาย ใจดี\",\"salary\":\"50000\"}}"
# → { "documentId": "...", "versionId": "...", "versionNumber": 1, "downloadUrl": "/api/documents/.../download" }
curl -H "Authorization: Bearer $TOKEN" -o ไฟล์ที่สร้าง.docx \
"http://localhost:5075$DOWNLOAD_URL"
```
เปิดไฟล์ที่ได้ตรวจได้ว่า: ค่าถูกทุก field (formatting ของ template คงเดิม) และมี custom properties `SavedByAddIn` / `DocumentTrackingId` (unzip ดู `docProps/custom.xml` หรือ Word → File → Info → Properties → Advanced Properties)
**4) Field sync ตอนสร้าง version ถัดไป** — upload v2 พร้อมแนบ `fields` (map tag→text ที่ extract จาก content controls — ในอนาคตฝั่ง add-in ทำให้อัตโนมัติ):
```bash
curl -X POST http://localhost:5075/api/documents/$DOC_ID/versions \
-H "Authorization: Bearer $TOKEN" \
-F "file=@ไฟล์แก้แล้ว.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
-F "clientVersionId=$(uuidgen)" \
-F 'fields={"employee_name":"สมชาย ใจดี มากกว่าเดิม","salary":"60000"}'
```
แล้วดู `GET /api/documents/$DOC_ID/versions` — แต่ละ version มี snapshot fields ของตัวเอง หากลบ content control ออกจาก Word ก่อน upload field นั้นจะเห็น `"missing": true` โดยค่าเดิมยังคงอยู่ (ไม่ error)
### 3) Add-in (ครบทั้ง services layer และ task pane UI)
```bash
cd addin
npx office-addin-dev-certs install # ครั้งแรกครั้งเดียว (https localhost)
npm run build # webpack build + type-check ทั้ง program
npm test # Jest — unit tests (45 ตัว)
npm run lint # eslint (ใช้ eslint.config.mjs ของโปรเจกต์)
npm run validate # ตรวจ manifest.xml
npm start # dev server https://localhost:3000 — sideload ผ่าน manifest.xml
```
**Services layer ที่มีแล้ว** ([addin/src/services/](addin/src/services/)) — ทุกตัวมี unit tests ครอบ (mock `Word.run`, `Office.context.ui`, `fetch`):
| ไฟล์ | หน้าที่ |
|---|---|
| `authService.ts` | OIDC Authorization Code + PKCE (S256) ผ่าน Office Dialog API, token ใน memory เท่านั้น, silent refresh, state กัน CSRF |
| `metadataService.ts` | อ่าน/stamp custom properties (`SavedByAddIn` ฯลฯ), Custom XML Part แบบ delete-then-add (fail เงียบๆ ตาม known bug บน Mac), `readContentControlFields()` (FR-11) |
| `fileService.ts` | ดึงไฟล์ปัจจุบันผ่าน File/Slice API ประกอบเป็น Blob — เรียก `closeAsync` เสมอ |
| `apiClient.ts` | fetch wrapper แนบ Bearer, โดน 401 → refresh + retry ครั้งเดียว, ยัง fail → `AuthRequiredError` ให้ UI กลับหน้า sign-in |
| `versioningService.ts` | uploadVersion (แนบ `fields`), getVersions, downloadVersion + getDownloadLink (presigned) |
| `versionFlow.ts` | orchestration ของ Start Versioning / Save & Version ตามลำดับ 7 ขั้นตอนของ spec |
หน้า login ของ add-in คือ [addin/src/taskpane/auth-callback.html](addin/src/taskpane/auth-callback.html) — redirect target ของ Keycloak (ตรง `https://localhost:3000/auth-callback.html` ใน realm-export.json)
**Config ฝั่ง add-in** ฉีดตอน build ผ่าน webpack DefinePlugin (อ่านจาก env — default ชี้ dev ในพื้นที่):
```bash
KEYCLOAK_AUTHORITY=https://localhost:8443/realms/word-versioning \
API_BASE_URL=https://localhost:7232 \
npm run build
```
(ตัวแปรที่เหลือ: `KEYCLOAK_CLIENT_ID` — default `word-addin-officejs`)
**Task pane UI (Phase 5)** — state machine `checking → signed-out → untracked → tracking`:
- **Sign-in gate (FR-1)** — ยังไม่ login → ปุ่ม Sign in (เปิด Keycloak ผ่าน Office Dialog API) · login แล้วตรวจ property `SavedByAddIn` ในไฟล์: ตรง → โหลด version history ทันที (ไฟล์ที่สร้างจากเว็บเข้า state นี้เลย เห็น v1 โดยไม่ต้องกดอะไร) / ไม่ตรง → ปุ่ม **Start Versioning This Document**
- **Save & Version (FR-2)** — confirm dialog → stamp properties → อัปเดต Custom XML Part → native save → ดึงไฟล์ + extract fields (FR-11) → upload → refresh history · ทุกขั้นมี loading + error ไม่มี unhandled rejection
- **Soft reminder (FR-3)** — ระหว่าง tracking ถามทุก 30 วิ ว่ามีแก้ไขที่ยังไม่ได้ version → banner เตือน (ไม่ block)
- **Version history (FR-4)** — รายการ v เวลา/ผู้สร้าง + ปุ่มดาวน์โหลด version นั้น (ขอ presigned URL อายุ 2 นาทีแล้วเปิดในเบราว์เซอร์ของเครื่อง — webview ของ Word ทิ้ง blob download แบบเงียบๆ ไฟล์จึงต้องมาจากเบราว์เซอร์จริง แล้วหาได้ในโฟลเดอร์ Downloads ของเบราว์เซอร์)
- **Session หมดอายุ (AC-9)** — 401 → silent refresh → ยังไม่ได้ → กลับหน้า sign-in พร้อมข้อความ
**Sideload เพื่อทดสอบใน Word จริง** (ต้องรัน infra + backend + `npm start` ก่อน):
- **Word Desktop (Mac/Windows)**: `cd addin && npm start` (ใช้ office-addin-debugging sideload ให้อัตโนมัติ) หรือ insert manual: Word → Insert → My Add-ins → Upload My Add-in → เลือก `addin/manifest.xml`
- **Word Online**: ไฟล์ใดก็ได้ใน OneDrive/SharePoint → Insert → Office Add-ins → Upload My Add-in → เลือก `addin/manifest.xml`
- ไล่ทดสอบตาม acceptance criteria: login ผ่าน dialog → เปิดไฟล์ใหม่เห็นปุ่ม Start Versioning → กดแล้วได้ v1 → แก้ไขแล้ว Save & Version → history เพิ่ม + ดาวน์โหลด version เก่าเปิดได้
- ⚠️ **ห้าม `cp` ทับ manifest ใน `~/Library/Containers/com.microsoft.Word/Data/Documents/wef/`** — `npm start` สร้าง symlink ไว้ การทับทำให้ครั้งถัดไปขึ้น `EEXIST` — ถาวรแก้ที่ `addin/manifest.xml` ต้นทางแล้ว restart Word (ถ้าเจอ EEXIST แล้ว: ลบไฟล์ใน wef ทิ้งแล้ว `npm start` ใหม่)
### 4) Web Portal (ยังเป็น scaffold)
```bash
cd web && npm run dev # http://localhost:5173 (ห้ามใช้ 3000 — ชน add-in)
```
logic จริง (login, template UI, dynamic form) มาตาม Phase 6
> คำแนะนำฉบับเต็ม (sideload บน Word Desktop/Online, สร้าง template แรก, สร้างเอกสารจากเว็บ) จะเติมเมื่อ implement ครบตาม Phase 8 ของแผน
## Troubleshooting (จากการทดสอบจริงบน Word for Mac)
ปัญหาที่เจอตอน sideload test รอบแรกๆ พร้อมสาเหตุ/วิธีแก้ — แก้ไขในโค้ดแล้วทั้งหมด เก็บไว้เป็นบทเรียน:
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| กด Sign in แล้ว "The HTTP protocol is not supported. Use HTTPS instead" | Office Dialog API **ปฏิเสธ http:// โดยสมบูรณ์** แต่ Keycloak dev เดิมรัน http://8080 | Keycloak เสิร์ฟ HTTPS ที่ `https://localhost:8443` ด้วย cert ของ office-addin-dev-certs (docker-compose mount `~/.office-addin-dev-certs` เข้า container) |
| Login ผ่านแต่ทุก API call ขึ้น "Load failed" | **WKWebView บล็อก fetch จากหน้า https ไป http (mixed content)** — task pane เป็น `https://localhost:3000` แต่ API เป็น `http://localhost:5075` (CORS ไม่เกี่ยว — ตรวจแล้วเรียบร้อย) | backend เปิด endpoint `https://localhost:7232` ด้วย cert ชุดเดียวกัน (`DevHttps:*` ใน `appsettings.Development.json`) — add-in ชี้อันนี้อัตโนมัติ |
| กด Start Versioning แล้ว "รูปแบบข้อมูลของ slice ไม่รองรับ" | host แต่ละตัวส่ง `slice.data` คนละรูป (Win = ArrayBuffer/view, **Mac = base64 string หรือ number[]**) | `fileService.sliceDataToBytes()` normalize ครบทั้ง 4 รูปแบบ |
| กดดาวน์โหลด version แล้ว**ไม่มีไฟล์โผล่ที่ไหนเลย** | **webview ของ Word ทิ้ง blob download (`a[download]`) แบบเงียบๆ** | ขอ presigned URL อายุ 2 นาทีจาก `download-link` แล้วเปิดใน**เบราว์เซอร์ของเครื่อง** (`openBrowserWindow`) — ไฟล์ลงโฟลเดอร์ Downloads ของเบราว์เซอร์ |
| แก้ webpack.config.js แล้วค่าเก่ายังติดอยู่ | webpack **ไม่ restart เองเมื่อ config เปลี่ยน** + dev-server ตัวเก่าถือ port 3000 อยู่ (ตัวใหม่ขึ้นไม่ได้) | kill dev-server เดิม (`lsof -ti :3000 | xargs kill`) แล้ว `npm start` ใหม่ทุกครั้งที่แก้ config |
| `npm start` ขึ้น `EEXIST ... wef/...manifest.xml` | มีการ `cp` ทับ symlink ของ sideload จนกลายเป็นไฟล์ธรรมดา | ลบไฟล์นั้นใน wef folder ทิ้งแล้ว `npm start` ใหม่ (ดูคำเตือนในหัวข้อ Sideload) |
| curl token จาก `admin-cli` แล้วเรียก backend ได้ 401 | token ของ admin-cli **ไม่มี claim `aud`** (ไม่ได้อยู่ใน realm-export จึงไม่มี audience mapper) | ใช้ `python3 scripts/dev-token.py` (เดิน PKCE จริงด้วย client `word-versioning-web`) |
**กติกาที่สรุปได้:** ใน dev ทุก service ที่ task pane (หน้า https) จะเรียก **ต้องเป็น HTTPS ด้วย cert จาก office-addin-dev-certs ทั้งหมด** — Keycloak (8443) และ backend (7232)
## เทคโนโลยี
- **Add-in**: Office.js (WordApi 1.5+), TypeScript, React, Fluent UI, webpack — scaffold ด้วย `yo office`
- **Backend**: .NET 8, EF Core + Npgsql (PostgreSQL), AWSSDK.S3 → MinIO (`ForcePathStyle = true`), DocumentFormat.OpenXml, JwtBearer
- **Web**: Nuxt.js (Vue 3), TailwindCSS (`darkMode: 'class'`)
- **Auth**: Keycloak — realm `word-versioning`, clients: `word-addin-officejs`, `word-versioning-web`, `word-addin-vsto` (public + PKCE S256 ทุกตัว)