Skip to content

Commit 84d539f

Browse files
committed
fix: registry client uses static JSON paths for CF Pages compatibility
Update RegistryClient to fetch from /api/registry/components.json and /api/registry/components/{name}.json (pre-built static files), with fallback to Next.js API routes. Works on CF Pages, GitHub Pages, and server mode. Add lux registry URL (ui.lux.finance).
1 parent 15c9f8b commit 84d539f

File tree

1 file changed

+121
-34
lines changed
  • pkg/hanzo-tools-ui/hanzo_tools/ui/registry

1 file changed

+121
-34
lines changed

pkg/hanzo-tools-ui/hanzo_tools/ui/registry/client.py

Lines changed: 121 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
"""Registry client — fetches component data from the Hanzo UI registry server.
1+
"""Registry client — fetches component data from the Hanzo UI site.
2+
3+
Works with both:
4+
- Static hosting (CF Pages, GitHub Pages): fetches pre-built JSON from /api/registry/
5+
- Server mode (Hanzo PaaS, dev): hits Next.js API routes at /api/registry/
26
37
Mirrors the GitHubAPIClient / LocalUIClient interface so it can be used
48
as a drop-in backend in UiTool. Has its own short TTL cache to avoid
@@ -11,12 +15,17 @@
1115

1216
import httpx
1317

18+
# Registry URLs by framework
19+
REGISTRY_URLS: dict[str, str] = {
20+
"hanzo": "https://ui.hanzo.ai",
21+
"lux": "https://ui.lux.finance",
22+
}
1423
DEFAULT_REGISTRY_URL = "https://ui.hanzo.ai"
15-
CLIENT_CACHE_TTL = 60 # 1 minute local cache
24+
CLIENT_CACHE_TTL = 300 # 5 min local cache (server data is pre-built)
1625

1726

1827
class RegistryClient:
19-
"""Client for the Hanzo UI registry server."""
28+
"""Client for the Hanzo UI registry (static or dynamic)."""
2029

