diff --git a/frontend/app/(admin)/admin/ai/page.tsx b/frontend/app/(admin)/admin/ai/page.tsx index 601524bd..21b9307c 100644 --- a/frontend/app/(admin)/admin/ai/page.tsx +++ b/frontend/app/(admin)/admin/ai/page.tsx @@ -8,12 +8,13 @@ // - 2026-05-25: เพิ่ม AI Model Management UI สำหรับเลือกโมเดลแบบไดนามิก (ADR-027). // - 2026-05-30: นำเข้าและแสดงผล OcrEngineSelector component ใน Overview tab (T019, T020) // - 2026-06-02: เพิ่มตัวบ่งชี้โมเดลหลักที่กำลังใช้งาน (Active Global Model badge) บนการ์ด System Toggle (T010, ADR-033) +// - 2026-06-13: [235] ลบ AI Model Management (ADR-027) และ OCR Engine Selector ออก; แก้ System Toggle แสดง canonical names (np-dms-ai/np-dms-ocr); แก้ label OCR Sidecar 'use client'; import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { Brain, Loader2, Power, ShieldCheck, Cpu, Database, Activity, Search, Info, HelpCircle, AlertCircle, Settings2, Trash2, ScanText } from 'lucide-react'; +import { Brain, Loader2, Power, ShieldCheck, Cpu, Database, Activity, Search, Info, HelpCircle, AlertCircle, ScanText } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -27,12 +28,10 @@ import { projectService } from '@/lib/services/project.service'; import { adminAiService, AiSandboxJobResult, - AiAvailableModel, AiRagCitation, } from '@/lib/services/admin-ai.service'; import { toast } from 'sonner'; import OcrSandboxPromptManager from '@/components/admin/ai/OcrSandboxPromptManager'; -import OcrEngineSelector from '@/components/admin/ai/OcrEngineSelector'; interface SandboxProject { publicId: string; @@ -96,6 +95,13 @@ function normalizeLoadedModels(value: unknown): VramLoadedModelView[] { }); } +function toCanonicalModel(rawName: string): string { + const name = rawName.toLowerCase(); + if (name.includes('ocr') || name.includes('typhoon-np-dms-ocr')) return 'np-dms-ocr'; + if (name.includes('typhoon') || name.includes('np-dms-ai')) return 'np-dms-ai'; + return rawName; +} + export default function AiAdminConsolePage() { const { data, isLoading, isError, refetch, isFetching } = useAiStatus(); const { data: health, isLoading: isHealthLoading, refetch: refetchHealth } = useAiHealth(); @@ -110,16 +116,6 @@ export default function AiAdminConsolePage() { const [sandboxProgress, setSandboxProgress] = useState(0); const [sandboxStatusText, setSandboxStatusText] = useState(''); - // AI Model Management State (ADR-027) - const { data: aiModelsData, refetch: refetchModels } = useQuery<{ models: AiAvailableModel[]; activeModel: string }>({ - queryKey: ['ai-available-models'], - queryFn: async () => { - return await adminAiService.getAvailableModels(); - }, - }); - const availableModels = ensureArray(aiModelsData?.models); - const activeModel = aiModelsData?.activeModel ?? ''; - // VRAM Monitoring State (T034, T036, US2) const { data: vramStatus, refetch: refetchVram } = useQuery({ queryKey: ['ai-vram-status'], @@ -154,44 +150,8 @@ export default function AiAdminConsolePage() { await toggleMutation.mutateAsync(enabled); }; - const handleModelChange = async (modelId: string): Promise => { - try { - const selectedModel = availableModels.find(m => m.modelId === modelId || String(m.id) === modelId); - const name = selectedModel?.modelName || modelId; - await adminAiService.setActiveModel(modelId); - toast.success(`เปลี่ยนโมเดลเป็น ${name} สำเร็จ`); - await refetchModels(); - refetchVram(); - } catch (err: unknown) { - const errorResponse = err as { response?: { data?: { message?: string } } }; - const errorMsg = errorResponse.response?.data?.message || 'ไม่สามารถเปลี่ยนโมเดลได้เนื่องจาก VRAM ไม่เพียงพอ'; - toast.error(errorMsg); - } - }; - - const handleToggleModel = async (modelName: string): Promise => { - try { - await adminAiService.toggleModelActive(modelName); - toast.success(`เปลี่ยนสถานะโมเดล ${modelName} สำเร็จ`); - await refetchModels(); - } catch { - toast.error('ไม่สามารถเปลี่ยนสถานะโมเดลได้'); - } - }; - - const handleRemoveModel = async (modelName: string): Promise => { - if (!confirm(`ต้องการลบโมเดล ${modelName} ใช่หรือไม่?`)) return; - try { - await adminAiService.removeModel(modelName); - toast.success(`ลบโมเดล ${modelName} สำเร็จ`); - await refetchModels(); - } catch { - toast.error('ไม่สามารถลบโมเดลได้'); - } - }; - const handleRefreshAll = async (): Promise => { - await Promise.all([refetch(), refetchHealth(), refetchModels(), refetchVram()]); + await Promise.all([refetch(), refetchHealth(), refetchVram()]); }; const handleSubmitSandbox = async (e: React.FormEvent): Promise => { @@ -368,7 +328,7 @@ export default function AiAdminConsolePage() { - OCR Sidecar (Tesseract) + OCR Sidecar (np-dms-ocr) {isHealthLoading ? : renderStatusBadge(health?.ocr?.status)} @@ -496,7 +456,7 @@ export default function AiAdminConsolePage() { - + @@ -513,10 +473,14 @@ export default function AiAdminConsolePage() {
Superadmin ยังสามารถเข้าถึงส่วนทดสอบและดูแลระบบได้ตามสิทธิ์
-
- Active Global Model: +
+ Active Models: - {activeModel || 'Loading...'} + {isHealthLoading ? 'Loading...' : toCanonicalModel(health?.activeModels?.main ?? 'np-dms-ai')} + + + + + {isHealthLoading ? 'Loading...' : toCanonicalModel(health?.activeModels?.ocr ?? 'np-dms-ocr')}
@@ -538,114 +502,6 @@ export default function AiAdminConsolePage() {
- {/* AI Model Management Card (ADR-027) */} - - - - - AI Model Management - ADR-027 - - - -
-
- - -
-
- โมเดลปัจจุบัน: {activeModel || 'Loading...'} -
-
- -
-

รายการโมเดลทั้งหมด

-
- {availableModels.length === 0 ? ( -

ไม่มีโมเดลในระบบ

- ) : ( - availableModels.map((model) => ( -
-
- - {model.isActive ? 'Active' : 'Inactive'} - - {model.modelName} - {model.isDefault && ( - Default - )} - {activeModel === model.modelName && ( - Current - )} - {model.vramRequirementMB && ( - - {Math.round(model.vramRequirementMB / 1024 * 10) / 10} GB VRAM - - )} -
-
- {!model.isDefault && ( - <> - - - - )} -
-
- )) - )} -
-
-
-
- - {/* OCR Engine Management Card (ADR-032) */} - -
@@ -675,7 +531,7 @@ export default function AiAdminConsolePage() {
- + diff --git a/specs/200-fullstacks/235-ai-runtime-policy-refactor/quickstart.md b/specs/200-fullstacks/235-ai-runtime-policy-refactor/quickstart.md index 4357ed3c..02cda9d0 100644 --- a/specs/200-fullstacks/235-ai-runtime-policy-refactor/quickstart.md +++ b/specs/200-fullstacks/235-ai-runtime-policy-refactor/quickstart.md @@ -1,6 +1,7 @@ // File: specs/200-fullstacks/235-ai-runtime-policy-refactor/quickstart.md // Change Log: // - 2026-06-11: Verification quickstart for AI Runtime Policy Refactor +// - 2026-06-12: เพิ่ม PowerShell syntax และ environment variable setup # Quickstart: AI Runtime Policy Refactor — Verification Guide @@ -11,51 +12,188 @@ - Ollama running with `np-dms-ai` and `np-dms-ocr` tags registered - Admin user token available +## Environment Setup + +### การเข้าถึง Backend (สำคัญ) + +จาก `@/specs/04-Infrastructure-OPS/04-00-docker-compose/QNAP/app/docker-compose-app.yml`: + +| Environment | Backend URL | ใช้เมื่อ | +|-------------|-------------|----------| +| **Production (QNAP + NPM)** | `https://backend.np-dms.work/api` | ทดสอบจากเครื่องภายนอก (WSL, บ้าน) | +| **QNAP Internal** | `http://backend:3000` | ทดสอบจากภายใน QNAP (docker network) | +| **Local dev** | `http://localhost:3001` | รัน backend บนเครื่องตัวเอง | + +**หมายเหตุ:** Backend container ใช้ port **3000** (ไม่ใช่ 3001) และอยู่ behind nginx proxy manager + +### Bash (Linux/macOS/Git Bash on Windows) + +```bash +# สำหรับ Production QNAP (ผ่าน HTTPS + NPM) +export BACKEND_URL="https://backend.np-dms.work/api" + +# หรือถ้า SSH tunnel ไป QNAP แล้ว +# export BACKEND_URL="http://localhost:3000" + +export TOKEN="your-jwt-token-here" +``` + +### PowerShell (Windows) + +```powershell +# สำหรับ Production QNAP (ผ่าน HTTPS + NPM) +$env:BACKEND_URL = "https://backend.np-dms.work/api" + +# หรือถ้า SSH tunnel ไป QNAP แล้ว +# $env:BACKEND_URL = "http://localhost:3000" + +$env:TOKEN = "your-jwt-token-here" +``` + +--- + +### วิธีหา TOKEN + +**วิธีที่ 1: Login ผ่าน API (Bash)** + +```bash +# Login แล้วดึง token จาก response +# หมายเหตุ: Backend ใช้ 'username' (ไม่ใช่ email) ใน login field +RESPONSE=$(curl -s -X POST "$BACKEND_URL/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username": "admin", "password": "Center2025"}') + +# วิธีดึง TOKEN (เลือก 1 จาก 3): + +# วิธี 1: ใช้ jq (ถ้าติดตั้งแล้ว) +# export TOKEN=$(echo $RESPONSE | jq -r '.access_token') + +# วิธี 2: ใช้ Python (ทั่วไปมีอยู่แล้ว) — แนะนำ +export TOKEN=$(echo $RESPONSE | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['access_token'])") + +# วิธี 3: ดู response แล้ว copy เอง (ถ้าไม่มีทั้ง jq และ Python) +# echo $RESPONSE +# export TOKEN="paste_token_here" +``` + +**วิธีที่ 2: ดึงจาก Browser DevTools** + +1. เปิด browser ไปที่ `http://192.168.10.8:3000` (frontend) +2. Login ด้วย account ที่มีสิทธิ์ admin +3. กด F12 → Network tab +4. รีเฟรชหน้า หรือ ทำ action ใดก็ได้ +5. ดู request ที่ส่งไป backend → Headers → `Authorization: Bearer eyJhbG...` +6. Copy ค่าหลัง `Bearer ` มาใส่ใน `$TOKEN` + +**วิธีที่ 3: ถ้ามี Access ตรงกับ Database** + +```sql +-- ดู username ที่มี role = 'admin' (หลังจากนั้นต้อง login ผ่าน API เพื่อเอา token) +SELECT username FROM users WHERE role = 'admin' LIMIT 1; +``` + +--- + +### Default Users (จาก Seed Data) + +ถ้าใช้ seed data เริ่มต้น มี users นี้ให้ใช้: + +| Username | Role | Password | +|----------|------|----------| +| `superadmin` | Superadmin | `Center2025` | +| `admin` | Org Admin | `Center2025` | +| `editor01` | Editor | `Center2025` | +| `viewer01` | Viewer | `Center2025` | + --- ## Gate 1: Policy Contract Verification ### 1A. Reject model.key (should return 400) +**Bash:** ```bash -curl -X POST http://localhost:3001/api/ai/jobs \ +curl -X POST "$BACKEND_URL/ai/jobs" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"type": "rag-query", "model": {"key": "typhoon2.5-np-dms:latest"}}' \ - | jq '.statusCode, .message' + | python3 -c "import sys, json; d=json.load(sys.stdin); e=d.get('error', {}); print(e.get('statusCode'), e.get('message'))" +# Expected: 400, message about model.key not allowed +``` + +**PowerShell:** +```powershell +$body = '{"type": "rag-query", "model": {"key": "typhoon2.5-np-dms:latest"}}' +Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{ + "Authorization" = "Bearer $env:TOKEN" + "Content-Type" = "application/json" +} -Body $body | Select-Object statusCode, message # Expected: 400, message about model.key not allowed ``` ### 1B. Reject parameter overrides (should return 400) +**Bash:** ```bash -curl -X POST http://localhost:3001/api/ai/jobs \ +curl -X POST "$BACKEND_URL/ai/jobs" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"type": "rag-query", "temperature": 0.9}' \ - | jq '.statusCode' + | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('error', {}).get('statusCode'))" +# Expected: 400 +``` + +**PowerShell:** +```powershell +$body = '{"type": "rag-query", "temperature": 0.9}' +(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{ + "Authorization" = "Bearer $env:TOKEN" + "Content-Type" = "application/json" +} -Body $body).statusCode # Expected: 400 ``` ### 1C. Valid executionProfile (should return 201) +**Bash:** ```bash -curl -X POST http://localhost:3001/api/ai/jobs \ +curl -X POST "$BACKEND_URL/ai/jobs" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - -d '{"type": "rag-query", "executionProfile": "balanced", "documentPublicId": ""}' \ - | jq '.data.modelUsed' + -d '{"type": "rag-query", "executionProfile": "balanced", "documentPublicId": ""}' \ + | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('data', {}).get('modelUsed'))" +# Expected: "np-dms-ai" +``` + +**PowerShell:** +```powershell +$body = '{"type": "rag-query", "executionProfile": "balanced", "documentPublicId": ""}' +(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{ + "Authorization" = "Bearer $env:TOKEN" + "Content-Type" = "application/json" +} -Body $body).data.modelUsed # Expected: "np-dms-ai" ``` ### 1D. large-context by non-admin (should return 403) +**Bash:** ```bash -curl -X POST http://localhost:3001/api/ai/jobs \ +curl -X POST "$BACKEND_URL/ai/jobs" \ -H "Authorization: Bearer $NON_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"type": "rag-query", "executionProfile": "large-context"}' \ - | jq '.statusCode' + | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('error', {}).get('statusCode'))" +# Expected: 403 +``` + +**PowerShell:** +```powershell +$body = '{"type": "rag-query", "executionProfile": "large-context"}' +(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{ + "Authorization" = "Bearer $env:NON_ADMIN_TOKEN" + "Content-Type" = "application/json" +} -Body $body).statusCode # Expected: 403 ``` @@ -103,18 +241,34 @@ docker logs ocr-sidecar --tail 20 ### 4A. Force GPU pressure then run RAG +**Step 1: Force load large model (Bash)** ```bash -# 1. Force load large model -curl http://localhost:11434/api/generate -d '{"model":"np-dms-ai","prompt":"warmup","keep_alive":-1}' +# ถ้า Ollama รันบน Desk-5439 (192.168.10.100) +curl http://192.168.10.100:11434/api/generate -d '{"model":"np-dms-ai","prompt":"warmup","keep_alive":-1}' +``` -# 2. Run RAG query -curl -X POST http://localhost:3001/api/ai/jobs \ +**Step 2: Run RAG query** + +*Bash:* +```bash +curl -X POST "$BACKEND_URL/ai/jobs" \ -H "Authorization: Bearer $TOKEN" \ -d '{"type":"rag-query","executionProfile":"balanced","documentPublicId":""}' \ | jq '.data.status' # Expected: "completed" (ไม่ fail) +``` -# 3. ตรวจ sidecar log +*PowerShell:* +```powershell +$body = '{"type":"rag-query","executionProfile":"balanced","documentPublicId":""}' +(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{ + "Authorization" = "Bearer $env:TOKEN" +} -Body $body).data.status +# Expected: "completed" (ไม่ fail) +``` + +**Step 3: ตรวจ sidecar log** +```bash docker logs ocr-sidecar --tail 20 # Expected: device=cpu reason=gpu-headroom-below-threshold ```