dragControls.start(event)}
className="mx-auto flex h-6 w-full cursor-grab touch-none items-center justify-center active:cursor-grabbing"
>
-
+
@@ -265,10 +268,10 @@ export function InstallSheet() {
{/* app card */}
-
+
@@ -276,7 +279,7 @@ export function InstallSheet() {
Freezmo
-
+
{domain}
@@ -302,22 +305,32 @@ export function InstallSheet() {
) : null}
{showNativePrompt ? (
-
+
Or install manually:
) : null}
-
+
{steps.map((step, index) => (
- -
- {index + 1}.
- {step}
+
-
+
+ {index + 1}
+
+ {step}
))}
+
+
@@ -330,17 +343,30 @@ export function InstallSheet() {
/** Small pill for a UI element the user has to tap. */
function Chip({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) {
return (
-
+
{icon}
{children}
);
}
-/** The one coloured element: the final confirmation button. */
+/** Icon-only pill, for buttons that are just a glyph (the ⋯ menu). */
+function IconChip({ icon, label }: { icon: React.ReactNode; label: string }) {
+ return (
+
+ {icon}
+
+ );
+}
+
+/** The one coloured element: the final confirmation button (iOS blue). */
function AccentPill({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) {
return (
-
+
{icon}
{children}
diff --git a/components/icons.tsx b/components/icons.tsx
index cef1091..ccc9633 100644
--- a/components/icons.tsx
+++ b/components/icons.tsx
@@ -307,13 +307,10 @@ export function EllipsisIcon({ className }: IconProps) {
);
}
-export function MoreIcon({ className }: IconProps) {
+export function ChevronDownIcon({ className }: IconProps) {
return (
);
}
diff --git a/lib/client-api.ts b/lib/client-api.ts
index 599f64b..d924771 100644
--- a/lib/client-api.ts
+++ b/lib/client-api.ts
@@ -200,12 +200,25 @@ export type Health = {
};
export async function fetchHealth(): Promise {
+ let response: Response;
try {
- const response = await fetch("/api/health", { cache: "no-store", headers: authHeaders() });
- return (await response.json()) as Health;
+ response = await fetch("/api/health", { cache: "no-store", headers: authHeaders() });
} catch {
throw new ApiError("Could not reach the Freezmo server.", "network");
}
+ const body = (await response.json().catch(() => null)) as (Partial & { error?: string; kind?: string }) | null;
+ if (!response.ok) {
+ throw new ApiError(
+ body?.error ?? `The health check failed with HTTP ${response.status}.`,
+ body?.kind ?? "http",
+ );
+ }
+ // Guard against anything that isn't a health payload (e.g. a proxy error
+ // page) so consumers can rely on the shape.
+ if (!body || typeof body !== "object" || !body.vision || !body.text) {
+ throw new ApiError("The Freezmo server sent an unexpected health response.", "bad_response");
+ }
+ return body as Health;
}
export type KeyCheck =