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.
This commit is contained in:
Viktor Barzin 2026-02-17 19:54:15 +00:00
parent 7833bd3ecf
commit 2d6726dcd7
No known key found for this signature in database
GPG key ID: 0EB088298288D958
5 changed files with 55 additions and 5 deletions

View file

@ -139,8 +139,13 @@ async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONR
@app.get("/api/status") @app.get("/api/status")
async def get_status() -> dict[str, str]: async def get_status() -> dict[str, str | None]:
return {"status": "OK"} 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") @app.get("/api/listing")

View file

@ -8,6 +8,20 @@ interface HealthIndicatorProps {
interval?: number; 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) { export function HealthIndicator({ interval = 30000 }: HealthIndicatorProps) {
const [health, setHealth] = useState<HealthCheckResult>({ status: 'checking' }); const [health, setHealth] = useState<HealthCheckResult>({ status: 'checking' });
@ -46,12 +60,19 @@ export function HealthIndicator({ interval = 30000 }: HealthIndicatorProps) {
}; };
const getTooltipContent = () => { const getTooltipContent = () => {
const lines: string[] = [];
if (health.status === 'checking') { if (health.status === 'checking') {
return 'Checking backend connection...'; return 'Checking backend connection...';
} }
if (health.status === 'healthy') { 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'}`; return `Backend unavailable: ${health.error || 'Unknown error'}`;
@ -72,10 +93,15 @@ export function HealthIndicator({ interval = 30000 }: HealthIndicatorProps) {
<span className={`text-xs ${getStatusColor(health.status)} hidden sm:inline`}> <span className={`text-xs ${getStatusColor(health.status)} hidden sm:inline`}>
{getStatusLabel(health.status)} {getStatusLabel(health.status)}
</span> </span>
{health.status === 'healthy' && health.lastUpdated && (
<span className="text-xs text-muted-foreground hidden sm:inline">
· {formatRelativeTime(health.lastUpdated)}
</span>
)}
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent side="bottom">
<p>{getTooltipContent()}</p> <p className="whitespace-pre-line">{getTooltipContent()}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>

View file

@ -6,6 +6,7 @@ export interface HealthCheckResult {
status: HealthStatus; status: HealthStatus;
latencyMs?: number; latencyMs?: number;
error?: string; error?: string;
lastUpdated?: string;
} }
/** /**
@ -39,6 +40,7 @@ export async function checkBackendHealth(): Promise<HealthCheckResult> {
return { return {
status: 'healthy', status: 'healthy',
latencyMs, latencyMs,
lastUpdated: data.last_updated ?? undefined,
}; };
} }

View file

@ -359,6 +359,22 @@ class ListingRepository:
return model_listing 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( def get_listing_ids(
self, self,
listing_type: ListingType = ListingType.RENT, listing_type: ListingType = ListingType.RENT,

View file

@ -16,7 +16,8 @@ class TestStatusEndpoint:
"""Test that status endpoint returns OK status.""" """Test that status endpoint returns OK status."""
response = await async_client.get("/api/status") response = await async_client.get("/api/status")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "OK"} assert response.json()["status"] == "OK"
assert "last_updated" in response.json()
class TestListingEndpoint: class TestListingEndpoint: