بنقرة واحدة
pm-system-deployment
PM System deployment and development guide
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
PM System deployment and development guide
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Dev-agent / checker-agent collaboration loop driven by downstream project docs/STATE.md. Dev agent implements and records work items; an independent checker agent verifies each item with real lint/typecheck/test/build evidence and writes findings back; loop continues until all items are VERIFIED or escalation limits are hit.
Synchronizes review feedback, QA findings, user corrections, and code-review suggestions into durable project docs before Build, merge, or ship.
Intake workflow for existing projects before continuing development. Analyze current source state, derive a truthful docs baseline, identify QA/regression gaps, and create a safe continuation plan.
防止修復過的 bug 重新出現(regression)。 規則:每個修過的 bug 必須留下「regression test」+「root cause note」+「為何會發生」分析, 確保日後 refactor / 改需求時唔會重新踩坑。 David 在 2026-06-06 kanban task 明確指出「舊的 bug 又出現」嘅困擾。
定時壓縮長時間任務的 context,避免 token 爆炸。寫入 context-summary.md 保留決策和當前狀態,丢棄細節。
Task Orchestration Subagent — Coordinates all subagents, manages task board, handles parallel/sequential execution, tracks dependencies and failures. The conductor of the development orchestra.
| name | pm-system-deployment |
| description | PM System deployment and development guide |
~/projects/pm-system/
# Backend
cd ~/projects/pm-system/backend
bun run src/index.ts # Runs on :4000
# Frontend
cd ~/projects/pm-system/frontend
bun run dev # Runs on :3000
cd ~/projects/pm-system/infra
sudo docker compose up -d --build
docker compose up 嘅 workaround(2026-06-08)terminal foreground 會攔 docker compose up(佢當 long-lived process)。
正確做法:
# 1. 用 background + notify_on_complete 起 docker
terminal(command="cd ~/www/pm-system && docker compose up -d 2>&1 | tail -5",
background=true, notify_on_complete=true)
# → 攞 session_id
# 2. process poll 追 readiness
process(action="poll", session_id=..., timeout=90)
# → 等 "Container ... Healthy"
# 3. 後續 terminal 跑 test(eg. curl / npm test)
Anti-pattern:用 nohup / disown / trailing & 喺 foreground mode
= 個 process 會被 Hermes 殺,測試失敗原因難追。
# 自動由 docker compose 做(seed 自動 run):
# - bunx prisma db push
# - bunx prisma db seed (createRole + 5 builtInUsers + agentUser)
# - bun src/index.ts
# 唔需要 manual psql / manual insert
# ⚠️ 2026-06-08: 舊嘅「Create Admin User」section 嘅 SQL insert 路徑
# 已過時 — seed 已經處理晒。Initial Setup 只係 docker compose up。
@import "tailwindcss" instead of @tailwind directives@theme {} block for custom colors instead of tailwind.config.js@tailwindcss/postcssfiles: []ignoreDeprecations: "6.0" to suppress baseUrl deprecation@prisma/adapter-pg with pg Pool for connection poolingprisma.config.ts (not schema url field)When adding new models to the Prisma schema:
schema.prisma with new modelnpx prisma db push — updates the database schema (does NOT regenerate client)npx prisma generate — regenerates the Prisma client types (MUST run separately!)bun build src/index.ts --outdir=dist --target=bunCommon bug: After db push, if new model fields are undefined, it's because prisma generate was never run. The error looks like:
undefined is not an object (evaluating 'prisma.wikiPage.findMany')
Always verify: Check node_modules/@prisma/client/index.d.ts contains your new model, or inside the container: grep WikiPage /app/node_modules/@prisma/client/index.d.ts
The Dockerfile has CMD ["bun", "src/index.ts"]. If you rebuild with bun build (outputting to dist/), you must either:
CMD ["bun", "dist/index.js"] AND rebuild the image, ORdist/index.js into the running container manuallyWhen the container's entrypoint/CMD runs bun src/index.ts (TypeScript source) but the Prisma client was generated for the bundled output, you'll get silent failures — the server starts but routes that use new Prisma models return empty/undefined.
sudo usermod -aG docker $USERsudo docker compose up -d --buildprisma.config.ts 三層 pitfall(RG-011, 2026-06-09)升級到 Prisma 7 之後,schema.prisma 入面嘅 url = env("DATABASE_URL") 已 deprecated,
要搬去 prisma.config.ts。呢個 migration 撞 3 層 pitfall,Dockerfile 漏 COPY
就係其中最 hidden 嘅一條:
Pitfall 1 — Dockerfile 漏 COPY prisma.config.ts(blocker)
prisma.config.ts 喺 backend 根目錄,唔喺 prisma/ sub-folder。如果 Dockerfile
只 COPY prisma ./prisma 唔 COPY backend root 嘅 prisma.config.ts,image 入面冇
呢個 file。Container start 跑 prisma db push / prisma migrate deploy 時 throw:
Error: The datasource.url property is required in your Prisma config file
when using prisma db push.
Container exit(1),docker compose up -d build OK 但 backend 完全起唔到。
Frontend 嗰邊 healthy(只 serve static file)所以容易 miss backend 死咗。
Fix:
# builder stage
COPY prisma.config.ts ./ # ← 必須加
RUN DATABASE_URL=postgresql://build:***@localhost:5432/build bunx prisma generate
# runtime stage
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
Pitfall 2 — prisma.config.ts 必須用 env() helper(strict config validation)
Prisma 7 config validator 唔 accept process.env["DATABASE_URL"] 嘅 generic
return type。要求用 prisma/config 提供嘅 env() helper:
// ❌ 唔 work
import { defineConfig } from "prisma/config";
export default defineConfig({
datasource: { url: process.env["DATABASE_URL"] },
});
// ✅ Work
import { defineConfig, env } from "prisma/config";
export default defineConfig({
datasource: { url: env("DATABASE_URL") },
});
Pitfall 3 — prisma generate 撞 missing DATABASE_URL 喺 builder stage
雖然 prisma generate 本身唔需要 DATABASE_URL,但 env("DATABASE_URL") 喺
config load 時 strict throw 假若 var undefined。所以 builder stage 必須
pass dummy URL 過 strict check:
RUN DATABASE_URL=postgresql://build:***@localhost:5432/build bunx prisma generate
URL 唔需要真實,runtime container 有真實 env from docker-compose environment:。
Verification recipe(每次升 Prisma 7 / 改 Dockerfile 必跑):
docker run --rm pm-system-backend ls /app
# 預期:node_modules prisma prisma.config.ts src tsconfig.json
# 全部 5 個 file 喺度
如果 prisma.config.ts 唔喺 list,後面 prisma db push 100% 會 throw。
Reference:
env() 行為onClick await-in-non-async function(2026-06-09)WorkLogsPage.tsx:413 撞 TypeScript error 阻 Vite build:
error TS1308: 'await' expressions are only allowed within async functions
and at the top levels of modules.
Root cause pattern:
<button onClick={() => {
// ... 好多 code
const buf = await ws2.xlsx.writeBuffer() // ❌ await in non-async arrow
// ...
}}>
Fix:
<button onClick={async () => {
try {
// ...
const buf = await ws2.xlsx.writeBuffer()
// ...
} catch (err) {
console.error('Failed to export:', err)
alert('導出失敗,請重試')
}
}}>
Why ESLint 通常 miss:
no-misused-promises rule 通常會 catch,但 PM-System frontend/
eslint config 似乎冇 enabletsc -p tsconfig.app.json && vite build 步驟Detection recipe:
cd frontend
docker build -t pm-system-frontend:test . 2>&1 | grep -E "error TS|ERROR"
# 或者 host 直接跑
bunx tsc -p tsconfig.app.json --noEmit
Audit command(全 project 掃同類 bug):
cd frontend
python3 -c "
import re, glob
issues = []
for f in glob.glob('src/**/*.{ts,tsx}', recursive=True):
src = open(f).read()
# 找 onClick={...} 入面有 await,但 callback 唔係 async
for m in re.finditer(r'onClick=\{(\(?[^{]*?\)?\s*=>)\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}', src, re.DOTALL):
body = m.group(2)
if 'await' in body and 'async' not in m.group(1):
issues.append(f)
print(f'Found {len(issues)} await-in-sync-onClick issue(s):', issues)
"
如果 script 報 Found 0 issue(s) 即 clean。
hasPermission from src/middleware/permission.ts.requirements.view, requirements.create, requirements.edit, requirements.delete.ProjectMember.role checks while adding hasPermission(user, 'requirements.*') checks.if (!user) guards before accessing user.id; several legacy routes assume authenticated context and can otherwise throw.bunx tsc --noEmit currently reports broad pre-existing project errors (Elysia user context typing and unrelated Prisma types). For a targeted route syntax/bundle verification, use:
cd ~/projects/pm-system/backend
bun build src/routes/<route>.ts --outdir /tmp/pm-system-<route>-build --target=bun
The rolePermissionCache in src/middleware/permission.ts is the single source of truth for role permissions. It is loaded at startup via refreshAllRolePermissions() in index.ts and is NOT automatically invalidated after role mutations.
Symptom: After creating/updating/deleting a role in AdminPanel, admin users get 403 Forbidden on subsequent API calls that check user.role !== 'admin' or permission-based auth — even though they are legitimately admins.
Fix in src/routes/roles.ts: After every POST (create), PUT (update), and DELETE on roles, call refreshAllRolePermissions() to re-sync the cache:
// At top of roles.ts
import { rolePermissionCache } from '../middleware/permission'
// After role.create / role.update / role.delete in each handler:
// 1. Invalidate the cache entry
rolePermissionCache.delete(roleName)
// 2. Refresh all roles (avoids stale admin permissions)
const { refreshAllRolePermissions } = await import('../index')
await refreshAllRolePermissions()
Note: Uses dynamic await import('../index') to avoid circular dependency since roles.ts is imported by index.ts.
seedRolePermissions() in roles.ts is called on every role route request. It uses a module-level permissionsSeeded flag to avoid redundant DB writes after the first call:
let permissionsSeeded = false
async function seedRolePermissions(prisma: any) {
if (!permissionsSeeded) {
// seed permissions and roles...
permissionsSeeded = true
}
// always upsert roles (they may be edited by admin)
await Promise.all(DEFAULT_ROLES.map(...))
}
The production backend runs in a restart: always Docker container. When replacing dist/index.js:
Correct sequence (avoids corrupted file from container restart loop):
# 1. Stop container (prevents restart loop during write)
sudo docker stop pm-system-backend-1
# 2. Copy new file into container's filesystem
sudo docker cp ~/projects/pm-system/backend/dist/index.js pm-system-backend-1:/app/dist/index.js
# 3. Start container
sudo docker start pm-system-backend-1
# 4. Verify startup logs
sudo docker logs pm-system-backend-1 --tail 5
What NOT to do: docker exec <container> tee /app/dist/index.js < dist/index.js — the tee approach causes the container to restart during the write (because restart: always policy detects the process death), resulting in a partial/corrupted file.
Wiki pages (WikiPage model) are scoped to projects via projectId FK. They are not a standalone feature:
/wiki standalone route or sidebar link — the WikiPage component is reserved for future embedding inside ProjectDetailPageLayout.tsx navItems and remove its Route from App.tsxbackend/prisma/seed.ts 用 prisma.upsert 自動建立以下 user:
| Password | Role | |
|---|---|---|
admin@test.com | admin123 | admin |
pm@test.com | pm123 | pm |
techlead@test.com | tl123 | tech_lead |
dev@test.com | dev123 | developer |
tester@test.com | test123 | tester |
agent-dev1@test.com | agent123 | developer(isAgent=true) |
Auth endpoint:POST /auth/login(無 /api prefix — authRoutes 喺 index.ts 嘅 .group('/api') 之外)
Token format(demo 用):{userId}:{role}(簡單 JWT-less,生產要改真 JWT)
curl -s -X POST http://localhost:4001/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@test.com","password":"admin123"}' | jq .accessToken
# → "fa3889e7-3445-443d-8f0f-c6cee0813539:admin"
Status (2026-06-08): 19 routes,12 prisma models。Backend
authRoutes喺.group('/api')之外(/auth/*),其他全部喺/api/*之下。
/api prefix)POST /auth/login → { accessToken, refreshToken, user }POST /auth/logoutPOST /auth/refresh/api)GET /api/projects / POST /api/projects / GET /api/projects/:id / PUT /api/projects/:id / DELETE /api/projects/:idGET /api/requirements / POST /api/requirements / GET /api/requirements/:id / PUT /api/requirements/:id / DELETE /api/requirements/:idGET /api/tasks / POST /api/tasks / GET /api/tasks/:id / PUT /api/tasks/:id / DELETE /api/tasks/:idGET /api/bugs / POST /api/bugs / GET /api/bugs/:id / PUT /api/bugs/:id / DELETE /api/bugs/:idGET /api/worklogs(query:page, pageSize, limit=-1 for export, groupBy)POST /api/worklogs(驗證 hours 0.01-24)PUT /api/worklogs/:id / DELETE /api/worklogs/:id(cutoff:5 號前嘅 log,non-admin 唔可改)GET/POST/PUT/DELETE /api/usersGET/POST/PUT/DELETE /api/rolesGET /api/permissionsGET /api/agents / POST /api/agents / PUT /api/agents/:id / DELETE /api/agents/:idGET /api/agents/:id/stats / GET /api/agents/available-tasksPOST /api/agents/claim-task / POST /api/agents/release-taskGET /api/token-logs / POST /api/token-logs / GET /api/token-logs/stats/by-model / GET /api/token-logs/stats/by-agentGET/POST/PUT/DELETE /api/wikisGET/POST/PUT/DELETE /api/documentsGET/POST /api/attachmentsGET/POST /api/llm-configGET/POST /api/chat(綁定項目)GET /api/reports/*GET/POST/PUT/DELETE /api/departments| 服務 | Docker port | Local access |
|---|---|---|
| Frontend (nginx) | 8080:80 | http://localhost:8080 |
| Backend (Elysia) | 4001:4000 | http://localhost:4001 |
| Postgres | 5433:5432 | postgresql://pmuser:pmpassword@localhost:5433/pmdb |
E2E test 兩個 URL 都 test:Frontend via nginx(:8080),Backend direct(:4001)。
e2e/ 目錄有 Playwright config + 2 個 spec file:
# 1. 確保 docker up
cd ~/www/pm-system
docker compose up -d
# 2. 跑 E2E
cd e2e
npm install
npx playwright install chromium
npx playwright test
# → 13 tests pass(3 critical-path + 10 rbac-negative)
tests/critical-path.spec.ts)/tests/rbac-negative.spec.ts)⚠️ Rate-limit interaction pitfall (RG-008, 2026-06-09) TD-008 嘅 login rate-limit (5 attempts / 60s per IP) 對 rbac-negative spec 嘅 multi-user login 有 interaction。E2E spec 11 次 login attempts 全部 hit 同一個 container IP,撞 5/60s lock 第 6 個開始返 429,而非 200。
Workaround(短期):
# 跑 E2E 之前 reset rate-limit counter — restart backend
docker compose restart backend
# 等 ~12s 重新跑 seed + API up
sleep 12 && cd e2e && npx playwright test
Long-term fix(E2E 設計): spec 應該 stub rate-limit 喺 beforeAll,或者 用 multi-IP 模擬(同一 host 多個 source port)。詳見 TECH-DEBT 嘅 TD-008 進度(待補完整 E2E integration test)。
詳見 e2e/README.md。
4001:4000(frontend 8080,postgres 5433)— 唔係文件寫嘅 3000/3001/auth/* 無 /api prefix:authRoutes 喺 index.ts 嘅 .group('/api') 之外{userId}:{role}(demo 用,無真 JWT signing)return { user: null } 後,permission check 失敗 → 403)prisma.user.findUnique 撞不存在 UUID throw 500 — 見 TD-011,fix 0.1 日Number() 比較/(dashboard)— 唔係 /projectsprojects.create perm(我以為冇)— verify seed.ts 知backend 個 healthcheck 用 curl /api/projects,但 /api 需要 auth — 撞 401 仍算 healthy5 個改動一次過 ship(David 揀 A = full package)。Working tree 開工前已 uncommitted 全部 code + 1 retro doc(唔係由零寫,係 verify + commit + push),4 個 commit 落到 feat/wiki-sprint-21 再 merge --no-ff 入 master。
| US | 範圍 | 實作 |
|---|---|---|
| 21.1 (P0) | Wiki 上傳支援 .doc/.xls/.txt | documents.ts parser + SUPPORTED_EXTENSIONS + Dockerfile apk add |
| 21.2 (P1) | AI 分析文件時包含 fileName | 早已 include ${fileName} in user message(US-21.1 嗰陣 verify 過,冇 hunks 改) |
| 21.3 (P0) | 同項目內同名 Wiki 偵測 + 確認 modal | utils/wiki-dedup.ts (new) + wikis.ts replaceId 邏輯 + WikiPage.tsx UI |
| 21.4 (P0) | Wiki 查詢窗口 10 + result metadata | chat.ts MAX_WIKI_PAGES 5→10 + WikiSearchResponse {requested, matched, returned, totalAvailable, hasMore} |
| 21.5 (P1) | 跨項目 AI 查詢 | chat.ts executeTool search_wiki: 冇 effectiveProjectId → admin 查全項目,non-admin 查 member projects,merge sort |
| Commit | File(s) | US 覆蓋 |
|---|---|---|
e39dcb5 feat(docs) | Dockerfile + documents.ts + documents.test.ts + WikiPage.tsx | US-21.1 (+ 21.2 skip + 21.3 leak 因為 hunks 纏埋) |
de53111 feat(wiki) | wiki-dedup.ts (new) + wiki-dedup.test.ts (new) + wikis.ts | US-21.3 |
00a1552 feat(chat) | chat.ts + ChatPage.tsx | US-21.4 + US-21.5(hunks 纏埋) |
0aaea13 docs(retro) | docs/retros/2026-06-16-sprint-21-wiki-improvements.md (new) | Retro doc |
Hunk 纏繞嘅 honest 處理:documents.ts hunks L580+ 包含 US-21.3 嘅 findExistingWikiPage import + 409 response,跟 US-21.1 parser hunks 喺同一 file,拆唔到。Commit message 明寫「Note: this commit also includes some pre-existing US-21.3 hunks ...」+ 後續 US-21.3 commit 解釋。唔好 pretend clean atomic 唔 leak。
MAX_WIKI_PAGES + WikiSearchResponse 永久 invariant(2026-06-16 起)// backend/src/routes/chat.ts
const MAX_WIKI_PAGES = 10 // 2026-06-16 bumped 5 → 10
const MAX_WIKI_TOOL_RESULTS = 10 // unchanged
const DEFAULT_WIKI_TOOL_RESULTS = 5 // unchanged
type WikiSearchResponse = {
results: WikiSearchResult[]
requested: number
matched: number
returned: number
totalAvailable: number
hasMore: boolean
}
如果日後要再加 wiki 內容窗口,MAX_WIKI_PAGES 同 MAX_WIKI_TOOL_RESULTS 都要同步改 — 前者係 LLM 收到嘅 system prompt 內容,後者係 AI tool call 嘅用戶面 limit。
findExistingWikiPage helper(2026-06-16 新)backend/src/utils/wiki-dedup.ts:
export function normalizeTitleForCompare(title: string): string {
return title
.toLowerCase()
.trim()
.replace(/\s+/g, ' ')
.replace(/[。.,,、:;!?]+$/g, '')
}
export async function findExistingWikiPage(projectId: string, title: string) {
// Prisma can't express normalize in WHERE → pre-filter case-insensitive
// contains, then post-filter for whitespace/punctuation normalization
// ...
}
不變量:同 project 內,標題 normalize 後相同 = 重覆,返回 409 DUPLICATE_PAGE + existingPage metadata。前端確認後 call POST /api/wikis 加 replaceId: <id> 做 in-place update(唔 overwrite silently)。
apk add package 鎖定(2026-06-16)RUN apk add --no-cache poppler-utils antiword wv gnumeric
| Package | Binary | 用途 |
|---|---|---|
poppler-utils | pdftoppm pdfinfo | PDF 轉 PNG (vision LLM 入口) |
antiword | antiword | .doc (Word 97-2003) parser |
wv | wvText | .doc fallback(antiword 失敗時) |
gnumeric | ssconvert | .xls (Excel 97-2003 BIFF8) parser |
不要改呢 4 個 package 名,因為佢哋已經喺 image 入面 verify 過 binary 真係 exist。改咗而又唔重新跑 docker run --rm pm-system-backend sh -c "which antiword wvText ssconvert" 嘅話,下次 docker build 隨時爆。詳見 docker-mac-arm64-elysia-vite 嘅 Pitfall 12(verify Alpine package 存在先寫 Dockerfile)。
M backend/Dockerfile (apk add line)
M backend/src/routes/chat.ts (MAX_WIKI_PAGES, WikiSearchResponse, cross-project)
M backend/src/routes/documents.test.ts (SUPPORTED_EXTENSIONS)
M backend/src/routes/documents.ts (.doc/.xls/.txt parsers + findExistingWikiPage hook)
M backend/src/routes/wikis.ts (replaceId + 409 DUPLICATE_PAGE)
A backend/src/utils/wiki-dedup.ts (NEW)
A backend/src/utils/wiki-dedup.test.ts (NEW, 12 cases)
M frontend/src/pages/ChatPage.tsx (🌐 全部項目 option)
M frontend/src/pages/WikiPage.tsx (重覆 modal + 更新此頁 button)
A docs/retros/2026-06-16-sprint-21-wiki-improvements.md (NEW)
Dockerfile 一開始寫 catdoc + xls2csv,Alpine v3.22 冇呢兩個 package。Docker build 第一次失敗(layer 2 RUN apk add exit 2)。Hotfix spec:
catdoc → wv (provides wvText)xls2csv → 直接用 ssconvert(gnumeric 入面),xls2csv + ssconvert fallback 兩層變一層Hotfix US-21.1.1 section 留 audit trail呢 3 個 file 改動仲未 commit(截至 2026-06-16 16:00) — 屬 image re-build hotfix,code change 應該後續追蹤成 1 個 fix(docs): US-21.1.1 replace unavailable Alpine packages commit。
cd backend && bun test
# → 710 pass, 0 fail, 1245 expect() calls, 31 files, [2.08s]
David 對客戶交付嘅需求:唔畀 source code、x86 + arm 都要行、會有更新。 2026-06-09 制定嘅方案 = 多 platform image + tarball handoff + 客戶一鍵 install。 本機 dev(上面)同客戶交付(呢度)係兩條完全唔同嘅 path,唔可以混淆。
| 約束 | 落地方法 |
|---|---|
| 唔畀 source code | 客戶機冇 Dockerfile,只有 image tar;source folder 唔出 package |
| x86 + arm 都要行 | docker buildx 建 manifest list,linux/amd64 + linux/arm64 同一個 tag |
| 可更新 | 客戶機 docker load 新 tar → docker compose up -d → 自動跑 prisma migrate deploy |
scripts/build-release.sh ← 我/CI 跑,build + save tar
deploy/docker-compose.client.yml ← 客戶用嘅 compose(無 seed)
deploy/.env.client.example ← 客戶 copy 做 .env 嘅 template
deploy/install.sh ← 客戶跑嘅 5-step 安裝
deploy/README.md ← 客戶 30 秒版手冊
deploy/CHANGELOG.md ← 每個 release 記低咩改咗
詳細內容睇 templates/client-release/(可 copy 落新 project 改用)。
pm-system-frontend:v1.0.0 ← 1 tag(nginx platform-neutral,理論上 1 份 binary 行晒兩個 arch)
pm-system-backend:v1.0.0 ← 1 tag + 2 個 arch,做成 manifest list:
├─ linux/amd64(oven/bun:1-alpine + bun 嘅 native binary)
└─ linux/arm64
Frontend 唔使 multi-arch 都得(nginx static 已經係 universal binary), 但 2026-06-09 嘅 build script 仍然 build 兩份 load 一次過,保持 image tag 結構一致。
pm-system-release-v1.0.0/
├── README.md ← 一張紙 3 步裝完
├── docker-compose.client.yml ← 客戶用
├── .env.client.example ← 客戶 copy 做 .env
├── pm-system-frontend-v1.0.0.tar
├── pm-system-backend-v1.0.0-multiarch.tar
├── CHECKSUMS.sha256 ← 客戶 install.sh 自動 verify
├── RELEASE-NOTES.md ← 客戶睇 update
└── install.sh ← 一鍵裝
# 1. 解壓
tar xzf pm-system-release-v1.0.0.tar.gz && cd pm-system-release-v1.0.0
# 2. 改 .env
cp .env.client.example .env && $EDITOR .env
# 3. 跑
chmod +x install.sh && ./install.sh
# 4. 訪問
open http://localhost/
install.sh 自動做晒:pre-flight 檢查 docker、.env field 驗證、CHECKSUMS 驗證、
docker load 兩個 image、docker compose up -d、等 backend health(最長 90s)、
印 access URL。
| 項目 | dev compose | 客戶 compose |
|---|---|---|
db 密碼 | pmpassword hard-coded | ${DB_PASSWORD} 必填,冇 default |
JWT_SECRET | dev-secret-key | ${JWT_SECRET} 必填,冇 default |
prisma db push | ✅ 跑 | ❌ 改用 prisma migrate deploy |
prisma db seed | ✅ 跑 | ❌ 唔跑(seed 屬 source,不外流) |
| backend port | 4001:4000 公開 | 只 internal expose: 4000,frontend 經 nginx proxy |
| db port | 5433:5432 公開 | 只 internal expose: 5432 |
restart: unless-stopped | 冇 | ✅ 3 個 service 都有 |
| log driver | default | json-file 10MB × 3 rotation |
| volume name | postgres_data(auto) | pm-system-postgres-data(named,update 唔會 data loss) |
| image source | build: 來源碼 | image: pm-system-*:${VERSION}(load 完用 tag) |
docker load -i 一個 file,自動揀 arch。簡單過畀 2 個 tar。docker/build-push-action@v5 行 multi-arch runner)prisma/seed.* 落 .dockerignore 排走migrate deploy 唔會跑 seed(image 都冇 seed file)# 客戶機收到新 tarball 之後
docker load -i pm-system-frontend-v1.1.0.tar
docker load -i pm-system-backend-v1.1.0-multiarch.tar
# 改 .env
sed -i '' 's/^VERSION=v.*/VERSION=v1.1.0/' .env
# 重啟(migration 自動跑,data volume 保留)
docker compose -p pm-system up -d
重要:data volume 係 named(pm-system-postgres-data),docker compose down
唔會刪;docker compose down -v 先會刪。客戶 update 唔會 data loss。
bun build(2026-06-05 crm-system 教訓)bun build --minify 撞 Elysia 1.2 嘅 runtime code gen(compile?.())會
ReferenceError: vn is not defined。客戶 backend 嘅 Dockerfile 直接 COPY
source 入 runtime image + bun run src/index.ts 啟動,唔用 bun build。
Image 大 ~200MB,cold start 慢少少,但係最穩 path。
詳見 devops/bun-elysia-react-vite-stack。
prisma.config.ts 三層 pitfall(沿用上面)客戶 backend Dockerfile 一定要 COPY prisma.config.ts 同 builder stage 嘅
prisma generate 嗰個 dummy DATABASE_URL 否則 image 入面 prisma config
validator throw datasource.url is required,container exit(1),backend 完全起唔到。
Frontend healthy 會誤以為 OK,一定要 curl backend health。
bash scripts/build-release.sh vX.Y.Z 跑完冇錯shasum -a 256 deploy/dist/*.tar 同 CHECKSUMS.sha256 對得齊docker load -i *.tar → docker compose -f docker-compose.client.yml up -dcurl http://localhost/api/projects = 200(backend healthy)curl http://localhost/ = HTML(frontend nginx 起咗)prisma db seed 唔喺 client compose command)restart: unless-stopped 設咗(全部 service 都有).env.client.example 嘅 secret 全部係 PLACEHOLDER 字眼=~ PLACEHOLDER)| 客戶需求 | 用呢個 path |
|---|---|
| 唔畀 source、要 image、要 offline | 本 section ✅ |
| 客戶肯用 Docker Hub / GHCR | 可以改用 docker push + image: registry/...,可省 tarball step |
| 客戶要 HA / multi-node | 改用 Kubernetes / ECS,呢個 path 唔啱,睇 cdk-ecs-fargate-deploy |
| 客戶要 source 自行 build | 違反「唔畀 source」,建議搬去 SaaS offering |
| 客戶要 HTTPS | 加 Caddy reverse proxy,睇 caddy-spa-api-proxy-deploy |
docs/PROJECT-OVERVIEW.md — 完整 scope / stackdocs/PRD.md — 50+ USdocs/QA-TRACKER.md — US 過 test 狀態docs/TECH-DEBT.md — TD-001 到 TD-011e2e/README.md — 點跑 E2Ebackend/CLAUDE.md — Bun-specific 規則templates/client-release/ — 客戶交付嘅 5 個 starter file(reproduce with modifications)references/multi-arch-build-decision-matrix.md — 點解用 A 而唔用 B/C/D