2130
def __init__(self, base_url: str | None = None):
2231
self._base_url = (
@@ -27,12 +36,16 @@ def __init__(self, base_url: str | None = None):
2736
self._client: httpx.AsyncClient | None = None
2837
self._cache: dict[str, tuple[Any, float]] = {}
2938
self._available: bool | None = None
39+
# Full index cache — hydrated on first use
40+
self._index: dict | None = None
41+
self._index_ts: float = 0
3042

3143
async def _get_client(self) -> httpx.AsyncClient:
3244
if self._client is None:
3345
self._client = httpx.AsyncClient(
3446
base_url=self._base_url,
3547
timeout=15.0,
48+
follow_redirects=True,
3649
headers={"User-Agent": "Hanzo-MCP-UI-Tool"},
3750
)
3851
return self._client
@@ -64,11 +77,17 @@ async def _get(self, path: str, params: dict | None = None) -> Any:
6477
return data
6578

6679
async def check_available(self) -> bool:
67-
"""Check if the registry server is reachable."""
80+
"""Check if the registry is reachable (tries static files first)."""
6881
if self._available is not None:
6982
return self._available
7083
try:
7184
client = await self._get_client()
85+
# Try the static components.json (works on CF Pages / static hosting)
86+
resp = await client.get("/api/registry/components.json", timeout=5.0)
87+
if resp.status_code == 200:
88+
self._available = True
89+
return True
90+
# Try server-mode health endpoint
7291
resp = await client.get("/api/health", timeout=5.0)
7392
self._available = resp.status_code == 200
7493
except Exception:
@@ -77,65 +96,133 @@ async def check_available(self) -> bool:
7796

7897
@property
7998
def available(self) -> bool | None:
80-
"""Cached availability check result. None if not yet checked."""
8199
return self._available
82100

101+
async def _ensure_index(self) -> dict:
102+
"""Fetch and cache the full index (single HTTP call, all components)."""
103+
if self._index and time.time() - self._index_ts < CLIENT_CACHE_TTL:
104+
return self._index
105+
106+
try:
107+
# Try static file first (CF Pages)
108+
data = await self._get("/api/registry/index.json")
109+
except Exception:
110+
# Fallback to server-mode API route
111+
data = await self._get("/api/registry/index")
112+
113+
self._index = data
114+
self._index_ts = time.time()
115+
return data
116+
117+
def _extract_source(self, component: dict) -> str | None:
118+
"""Extract source code from a registry component entry."""
119+
files = component.get("files", [])
120+
if not files:
121+
return None
122+
first = files[0]
123+
if isinstance(first, dict):
124+
return first.get("content")
125+
return None
126+
83127
# --- Mirror of GitHubAPIClient / LocalUIClient interface ---
84128

85129
async def list_components(self, framework: str = "hanzo") -> list[dict]:
86-
data = await self._get("/api/components", {"framework": framework})
130+
try:
131+
data = await self._get("/api/registry/components.json")
132+
except Exception:
133+
data = await self._get("/api/registry", {"type": f"components:ui"})
87134
return data.get("components", [])
88135

89136
async def fetch_component(self, name: str, framework: str = "hanzo") -> str:
90-
data = await self._get(f"/api/components/{name}", {"framework": framework})
91-
source = data.get("source")
137+
try:
138+
# Try static file (CF Pages)
139+
data = await self._get(f"/api/registry/components/{name}.json")
140+
except Exception:
141+
# Fallback to API route
142+
data = await self._get(f"/api/registry/components/{name}")
143+
144+
source = self._extract_source(data)
92145
if source is None:
93146
raise FileNotFoundError(f"Component '{name}' source not available")
94147
return source
95148

96149
async def fetch_component_demo(self, name: str, framework: str = "hanzo") -> str:
97-
data = await self._get(
98-
f"/api/components/{name}/demo", {"framework": framework}
99-
)
100-
demo = data.get("demo")
101-
if demo is None:
150+
try:
151+
data = await self._get(f"/api/registry/components/{name}-demo.json")
152+
except Exception:
153+
try:
154+
data = await self._get(f"/api/registry/components/{name}-demo")
155+
except Exception:
156+
raise FileNotFoundError(f"Demo for '{name}' not available")
157+
158+
source = self._extract_source(data)
159+
if source is None:
102160
raise FileNotFoundError(f"Demo for '{name}' not available")
103-
return demo
161+
return source
104162

105163
async def fetch_component_metadata(
106164
self, name: str, framework: str = "hanzo"
107165
) -> dict:
108-
data = await self._get(
109-
f"/api/components/{name}/metadata", {"framework": framework}
110-
)
111-
return data.get("metadata", data)
166+
try:
167+
data = await self._get(f"/api/registry/components/{name}.json")
168+
except Exception:
169+
data = await self._get(f"/api/registry/components/{name}")
170+
171+
return {
172+
"name": data.get("name", name),
173+
"type": data.get("type"),
174+
"dependencies": data.get("dependencies", []),
175+
"registryDependencies": data.get("registryDependencies", []),
176+
"source": "registry",
177+
}
112178

113179
async def list_blocks(self, framework: str = "hanzo") -> list[dict]:
114-
data = await self._get("/api/blocks", {"framework": framework})
115-
return data.get("blocks", [])
180+
comps = await self.list_components(framework)
181+
return [c for c in comps if "block" in c.get("type", "").lower()]
116182

117183
async def fetch_block(self, name: str, framework: str = "hanzo") -> str:
118-
data = await self._get(f"/api/blocks/{name}", {"framework": framework})
119-
source = data.get("source")
120-
if source is None:
121-
raise FileNotFoundError(f"Block '{name}' source not available")
122-
return source
184+
return await self.fetch_component(name, framework)
123185

124-
async def search_components(self, query: str, framework: str = "hanzo") -> list[dict]:
125-
data = await self._get("/api/search", {"q": query, "framework": framework})
126-
return data.get("results", [])
186+
async def search_components(
187+
self, query: str, framework: str = "hanzo"
188+
) -> list[dict]:
189+
try:
190+
# Try server-mode search
191+
data = await self._get("/api/registry/search", {"q": query})
192+
return data.get("results", [])
193+
except Exception:
194+
pass
195+
196+
# Fallback: search client-side using the static index
197+
try:
198+
data = await self._get("/api/registry/search-index.json")
199+
except Exception:
200+
data = await self.list_components(framework)
201+
q = query.lower()
202+
return [c for c in data if q in c.get("name", "").lower()]
203+
204+
q = query.lower()
205+
return [
206+
{"name": item["n"], "type": item.get("t", "")}
207+
for item in data
208+
if q in item.get("n", "").lower()
209+
]
127210

128211
async def get_directory_structure(
129212
self, path: str, framework: str = "hanzo"
130213
) -> dict:
131-
return await self._get("/api/structure", {"path": path, "framework": framework})
214+
# Not available as static — return component list as structure
215+
comps = await self.list_components(framework)
216+
return {
217+
"path": path or "/",
218+
"children": [
219+
{"name": c["name"], "type": "file"} for c in comps
220+
],
221+
}
132222

133223
async def fetch_full_index(self, framework: str = "hanzo") -> dict:
134-
"""Fetch the full registry index — all components in one payload.
135-
136-
Use this to hydrate a local cache with a single HTTP call.
137-
"""
138-
return await self._get("/registry/index.json", {"framework": framework})
224+
"""Fetch the full registry index — all components in one payload."""
225+
return await self._ensure_index()
139226

140227
async def close(self) -> None:
141228
if self._client is not None:

0 commit comments

Comments
 (0)