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:
parent
7833bd3ecf
commit
2d6726dcd7
5 changed files with 55 additions and 5 deletions
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue