diff --git a/.forgejo/workflows/uat.yml b/.forgejo/workflows/uat.yml index e38514c..a09aa77 100644 --- a/.forgejo/workflows/uat.yml +++ b/.forgejo/workflows/uat.yml @@ -5,10 +5,6 @@ on: tags: - "deploy*.*.*" workflow_dispatch: - inputs: - version: - description: "เวอร์ชันที่จะ deploy เช่น 1.2.3 / v1.2.3 / deploy1.2.3 — จะ map เป็น image v1.2.3 (ต้อง push tag v1.2.3 ให้ build.yml build ไว้ก่อน)" - required: true env: # Container Registry @@ -16,11 +12,11 @@ env: REGISTRY_USERNAME: ${{ vars.CONTAINER_REGISTRY_USERNAME }} REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - # Docker Image — ไม่ build เอง ใช้ image เดียวกับที่ build.yml push ไว้ - # (push tag deploy1.2.3 หรือ Run workflow จาก UI ใส่เวอร์ชัน → map เป็น image v1.2.3 เสมอ) + # Docker Image WEB_IMAGE_NAME: chamomind/ecourt-note + IMAGE_TAG: ${{ github.ref_name }} - # SSH Deployment — ต่างจาก build.yml: ชี้เครื่อง UAT (ตั้ง vars.SSH_UAT_DEPLOY_HOST เพิ่มใน Forgejo) + ไม่ build image เอง + # SSH Deployment — ต่างจาก build.yml ตัวเดียว: ชี้เครื่อง UAT (ตั้ง vars.SSH_UAT_DEPLOY_HOST เพิ่มใน Forgejo) DEPLOY_HOST: ${{ vars.SSH_UAT_DEPLOY_HOST }} DEPLOY_USER: ${{ vars.SSH_DEPLOY_USER }} @@ -89,28 +85,14 @@ jobs: echo "" echo "📚 Reference: README.md / docs/PLAN.md §3" - deploy: - name: Deploy eCourt Note Web to UAT Server + build: + name: Build Web Docker Image runs-on: ubuntu-latest needs: validate-schema - # รันได้ 2 ทาง: push tag deploy*.*.* หรือกด Run workflow จาก UI (ใส่เวอร์ชันใน input) - # !failure() && !cancelled() = ผ่านได้เมื่อ validate-schema success หรือถูก skip (กรณี dispatch) - # แต่ยังไม่รันถ้า validate-schema fail (tag ไม่อยู่บน dev) - if: >- - always() && !failure() && !cancelled() && - (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch') - outputs: - image_tag: ${{ steps.image-tag.outputs.tag }} + if: startsWith(github.ref, 'refs/tags/') steps: - - name: Resolve image tag (deploy1.2.3 / v1.2.3 → v1.2.3) - id: image-tag - run: | - RAW="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version || github.ref_name }}" - TAG="${RAW#deploy}" - TAG="${TAG#v}" - TAG="v$TAG" - echo "tag=$TAG" >> $GITHUB_OUTPUT - echo "Reuse image จาก build.yml: ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:$TAG" + - name: Checkout code + uses: actions/checkout@v4 - name: Log in to Container Registry uses: docker/login-action@v3 @@ -119,15 +101,39 @@ jobs: username: ${{ env.REGISTRY_USERNAME }} password: ${{ env.REGISTRY_PASSWORD }} - - name: Verify image exists in registry + - name: Build Docker image (no buildx) run: | - if ! docker manifest inspect "${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:${{ steps.image-tag.outputs.tag }}" > /dev/null; then - echo "❌ Image ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:${{ steps.image-tag.outputs.tag }} ไม่มีใน registry" - echo "workflow นี้ไม่ build เอง — push tag ${{ steps.image-tag.outputs.tag }} ก่อน เพื่อให้ build.yml build + push image ให้" - exit 1 - fi - echo "✅ Image found — deploy ด้วย image เดียวกับ build.yml" + # Build using standard docker build (not buildx) + git reset --hard HEAD + cd ${{ github.workspace }} + docker build \ + --no-cache \ + --pull \ + --file ./docker/web.Dockerfile \ + --tag ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ + --tag ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:latest \ + . + echo "✅ Build successful!" + docker images | grep ${{ env.WEB_IMAGE_NAME }} + + - name: Push Docker image + run: | + docker push ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:${{ env.IMAGE_TAG }} + docker push ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:latest + echo "✅ Image pushed to registry" + + - name: Test image can run + run: | + # Quick test that image can start + docker run --rm ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:${{ env.IMAGE_TAG }} node --version + + deploy: + name: Deploy eCourt Note Web to UAT Server + runs-on: ubuntu-latest + needs: build + if: startsWith(github.ref, 'refs/tags/') + steps: - name: Deploy web on UAT server uses: appleboy/ssh-action@v1.2.1 with: @@ -139,13 +145,13 @@ jobs: cd ~/repo chmod +x ./replace-env.sh ./deploy.sh - ./replace-env.sh ECOURT_NOTE_TAG "${{ steps.image-tag.outputs.tag }}" + ./replace-env.sh ECOURT_NOTE_TAG "${{ env.IMAGE_TAG }}" ./deploy.sh notify: name: Notify Deployment Status runs-on: ubuntu-latest - needs: [deploy] + needs: [build, deploy] if: always() steps: - name: Send Discord notification - Success @@ -165,7 +171,7 @@ jobs: }, { "name": "Tag", - "value": "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version || github.ref_name }}", + "value": "${{ github.ref_name }}", "inline": true }, { @@ -175,7 +181,7 @@ jobs: }, { "name": "Image Tag", - "value": "`${{ needs.deploy.outputs.image_tag }}`", + "value": "`${{ env.IMAGE_TAG }}`", "inline": true }, { @@ -198,7 +204,7 @@ jobs: ${{ env.DISCORD_WEBHOOK_URL }} - name: Send Discord notification - Failure - if: ${{ needs.deploy.result == 'failure' }} + if: ${{ needs.deploy.result == 'failure' || needs.build.result == 'failure' }} run: | curl -H "Content-Type: application/json" \ -d '{ @@ -224,7 +230,7 @@ jobs: }, { "name": "Failed Job", - "value": "Deploy", + "value": "${{ needs.build.result == 'failure' && 'Build' || 'Deploy' }}", "inline": true }, { diff --git a/CLAUDE.md b/CLAUDE.md index 1681511..41b5d9f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,8 +41,7 @@ lib/server-pdf.ts ← path guard อ่าน PDF จาก public/ (export lib/session.ts ← getSessionUser — identity จาก cookie oacCase (mock login e-Court) utils/coordinates.ts ← pixel⇄% + getPointerPercent + clamp + mergeRectsByLine utils/geometry.ts ← bbox/translate/scale/hitTestPath ของ annotation (pure, หน่วย %) -utils/annotation-display.ts ← label/ข้อความย่อต่อชนิด + ค่า default TEXTBOX/STAMP + describeAnnotationChange สรุปสิ่งที่เปลี่ยนของ version history F19 (sidebar/สรุป/CSV ใช้ร่วม) -utils/date-format.ts ← formatThaiDate วันที่ไทยแบบเต็ม "18 กันยายน พ.ศ. 2569" (+เวลา "… 16:33:00 น.") — ทุกจุดที่แสดงวันที่ใน UI ใช้ร่วม (เว้น export CSV ที่ lock Asia/Bangkok) +utils/annotation-display.ts ← label/ข้อความย่อต่อชนิด + ค่า default TEXTBOX/STAMP (sidebar/สรุป/CSV ใช้ร่วม) utils/pdf-export.ts ← แปลงพิกัดจอ⇄PDF (/Rotate) + wrapText + hexToRgb (export route) utils/user.ts ← parse/build ค่า cookie oacCase → OacUser {userId: u_userid, userName: u_userlogin} (pure module — middleware ใช้ร่วมได้) utils/base-path.ts ← withBasePath() เติม basePath /docedit/note ให้ URL ฝั่ง browser ที่ Next ไม่เติมให้เอง (ค่าจาก next.config.mjs ผ่าน env — invariant #12) diff --git a/app/actions/annotations.ts b/app/actions/annotations.ts index 6aea7b9..2217211 100644 --- a/app/actions/annotations.ts +++ b/app/actions/annotations.ts @@ -30,10 +30,6 @@ import { type StampData, type TextboxData, } from "@/db/schema"; -import { - MAX_BOOKMARK_NOTE_CHARS, - MAX_BOOKMARK_TITLE_CHARS, -} from "@/utils/bookmark-limits"; // ─── Input types (client ใช้ตอนเรียก action) ──────────────────────────────── @@ -92,6 +88,8 @@ const SHAPE_KINDS: readonly string[] = ["rect", "ellipse", "line", "arrow"]; // ตรง ๆ ด้วยข้อความขนาดหลาย MB จน JSONB บวมและ SSE event ใหญ่ผิดปกติ const MAX_USER_NAME_CHARS = 100; const MAX_TEXT_CHARS = 5_000; // message/text/note/label ของ annotation + reply +const MAX_BOOKMARK_TITLE_CHARS = 120; // ให้ตรงค่าที่ UI ตัดไว้ +const MAX_BOOKMARK_NOTE_CHARS = 200; const MAX_QUERY_CHARS = 200; // คำค้นหา (F14) function fieldError(field: string, expect: string): never { diff --git a/app/page.tsx b/app/page.tsx index de03430..7ed5772 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,7 +8,6 @@ import { files } from "@/db/schema"; import SessionBar from "@/components/SessionBar"; import UploadFileForm from "@/components/UploadFileForm"; import { imageFormatFromUrl } from "@/utils/file-kind"; -import { formatThaiDate } from "@/utils/date-format"; import { getSessionUser } from "@/lib/session"; export const dynamic = "force-dynamic"; @@ -96,7 +95,7 @@ export default async function HomePage() { {kindBadge(f.pdfUrl)} - {formatThaiDate(f.createdAt, true)} + {new Date(f.createdAt).toLocaleString("th-TH")} {/* ปุ่มเข้า annotate แทนลิงก์กางเต็มการ์ด (M4) ตาม feedback — เป้าจิ้ม 36px (H4) */} diff --git a/components/pdf/PDFViewer.tsx b/components/pdf/PDFViewer.tsx index 6b5c939..7a6a30b 100644 --- a/components/pdf/PDFViewer.tsx +++ b/components/pdf/PDFViewer.tsx @@ -114,13 +114,8 @@ import { type Size, } from "@/utils/coordinates"; import type { OacUser } from "@/utils/user"; -import { - MAX_BOOKMARK_NOTE_CHARS, - MAX_BOOKMARK_TITLE_CHARS, -} from "@/utils/bookmark-limits"; import { fileKindFromUrl, imageFormatFromUrl } from "@/utils/file-kind"; import { withBasePath } from "@/utils/base-path"; -import { formatThaiDate } from "@/utils/date-format"; import { decodeTiffPages } from "@/utils/tiff-client"; import { TEXTBOX_DEFAULT_WIDTH_PCT, @@ -201,13 +196,10 @@ const HIGHLIGHT_COLORS = ["#fde047", "#86efac", "#93c5fd", "#f9a8d4", "#fdba74"] /** ชื่อ bookmark ที่มาจากข้อความ (คลุม/ข้อความรายการ) — ล้างขยะของ textLayer ไทย * (sanitizePdfText ครอบด้วยข้อความจาก DB เก่าที่ยังไม่ผ่านการล้าง) ยุบ whitespace - * และตัดยาวไม่เกิน MAX_BOOKMARK_TITLE_CHARS ตัวอักษร (รวม … ที่ต่อท้าย — - * เดิม slice 120 แล้วต่อ … กลายเป็น 121 ตัว โดน limit ของ server ตีกลับตอนบันทึก) */ + * และตัดยาวไม่เกิน 120 ตัวอักษร */ function normalizeBookmarkTitle(s: string): string { const t = sanitizePdfText(s).replace(/\s+/g, " ").trim(); - return t.length > MAX_BOOKMARK_TITLE_CHARS - ? `${t.slice(0, MAX_BOOKMARK_TITLE_CHARS - 1)}…` - : t; + return t.length > 120 ? `${t.slice(0, 120)}…` : t; } /** ความสูง sticky header ของ viewer — ใช้เว้นระยะเวลาเลื่อนหน้าไม่ให้เป้าหมายถูก header บัง */ @@ -500,7 +492,12 @@ function ReplyBox({

{r.userName} - {formatThaiDate(r.createdAt, true)} + {new Date(r.createdAt).toLocaleString("th-TH", { + hour: "2-digit", + minute: "2-digit", + day: "2-digit", + month: "short", + })}

{r.message}

@@ -1078,7 +1075,7 @@ function PdfPage({

)}

- {formatThaiDate(a.createdAt, true)} + {new Date(a.createdAt).toLocaleString("th-TH")}

{/* F10/A5: toggle จัดการแล้ว — ใครก็ได้ (D6) */}