151773ab51
Wrap check_all() call inside the loop with try/except so a transient error cannot exit the while-True and freeze all health statuses. Add transition logging (HEALTHY / UNHEALTHY) so docker logs show when a service changes state. Also add refetchIntervalInBackground on the frontend query so the poll continues even when the browser tab is not focused. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
import { Link } from "react-router-dom";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { getMe, getServices } from "../api/client";
|
|
|
|
const cardBase: React.CSSProperties = {
|
|
backgroundColor: "rgb(var(--color-surface))",
|
|
border: "1px solid rgb(var(--color-border))",
|
|
borderRadius: 8,
|
|
padding: 24,
|
|
width: 280,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 12,
|
|
};
|
|
|
|
const clickableCard: React.CSSProperties = {
|
|
...cardBase,
|
|
cursor: "pointer",
|
|
textDecoration: "none",
|
|
color: "inherit",
|
|
transition: "box-shadow 150ms, border-color 150ms",
|
|
};
|
|
|
|
const unavailableCard: React.CSSProperties = {
|
|
...cardBase,
|
|
opacity: 0.6,
|
|
};
|
|
|
|
export default function AppsPage() {
|
|
const { data: user } = useQuery({ queryKey: ["me"], queryFn: getMe });
|
|
const { data: services = [] } = useQuery({
|
|
queryKey: ["services"],
|
|
queryFn: getServices,
|
|
refetchInterval: 30_000,
|
|
refetchIntervalInBackground: true,
|
|
});
|
|
|
|
return (
|
|
<div style={{ padding: 32, maxWidth: 900, margin: "0 auto" }}>
|
|
<h1>Apps</h1>
|
|
<div style={{ display: "flex", gap: 24, flexWrap: "wrap", marginTop: 24 }}>
|
|
{services.map((svc) => {
|
|
const canOpen = svc.healthy && !!svc.app_path;
|
|
const CardWrapper = canOpen ? Link : "div";
|
|
const wrapperProps = canOpen
|
|
? {
|
|
to: svc.app_path,
|
|
style: clickableCard,
|
|
onMouseEnter: (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
e.currentTarget.style.boxShadow = "0 4px 12px rgb(0 0 0 / 0.12)";
|
|
e.currentTarget.style.borderColor = "rgb(var(--color-primary))";
|
|
},
|
|
onMouseLeave: (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
e.currentTarget.style.boxShadow = "";
|
|
e.currentTarget.style.borderColor = "rgb(var(--color-border))";
|
|
},
|
|
}
|
|
: { style: svc.healthy ? cardBase : unavailableCard };
|
|
|
|
return (
|
|
<CardWrapper key={svc.id} {...(wrapperProps as any)}>
|
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
<h2 style={{ margin: 0, fontSize: 18 }}>{svc.name}</h2>
|
|
{svc.healthy ? (
|
|
<span style={{ fontSize: 12, color: "#2a9d8f", fontWeight: 600 }}>Available</span>
|
|
) : (
|
|
<span style={{ fontSize: 12, color: "#e76f51", fontWeight: 600 }}>Unavailable</span>
|
|
)}
|
|
</div>
|
|
<p style={{ margin: 0, color: "rgb(var(--color-text-muted))", fontSize: 14 }}>
|
|
{svc.description}
|
|
</p>
|
|
{!svc.healthy && (
|
|
<p style={{ margin: 0, fontSize: 12, color: "#e76f51" }}>
|
|
This service is currently unavailable. Please try again later or contact your administrator.
|
|
</p>
|
|
)}
|
|
<div style={{ display: "flex", gap: 8, marginTop: "auto" }}>
|
|
{user?.is_admin && svc.settings_path && (
|
|
<Link
|
|
to={svc.settings_path}
|
|
onClick={(e) => e.stopPropagation()}
|
|
style={{
|
|
padding: "6px 14px",
|
|
border: "1px solid #ccc",
|
|
borderRadius: 4,
|
|
textDecoration: "none",
|
|
fontSize: 14,
|
|
color: "#333",
|
|
}}
|
|
title="Settings"
|
|
>
|
|
Settings
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</CardWrapper>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|