All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m52s
User request: anh trỏ 3 subdomain mới về VPS IP 103.124.94.38:
- api.huypham.vn → api.solutions.com.vn
- admin.huypham.vn → admin.solutions.com.vn
- user.huypham.vn → eoffice.solutions.com.vn
Verified DNS: cả 3 resolve 103.124.94.38 ✓
Update 17 file repo:
FE (4): fe-admin/.env.production + fe-user/.env.production
(VITE_API_BASE_URL → https://api.solutions.com.vn)
fe-admin/src/lib/{api,realtime}.ts + fe-user equivalents (comment)
BE (1): appsettings.Production.json.example — CORS AllowedOrigins
CI/CD (1): .gitea/workflows/deploy.yml — smoke test URL
Scripts (3): setup-iis-sites (DomainApi/Admin/User), setup-ssl (3 host),
deploy-all (verify curls)
Docs (5): STATUS, HANDOFF, PROJECT-MAP, vps-setup, gotchas
Skill (1): iis-deploy-runbook — 3 site table + description
Email admin@huypham.vn giữ nguyên (Let's Encrypt contact — không phải
domain serve).
Thêm scripts/migrate-domains.ps1 — 1-shot VPS migration:
1. Pre-flight: resolve DNS 3 domain → verify IP VPS khớp
2. Add HTTP binding mới cho 3 IIS site (giữ binding cũ làm fallback)
3. Run win-acme xin 3 cert Let's Encrypt qua HTTP-01 challenge
(auto add HTTPS binding + http→https redirect)
4. Verify /health/live + /health/ready + 2 FE endpoint
5. (Optional -RemoveOld) xóa binding huypham.vn sau verify OK
Rollback: nếu fail, binding cũ vẫn active → site serve qua huypham.vn.
Anh chạy trên VPS:
cd C:\solution-erp\scripts ; .\migrate-domains.ps1
# Sau 1-2 ngày verify stable:
.\migrate-domains.ps1 -RemoveOld -SkipCert
49 lines
1.6 KiB
TypeScript
49 lines
1.6 KiB
TypeScript
import { HubConnection, HubConnectionBuilder, HubConnectionState, LogLevel } from '@microsoft/signalr'
|
|
import { TOKEN_KEY } from '@/lib/api'
|
|
|
|
// Hub URL resolution:
|
|
// - Dev: Vite proxy forwards /api → :5443 but SignalR bypasses axios, so we
|
|
// hit the API origin directly from the browser.
|
|
// - Prod: VITE_API_BASE_URL (https://api.solutions.com.vn)
|
|
const HUB_URL = (import.meta.env.VITE_API_BASE_URL ?? window.location.origin) + '/hubs/notifications'
|
|
|
|
let connection: HubConnection | null = null
|
|
let startPromise: Promise<void> | null = null
|
|
|
|
/** Lazily starts (or reuses) a single hub connection. Token read on connect. */
|
|
export async function ensureConnection(): Promise<HubConnection> {
|
|
if (connection && connection.state === HubConnectionState.Connected) return connection
|
|
|
|
if (!connection) {
|
|
connection = new HubConnectionBuilder()
|
|
.withUrl(HUB_URL, {
|
|
accessTokenFactory: () => localStorage.getItem(TOKEN_KEY) ?? '',
|
|
})
|
|
.withAutomaticReconnect([0, 2_000, 5_000, 10_000, 15_000]) // exponential-ish backoff
|
|
.configureLogging(LogLevel.Warning)
|
|
.build()
|
|
}
|
|
|
|
if (connection.state === HubConnectionState.Disconnected) {
|
|
startPromise ??= connection.start().catch(err => {
|
|
startPromise = null
|
|
throw err
|
|
})
|
|
await startPromise
|
|
startPromise = null
|
|
}
|
|
return connection
|
|
}
|
|
|
|
/** Stops + forgets the connection. Call on logout. */
|
|
export async function stopConnection(): Promise<void> {
|
|
if (!connection) return
|
|
try {
|
|
await connection.stop()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
connection = null
|
|
startPromise = null
|
|
}
|