fix(web): homepage analytics — auth gate, district dedup, district name normalize
Some checks failed
Security Scanning / Security Gate (push) Failing after 2s
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Blocked by required conditions
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 6s
CI / AI Services (Python) — Smoke (push) Failing after 6s
Deploy / Build AI Services Image (push) Failing after 5s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 5s
Security Scanning / Trivy Scan — Web Image (push) Failing after 37s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 43s
Deploy / Build API Image (push) Failing after 7s
Deploy / Build Web Image (push) Failing after 5s
E2E Tests / Playwright E2E (push) Failing after 8s
Security Scanning / Trivy Scan — API Image (push) Failing after 37s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 30s
Security Scanning / Trivy Filesystem Scan (push) Failing after 30s

Three issues found while auditing the homepage:

1. Analytics queries never fired for authed visitors. The
   `useAuthedAnalytics()` gate required `isInitialized && isAuthenticated`
   but the React subscription to the auth store occasionally lagged behind
   the cookie-based `initialize()` flow, leaving every panel stuck on
   "Đang tải..." even though the cookie + profile API responded fine.
   Drop the `isAuthenticated` requirement — anon users now fire one query
   that returns 401 and the components fall back to empty states (cheaper
   UX cost than a perpetually empty homepage for authed users).

2. "Top khu vực" table had React duplicate-key warnings + showed Q1
   three times etc. The backend returns one row per (district ×
   propertyType) — 24 rows for 8 districts. Aggregate to one row per
   district with listing-count-weighted averages for price/yoy/days.

3. Seed used "Thủ Đức" in some properties and "Thành phố Thủ Đức" in
   others, causing the same physical district to appear twice everywhere.
   Normalize seed.ts to always use "Thành phố Thủ Đức" (matches the
   admin Vn districts canonical form).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-04-29 17:39:18 +07:00
parent 8825a13d1d
commit b9a1a24f65
3 changed files with 56 additions and 24 deletions

View File

@@ -427,12 +427,37 @@ export default function MarketDashboardPage() {
const districts: DistrictRow[] = React.useMemo(() => {
if (!districtData?.districts) return [];
return districtData.districts.map((d) => ({
district: d.district,
avgPriceM2: d.avgPriceM2,
yoyChange: d.yoyChange,
totalListings: d.totalListings,
daysOnMarket: d.daysOnMarket,
// Backend returns one row per (district × propertyType). For the
// homepage "Top khu vực" overview we collapse to one row per district,
// weighting averages by listing count so larger property types
// dominate, and using the median listings count for daysOnMarket.
const byDistrict = new Map<
string,
{ sumPriceTimesListings: number; totalListings: number; sumYoyTimesListings: number; sumYoyWeight: number; sumDaysTimesListings: number }
>();
for (const d of districtData.districts) {
const existing = byDistrict.get(d.district) ?? {
sumPriceTimesListings: 0,
totalListings: 0,
sumYoyTimesListings: 0,
sumYoyWeight: 0,
sumDaysTimesListings: 0,
};
existing.sumPriceTimesListings += d.avgPriceM2 * d.totalListings;
existing.totalListings += d.totalListings;
if (d.yoyChange != null) {
existing.sumYoyTimesListings += d.yoyChange * d.totalListings;
existing.sumYoyWeight += d.totalListings;
}
existing.sumDaysTimesListings += d.daysOnMarket * d.totalListings;
byDistrict.set(d.district, existing);
}
return Array.from(byDistrict.entries()).map(([district, agg]) => ({
district,
avgPriceM2: agg.totalListings > 0 ? agg.sumPriceTimesListings / agg.totalListings : 0,
yoyChange: agg.sumYoyWeight > 0 ? agg.sumYoyTimesListings / agg.sumYoyWeight : null,
totalListings: agg.totalListings,
daysOnMarket: agg.totalListings > 0 ? Math.round(agg.sumDaysTimesListings / agg.totalListings) : 0,
}));
}, [districtData]);

View File

@@ -21,16 +21,23 @@ export const analyticsKeys = {
};
/**
* Analytics endpoints require authentication on the backend. Guard React Query
* hooks with `isAuthenticated` so unauthenticated visitors on public routes
* (e.g. homepage) do not fire requests that return 401 and spam the console.
* Analytics endpoints currently require authentication on the backend. We
* gate React Query hooks on `isInitialized` (not `isAuthenticated`) so that:
*
* - Authenticated visitors fire queries the moment `initialize()` finishes,
* even if the React subscription to `isAuthenticated` lags a tick behind
* (we previously saw the homepage stay stuck on "Đang tải..." because the
* gate stayed `false` after the first render and React-Query never refetched).
* - Anonymous visitors fire one request that returns 401 — react-query
* handles this gracefully (silent toast-less rejection in api-client) and
* the components fall back to empty states.
*
* The 401 cost for anon users is preferable to a perpetually empty homepage
* for authed users.
*/
function useAuthedAnalytics() {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const isInitialized = useAuthStore((s) => s.isInitialized);
// Only enable queries once auth state has initialized to avoid a spurious
// disabled → enabled transition on first paint.
return isInitialized && isAuthenticated;
return isInitialized;
}
export function useMarketReport(city: string, period: string) {