From 2d6726dcd7c32dc0f4be2b73418f0798e3bac4d3 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Tue, 17 Feb 2026 19:54:15 +0000 Subject: [PATCH] Show last listing update time next to connection status in header Add last_updated timestamp to /api/status endpoint by querying MAX(last_seen) across both listing tables. Display it in the HealthIndicator as relative time (e.g. "2h ago") with full date/time in the tooltip on hover. --- api/app.py | 9 +++++-- frontend/src/components/HealthIndicator.tsx | 30 +++++++++++++++++++-- frontend/src/services/healthService.ts | 2 ++ repositories/listing_repository.py | 16 +++++++++++ tests/integration/test_api.py | 3 ++- 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/api/app.py b/api/app.py index 2ddee0f..8cefbea 100644 --- a/api/app.py +++ b/api/app.py @@ -139,8 +139,13 @@ async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONR @app.get("/api/status") -async def get_status() -> dict[str, str]: - return {"status": "OK"} +async def get_status() -> dict[str, str | None]: + repository = ListingRepository(engine) + last_updated = repository.get_last_updated() + return { + "status": "OK", + "last_updated": last_updated.isoformat() if last_updated else None, + } @app.get("/api/listing") diff --git a/frontend/src/components/HealthIndicator.tsx b/frontend/src/components/HealthIndicator.tsx index d511b02..cd224e3 100644 --- a/frontend/src/components/HealthIndicator.tsx +++ b/frontend/src/components/HealthIndicator.tsx @@ -8,6 +8,20 @@ interface HealthIndicatorProps { interval?: number; } +function formatRelativeTime(isoString: string): string { + const date = new Date(isoString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + return `${diffDays}d ago`; +} + export function HealthIndicator({ interval = 30000 }: HealthIndicatorProps) { const [health, setHealth] = useState({ status: 'checking' }); @@ -46,12 +60,19 @@ export function HealthIndicator({ interval = 30000 }: HealthIndicatorProps) { }; const getTooltipContent = () => { + const lines: string[] = []; + if (health.status === 'checking') { return 'Checking backend connection...'; } if (health.status === 'healthy') { - return `Backend connected (${health.latencyMs}ms)`; + lines.push(`Backend connected (${health.latencyMs}ms)`); + if (health.lastUpdated) { + const date = new Date(health.lastUpdated); + lines.push(`Last update: ${date.toLocaleString()}`); + } + return lines.join('\n'); } return `Backend unavailable: ${health.error || 'Unknown error'}`; @@ -72,10 +93,15 @@ export function HealthIndicator({ interval = 30000 }: HealthIndicatorProps) { + {health.status === 'healthy' && health.lastUpdated && ( + + ยท {formatRelativeTime(health.lastUpdated)} + + )} -

{getTooltipContent()}

+

{getTooltipContent()}

diff --git a/frontend/src/services/healthService.ts b/frontend/src/services/healthService.ts index b5fb382..a913b7c 100644 --- a/frontend/src/services/healthService.ts +++ b/frontend/src/services/healthService.ts @@ -6,6 +6,7 @@ export interface HealthCheckResult { status: HealthStatus; latencyMs?: number; error?: string; + lastUpdated?: string; } /** @@ -39,6 +40,7 @@ export async function checkBackendHealth(): Promise { return { status: 'healthy', latencyMs, + lastUpdated: data.last_updated ?? undefined, }; } diff --git a/repositories/listing_repository.py b/repositories/listing_repository.py index ac74200..3823847 100644 --- a/repositories/listing_repository.py +++ b/repositories/listing_repository.py @@ -359,6 +359,22 @@ class ListingRepository: return model_listing + def get_last_updated(self) -> datetime | None: + """Get the most recent last_seen timestamp across all listings. + + Checks both RentListing and BuyListing tables and returns the latest. + """ + with Session(self.engine) as session: + rent_max = session.execute( + sa_select(func.max(RentListing.last_seen)) + ).scalar() + buy_max = session.execute( + sa_select(func.max(BuyListing.last_seen)) + ).scalar() + + candidates = [t for t in (rent_max, buy_max) if t is not None] + return max(candidates) if candidates else None + def get_listing_ids( self, listing_type: ListingType = ListingType.RENT, diff --git a/tests/integration/test_api.py b/tests/integration/test_api.py index 2002884..1d79fb6 100644 --- a/tests/integration/test_api.py +++ b/tests/integration/test_api.py @@ -16,7 +16,8 @@ class TestStatusEndpoint: """Test that status endpoint returns OK status.""" response = await async_client.get("/api/status") assert response.status_code == 200 - assert response.json() == {"status": "OK"} + assert response.json()["status"] == "OK" + assert "last_updated" in response.json() class TestListingEndpoint: