diff --git a/.forgejo/workflows/uat.yml b/.forgejo/workflows/uat.yml index a09aa77..e38514c 100644 --- a/.forgejo/workflows/uat.yml +++ b/.forgejo/workflows/uat.yml @@ -5,6 +5,10 @@ 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 @@ -12,11 +16,11 @@ env: REGISTRY_USERNAME: ${{ vars.CONTAINER_REGISTRY_USERNAME }} REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - # Docker Image + # Docker Image — ไม่ build เอง ใช้ image เดียวกับที่ build.yml push ไว้ + # (push tag deploy1.2.3 หรือ Run workflow จาก UI ใส่เวอร์ชัน → map เป็น image v1.2.3 เสมอ) WEB_IMAGE_NAME: chamomind/ecourt-note - IMAGE_TAG: ${{ github.ref_name }} - # SSH Deployment — ต่างจาก build.yml ตัวเดียว: ชี้เครื่อง UAT (ตั้ง vars.SSH_UAT_DEPLOY_HOST เพิ่มใน Forgejo) + # SSH Deployment — ต่างจาก build.yml: ชี้เครื่อง UAT (ตั้ง vars.SSH_UAT_DEPLOY_HOST เพิ่มใน Forgejo) + ไม่ build image เอง DEPLOY_HOST: ${{ vars.SSH_UAT_DEPLOY_HOST }} DEPLOY_USER: ${{ vars.SSH_DEPLOY_USER }} @@ -85,14 +89,28 @@ jobs: echo "" echo "📚 Reference: README.md / docs/PLAN.md §3" - build: - name: Build Web Docker Image + deploy: + name: Deploy eCourt Note Web to UAT Server runs-on: ubuntu-latest needs: validate-schema - if: startsWith(github.ref, 'refs/tags/') + # รันได้ 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 }} steps: - - name: Checkout code - uses: actions/checkout@v4 + - 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: Log in to Container Registry uses: docker/login-action@v3 @@ -101,39 +119,15 @@ jobs: username: ${{ env.REGISTRY_USERNAME }} password: ${{ env.REGISTRY_PASSWORD }} - - name: Build Docker image (no buildx) + - name: Verify image exists in registry run: | - # 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 \ - . + 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" - 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: @@ -145,13 +139,13 @@ jobs: cd ~/repo chmod +x ./replace-env.sh ./deploy.sh - ./replace-env.sh ECOURT_NOTE_TAG "${{ env.IMAGE_TAG }}" + ./replace-env.sh ECOURT_NOTE_TAG "${{ steps.image-tag.outputs.tag }}" ./deploy.sh notify: name: Notify Deployment Status runs-on: ubuntu-latest - needs: [build, deploy] + needs: [deploy] if: always() steps: - name: Send Discord notification - Success @@ -171,7 +165,7 @@ jobs: }, { "name": "Tag", - "value": "${{ github.ref_name }}", + "value": "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version || github.ref_name }}", "inline": true }, { @@ -181,7 +175,7 @@ jobs: }, { "name": "Image Tag", - "value": "`${{ env.IMAGE_TAG }}`", + "value": "`${{ needs.deploy.outputs.image_tag }}`", "inline": true }, { @@ -204,7 +198,7 @@ jobs: ${{ env.DISCORD_WEBHOOK_URL }} - name: Send Discord notification - Failure - if: ${{ needs.deploy.result == 'failure' || needs.build.result == 'failure' }} + if: ${{ needs.deploy.result == 'failure' }} run: | curl -H "Content-Type: application/json" \ -d '{ @@ -230,7 +224,7 @@ jobs: }, { "name": "Failed Job", - "value": "${{ needs.build.result == 'failure' && 'Build' || 'Deploy' }}", + "value": "Deploy", "inline": true }, { diff --git a/CLAUDE.md b/CLAUDE.md index 41b5d9f..1681511 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,8 @@ 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 (sidebar/สรุป/CSV ใช้ร่วม) +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/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 2217211..6aea7b9 100644 --- a/app/actions/annotations.ts +++ b/app/actions/annotations.ts @@ -30,6 +30,10 @@ 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) ──────────────────────────────── @@ -88,8 +92,6 @@ 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 7ed5772..de03430 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,6 +8,7 @@ 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"; @@ -95,7 +96,7 @@ export default async function HomePage() { {kindBadge(f.pdfUrl)} - {new Date(f.createdAt).toLocaleString("th-TH")} + {formatThaiDate(f.createdAt, true)} {/* ปุ่มเข้า annotate แทนลิงก์กางเต็มการ์ด (M4) ตาม feedback — เป้าจิ้ม 36px (H4) */} diff --git a/components/pdf/PDFViewer.tsx b/components/pdf/PDFViewer.tsx index 7a6a30b..6b5c939 100644 --- a/components/pdf/PDFViewer.tsx +++ b/components/pdf/PDFViewer.tsx @@ -114,8 +114,13 @@ 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, @@ -196,10 +201,13 @@ const HIGHLIGHT_COLORS = ["#fde047", "#86efac", "#93c5fd", "#f9a8d4", "#fdba74"] /** ชื่อ bookmark ที่มาจากข้อความ (คลุม/ข้อความรายการ) — ล้างขยะของ textLayer ไทย * (sanitizePdfText ครอบด้วยข้อความจาก DB เก่าที่ยังไม่ผ่านการล้าง) ยุบ whitespace - * และตัดยาวไม่เกิน 120 ตัวอักษร */ + * และตัดยาวไม่เกิน MAX_BOOKMARK_TITLE_CHARS ตัวอักษร (รวม … ที่ต่อท้าย — + * เดิม slice 120 แล้วต่อ … กลายเป็น 121 ตัว โดน limit ของ server ตีกลับตอนบันทึก) */ function normalizeBookmarkTitle(s: string): string { const t = sanitizePdfText(s).replace(/\s+/g, " ").trim(); - return t.length > 120 ? `${t.slice(0, 120)}…` : t; + return t.length > MAX_BOOKMARK_TITLE_CHARS + ? `${t.slice(0, MAX_BOOKMARK_TITLE_CHARS - 1)}…` + : t; } /** ความสูง sticky header ของ viewer — ใช้เว้นระยะเวลาเลื่อนหน้าไม่ให้เป้าหมายถูก header บัง */ @@ -492,12 +500,7 @@ function ReplyBox({
{r.userName} - {new Date(r.createdAt).toLocaleString("th-TH", { - hour: "2-digit", - minute: "2-digit", - day: "2-digit", - month: "short", - })} + {formatThaiDate(r.createdAt, true)}
{r.message}
@@ -1075,7 +1078,7 @@ function PdfPage({ )}- {new Date(a.createdAt).toLocaleString("th-TH")} + {formatThaiDate(a.createdAt, true)}
{/* F10/A5: toggle จัดการแล้ว — ใครก็ได้ (D6) */}