尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Lịch cá nhân dương lịch và âm lịch Việt Nam, chạy trên web và cài được như app (PWA).
翻譯準備中
正在載入完整 README。
檢視完整 README ↗Lịch cá nhân dương lịch và âm lịch Việt Nam, chạy trên web và cài được như app (PWA).
/events): mọi sự kiện theo năm và tháng, cuộn vô hạn hai chiều, tìm kiếm, lọc theo danh mục, chọn nhiều để xoá.Tài liệu yêu cầu: tasks/prd-personal-calendar.md. Danh sách task: tasks/tasks-personal-calendar.md. Lịch vạn niên: tasks/prd-day-almanac.md, tasks/tasks-day-almanac.md. Cỡ chữ: tasks/prd-text-size.md, tasks/tasks-text-size.md. Giải nghĩa Bát Tự: tasks/prd-bazi-interpretation.md, tasks/tasks-bazi-interpretation.md. Kế hoạch SEO: tasks/tasks-seo-audit.md.
| Phần | Dùng |
|---|---|
| Frontend | React 19, TypeScript, Vite, vite-plugin-pwa (service worker tự viết ở src/sw.ts) |
| Dữ liệu trên máy | IndexedDB qua Dexie |
| Cloud | Firebase Auth (Google), Cloud Firestore, Cloud Messaging (Web Push), Hosting |
| Nhắc hẹn phía server | Cloud Functions v2 trong functions/, email qua Resend |
| Test | Vitest, Testing Library, Firebase Emulator, Playwright, axe-core |
Logic lịch âm, lặp sự kiện và tính giờ nhắc nằm trong shared/. Thư mục này không phụ thuộc React hay Firebase, nên cả app lẫn Cloud Functions dùng chung.
Hồ sơ sinh được lưu cùng cài đặt trên thiết bị hoặc tài khoản, có thể xuất/nhập bằng JSON và được
xóa liên đới khi xóa ngày sinh. Phép tính phong thủy và Tứ Trụ chạy trong ứng dụng, dùng được offline;
ngày, giờ, nơi sinh và kết quả không được đưa vào analytics, URL, SEO/prerender hoặc dịch vụ luận
giải bên ngoài. Xem phạm vi, nguồn và bằng chứng phát hành tại
tasks/feng-shui-release-checklist.md.
Cần có:
npx npm@11 install nếu máy đang có npm cũ.brew install openjdk.npm install -g firebase-tools.npx npm@11 install
cd functions && npx npm@11 install && cd ..
cp .env.example .env.local
App chạy được ngay khi chưa điền .env.local. Khi đó chỉ có chế độ lưu trên máy, không có đăng nhập.
.env.local ở thư mục gốcLấy giá trị ở Firebase Console > Project settings > Your apps (Web app).
| Biến | Ý nghĩa |
|---|---|
VITE_FIREBASE_API_KEY, VITE_FIREBASE_AUTH_DOMAIN, VITE_FIREBASE_PROJECT_ID, VITE_FIREBASE_STORAGE_BUCKET, VITE_FIREBASE_MESSAGING_SENDER_ID, VITE_FIREBASE_APP_ID | Cấu hình Firebase Web app |
VITE_FIREBASE_VAPID_KEY | Khóa Web Push. Tạo ở Project settings > Cloud Messaging > Web Push certificates > Generate key pair |
VITE_USE_FIREBASE_EMULATOR | true để bản dev kết nối Firebase Emulator thay vì project thật |
Các giá trị VITE_* được đóng vào bundle và ai cũng xem được. Đây là cấu hình công khai của Firebase Web app, không phải bí mật. Dữ liệu được bảo vệ bằng firestore.rules.
functions/.envCopy từ functions/.env.example.
| Biến | Ý nghĩa |
|---|---|
EMAIL_FROM | Địa chỉ gửi email, ví dụ Lịch Việt <[email protected]>. Tên miền phải được xác minh trong Resend |
APP_URL | Địa chỉ app, dùng cho link trong email. Mặc định https://lich.thanhnghiep.top |
API key của Resend là bí mật, không để trong file hay trong code. Lưu vào Secret Manager:
firebase functions:secrets:set RESEND_API_KEY
Chưa đặt key thì nhắc hẹn email bị bỏ qua, nhắc hẹn thông báo vẫn chạy.
npm run dev # http://localhost:5173
Dùng Firebase Emulator để không đụng dữ liệu thật:
npm run emulators # Auth :9099, Firestore :8080, giao diện :4000
Rồi đặt VITE_USE_FIREBASE_EMULATOR=true trong .env.local, hoặc chạy VITE_USE_FIREBASE_EMULATOR=true npm run dev.
Service worker (offline, thông báo khi đóng tab) chỉ có ở bản build:
npm run build && npm run preview # http://localhost:4173
| Lệnh | Làm gì |
|---|---|
npm run typecheck | Kiểm tra kiểu cho app, service worker, test emulator và functions |
npm run lint | oxlint |
npm run format | Prettier |
npm test | Unit test (Vitest, jsdom, múi giờ Asia/Ho_Chi_Minh), bỏ p4Puzhu17Audit.test.ts (~455 giây); không đủ cho merge/release, xem ci:full |
npm run test:emulator | Test Security Rules, Firestore repository và hàng đợi nhắc hẹn trên Emulator. Dùng project demo-calendar-hana, không bao giờ chạm project thật. Cần Java |
npm run test:e2e | Playwright cho các user story, giao diện điện thoại và quét accessibility (axe), trên Chrome desktop và Pixel 7. Tự build và chạy preview |
npx playwright test --project=firefox --project=webkit --project=iphone | Chạy e2e thêm trên Firefox, Safari và iPhone. Cài trình duyệt trước bằng npx playwright install firefox webkit |
e2e/visual-default.spec.ts so ảnh các màn chính ở cỡ chữ Mặc định (Chromium, chỉ chạy trên macOS vì ảnh chuẩn chụp trên macOS; CI Linux bỏ qua) để chắc phóng to chữ không làm lệch giao diện gốc. Chỉ cập nhật ảnh chuẩn khi cố ý đổi giao diện: npx playwright test visual-default --project=desktop --project=mobile --update-snapshots.
CSS mới: viết cỡ chữ, khoảng cách, kích thước dạng calc(Npx * var(--ui-scale)) và icon dạng size={iconSize(N)} (src/styles/textSize.ts). Không nhân tỉ lệ cho viền, bo góc, bóng, blur và lề trang.
Playwright chỉ hỗ trợ service worker trên Chromium. Vì vậy test offline (US-8) bị bỏ qua trên WebKit, cần kiểm tra Safari bằng tay.
Cần đăng nhập Firebase CLI (firebase login) và có quyền trên project calendar-hana (xem .firebaserc).
# Security Rules và index
firebase deploy --only firestore
# Web app
npm run build && firebase deploy --only hosting
# Cloud Functions (cần gói Blaze). Lệnh này tự build functions trước khi deploy.
firebase deploy --only functions
Trước lần deploy functions đầu tiên:
asia-southeast1 (trong functions/src/index.ts) và phải cùng vùng với Firestore.RESEND_API_KEY và tạo functions/.env như mục trên.Kiểm tra chạy ở máy, 3 mức: npm run check (lint, typecheck; hook .githooks/pre-push chạy lệnh này trước mỗi git push, bật bởi npm install qua script prepare, lỗi thì chặn push, bỏ qua bằng git push --no-verify); npm run test:changed (Vitest chỉ chạy test liên quan file đổi so với origin/main); npm run ci:full (check + toàn bộ unit qua vitest run, kể cả p4Puzhu17Audit.test.ts, rồi check:audit-clean, Emulator, mutation gate, e2e Chrome desktop; ci:local là alias) khi đổi logic lịch, thiên văn, dữ liệu, test infra hay trước release lớn. npm test (~5 phút) bỏ puzhu17 audit nên chỉ dùng khi code, không thay ci:full. npm run ci:p4:full (ci:p4: mutation P4/P5 và full mode, lâu; cộng test:p4:heading-lookup-full, quét 2025–2027) chạy tay khi đổi shared/lunar/p4*/scripts/audit-p4-*. npm run ci:p4:fast (check + test P4 chế độ thường, không mutation/full mode/puzhu17 audit, ~3 phút) chỉ dùng khi đang code P4, không phải cổng merge/release; trước merge vẫn chạy các cổng trên, đổi authority/Golden/migration thì nó không đủ. npm run test:p4:puzhu17-freeze (~8 phút; docs/audits/sources/ phải sạch trước khi chạy) dựng lại freeze puzhu17, kiểm lock rồi check:audit-clean: kiểm riêng chuỗi authority. GitHub Actions chỉ lint, build, deploy.
Workflow .github/workflows/deploy.yml không chạy test: push vào main là build rồi firebase deploy --only firestore,hosting,functions.
Cấu hình trên GitHub (Settings > Secrets and variables > Actions):
| Loại | Tên | Nội dung |
|---|---|---|
| Variables | VITE_FIREBASE_* (7 biến) | Giống .env.local, là cấu hình công khai |
| Variables | EMAIL_FROM, APP_URL | Giống functions/.env |
| Secret | FIREBASE_SERVICE_ACCOUNT | Nội dung file JSON key của service account dùng để deploy |
Chưa có FIREBASE_SERVICE_ACCOUNT thì bước deploy được bỏ qua. RESEND_API_KEY vẫn nằm trong Secret Manager, không cần đưa lên GitHub.
Tạo service account ở Google Cloud Console > IAM & Admin > Service Accounts, cấp các role:
Rồi tạo key JSON và dán nội dung vào secret FIREBASE_SERVICE_ACCOUNT.
App chạy ở https://lich.thanhnghiep.top, đặt trong .env.production (VITE_CANONICAL_HOST). Bản trên calendar-hana.web.app và calendar-hana.firebaseapp.com tự chuyển sang tên miền này. Máy đang có dữ liệu chỉ lưu trên máy thì không bị chuyển: app hiện hướng dẫn xuất dữ liệu rồi nhập ở địa chỉ mới, vì dữ liệu trên máy gắn với từng tên miền.
Khi đổi tên miền, cập nhật thêm:
VITE_FIREBASE_AUTH_DOMAIN trong .env.local đặt bằng tên miền, và thêm https://<tên miền>/__/auth/handler vào Authorized redirect URIs của OAuth client (Google Cloud Console > APIs & Services > Credentials).APP_URL trong functions/.env.SITE_URL trong shared/seo/daySeo.ts, các URL trong index.html và public/robots.txt.npm run build tạo sẵn dist/day/YYYY-MM-DD.html cho mọi ngày từ đầu năm trước tới cuối năm sau (scripts/prerenderDays.ts), mỗi trang có title, description, canonical, Open Graph, JSON-LD và nội dung lịch vạn niên dạng HTML tĩnh, và ghi dist/sitemap.xml liệt kê các trang này. Firebase Hosting (cleanUrls) phục vụ /day/2026-09-25 từ file tương ứng; ngày ngoài khoảng vẫn mở được qua rewrite, app tự cập nhật <head> (src/hooks/usePageHead.ts). Xem Tuần và Tháng có noindex, follow. Service worker không precache các trang này. Khoảng ngày trượt theo năm build, nên cần deploy lại ít nhất mỗi năm.
Kết quả audit SEO 02/10/2026 và kế hoạch sửa (hiệu năng mobile, liên kết nội bộ, trang tháng, soft 404) nằm ở tasks/tasks-seo-audit.md.
Hai trang HTML công khai (không cần JavaScript hay đăng nhập):
https://lich.thanhnghiep.top/privacyhttps://lich.thanhnghiep.top/termsNội dung ở public/privacy.html và public/terms.html, CSS chung ở public/legal.css.
Tên app là Lịch Việt, email liên hệ [email protected]. Trang chủ và footer
cuối trang Cài đặt (ngoài các tab) có liên kết; sitemap có hai URL; service worker cache các trang cho offline.
Firebase Hosting với cleanUrls: true phục vụ chúng từ file HTML tương ứng.
Trong app, các liên kết này dùng React Router và hiển thị nội dung từ cùng file HTML,
giữ nguyên phiên đăng nhập và kết nối Firestore khi quay lại lịch trên PWA iOS.
Vite dev dùng scripts/publicLegalPages.ts để ánh xạ /privacy và /terms sang HTML công khai,
giữ nguyên URL trên trình duyệt. Kiểm tra riêng dev bằng
npx playwright test --config playwright.dev.config.ts --project=desktop --project=mobile.
Sau khi deploy, điền các URL trên vào Google Auth Platform > Branding (cùng trang chủ
https://lich.thanhnghiep.top/), xác minh tên miền thanhnghiep.top, khai báo scope
user.birthday.read trong Data Access và thực hiện quy trình xác minh Google yêu cầu.
Việc có hai trang không tự bật People API hoặc tự hoàn tất xét duyệt OAuth.
Code và .env* không chứa Client ID: Firebase Auth lấy từ cấu hình trong console. Khi đổi client:
https://lich.thanhnghiep.top, https://calendar-hana.firebaseapp.com,
https://calendar-hana.web.app (thêm http://localhost nếu thử local).https://lich.thanhnghiep.top/__/auth/handler và
https://calendar-hana.firebaseapp.com/__/auth/handler, khớp với VITE_FIREBASE_AUTH_DOMAIN.user.birthday.read) và
xác minh OAuth. Khác project thì phải làm lại các bước ở trên và bật lại People API.redirect_uri_mismatch
nghĩa là thiếu URI ở bước 1.Cài đặt > Tài khoản có ngày, tháng, năm sinh theo dương lịch và giới tính (tùy chọn, dùng cho Sao chiếu mệnh theo Nam/Nữ mạng) trên cùng một hàng. Nhóm Giờ và nơi sinh (để lập Tứ Trụ) có giờ sinh 24 giờ và nơi sinh; độ chắc chắn và sai số chỉ hiện khi đã nhập giờ; quy ước giờ Tý nằm trong Tùy chọn nâng cao và tự mở khi hồ sơ dùng quy ước khác mặc định. Nhập tay và bấm Lưu thông tin để lưu trên máy hoặc đồng bộ vào tài khoản đang đăng nhập. Xóa thông tin ngày sinh làm trống form; bấm lưu để xác nhận xóa (giới tính và các trường giờ, nơi sinh bị xóa cùng). Thông tin này đi cùng bản sao lưu JSON, không gửi vào analytics. Khi chuyển dữ liệu local lên cloud, ngày sinh local chỉ được điền nếu tài khoản chưa từng lưu ngày sinh.
Sau khi đăng nhập, Lấy ngày sinh từ Google xin thêm quyền
https://www.googleapis.com/auth/user.birthday.read, đọc Google People API và điền vào form.
Người dùng kiểm tra rồi bấm lưu; năm sinh Google không cung cấp thì cần nhập bổ sung.
Giờ sinh luôn nhập tay. OAuth access token chỉ dùng cho yêu cầu API, không được lưu.
Để bật lấy từ Google trên production, bật People API trong Google Cloud Console của project Firebase và cấu hình scope ngày sinh trong Google Auth Platform > Data Access. Thực hiện xác minh OAuth theo yêu cầu Google trước khi mở quyền này cho mọi người. Nếu API chưa bật, người dùng từ chối quyền hoặc trình duyệt/PWA chặn popup, app báo lỗi và vẫn cho phép nhập thủ công. Luồng này dùng popup để xin quyền cho đúng tài khoản hiện tại.
Xem Google People API và Firebase Google Auth.
src/reminders/localScheduler.ts).users/{uid}/pushTokens. Khi sự kiện thay đổi, function scheduleReminders ghi lần nhắc kế tiếp vào reminderQueue. Mỗi phút sendDueReminders gửi các nhắc đến hạn qua FCM hoặc email, rồi tính lần kế tiếp. Mỗi nhắc chỉ gửi tối đa 1 lần. Nhắc trễ quá 1 giờ thì bỏ qua.shared/ Logic thuần: lịch âm, Can Chi, ngày lễ, lịch vạn niên (28 tú, trực, sao Ngọc Hạp...), lặp sự kiện, giờ nhắc, format tiếng Việt
src/
auth/ Đăng nhập Google (Firebase Auth tải khi cần)
data/ Repository local (Dexie) và cloud (Firestore), chuyển dữ liệu, xuất/nhập JSON, ICS, CSV (Google Calendar)
reminders/ Quyền thông báo, lịch nhắc local, Web Push, đăng ký service worker
views/ Xem Ngày (kèm khối Lịch vạn niên), Tuần, Tháng
components/ Form sự kiện, chọn ngày, dòng sự kiện, hộp thoại...
pages/ Trang lịch, Danh sách sự kiện và Cài đặt
styles/ Theme (kính mờ kiểu iOS, sáng/tối)
sw.ts Service worker: cache offline, Web Push, bấm thông báo
functions/ Cloud Functions (bundle bằng esbuild, gói luôn shared/)
e2e/ Test Playwright
docs/ Tài liệu audit, quyết định kỹ thuật, thiết kế (xem docs/README.md)
tasks/ PRD, danh sách task, bằng chứng audit gốc
golden-v3/, golden-v4/ Dữ liệu chuẩn đã đóng băng cho test lịch
firestore.rules
MIT. Thành phần bên thứ ba: xem THIRD_PARTY_NOTICES.md.