1 <!DOCTYPE html>2 <html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6 <title>Hola Cloud Console Prototype</title>7 <script src="https://cdn.tailwindcss.com"></script>8 <script>9 tailwind.config = {10 theme: {11 extend: {12 fontFamily: {13 sans: ['"Inter"', 'system-ui', 'sans-serif'],14 },15 colors: {16 'hc-panel': '#0f1729',17 },18 },19 },20 };21 </script>22 <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>23 </head>24 <body class="bg-slate-950 text-slate-100">25 <div id="app" class="min-h-screen flex flex-col">26 <header class="bg-slate-900/80 border-b border-slate-800 px-6 py-3">27 <div class="flex items-center justify-between gap-4">28 <div class="flex items-center gap-3">29 <div class="h-10 w-10 rounded-full bg-emerald-500/10 flex items-center justify-center border border-emerald-500/40">30 <span class="text-emerald-400 text-xl font-semibold">☁️</span>31 </div>32 <div>33 <div class="text-lg font-semibold tracking-wide">hola.cloud console</div>34 <div class="text-xs text-slate-400 uppercase tracking-[0.3em]">UNIFIED CONTROL PLANE</div>35 </div>36 </div>37 <div class="flex-1 flex items-center justify-center gap-3 max-w-2xl">38 <div class="flex items-center gap-2 bg-slate-900/60 border border-slate-800 rounded-lg px-4 py-2 shadow-inner shadow-slate-900/60">39 <span class="text-xs uppercase tracking-wide text-slate-400">Project</span>40 <select v-model="selectedProjectId" class="bg-transparent text-sm font-medium focus:outline-none focus:ring-0">41 <option v-for="project in projects" :key="project.id" :value="project.id" class="bg-slate-900 text-slate-200">{{ project.name }}</option>42 </select>43 <span class="text-[10px] px-2 py-0.5 rounded-full border border-emerald-500/40 text-emerald-300 bg-emerald-500/10" v-if="selectedProject?.auth?.enabled">AUTH ENABLED</span>44 <span class="text-[10px] px-2 py-0.5 rounded-full border border-amber-500/40 text-amber-300 bg-amber-500/10" v-else>PUBLIC BUILD</span>45 </div>46 <button @click="openProjectManager" class="px-4 py-2 text-sm font-medium rounded-lg border border-slate-700/60 bg-slate-900/50 hover:border-emerald-500/60 hover:text-emerald-300 transition">Manage projects</button>47 </div>48 <div class="flex items-center gap-3">49 <div class="text-right">50 <div class="text-sm font-medium">Lucía Ops</div>51 <div class="text-xs text-slate-400">Console admin</div>52 </div>53 <button class="h-10 w-10 rounded-full bg-slate-800 border border-slate-700 flex items-center justify-center text-slate-300 hover:text-emerald-300 transition">⎋</button>54 </div>55 </div>56 </header>57 <main class="flex flex-1 overflow-hidden">58 <aside class="w-80 bg-slate-900/40 border-r border-slate-800 flex flex-col">59 <div class="p-4 space-y-3">60 <div>61 <label class="text-xs uppercase tracking-wide text-slate-400">Resources</label>62 <input v-model="resourceSearch" type="search" placeholder="Search services or resources" class="mt-2 w-full rounded-lg bg-slate-900/60 border border-slate-800 px-3 py-2 text-sm focus:outline-none focus:border-emerald-500/60" />63 </div>64 <div class="flex items-center justify-between text-[10px] uppercase text-slate-500 tracking-wide">65 <span>Project scope</span>66 <span>{{ selectedProject?.name }}</span>67 </div>68 </div>69 <div class="flex-1 overflow-auto px-3 pb-6">70 <ul class="space-y-1">71 <tree-node72 v-for="node in filteredTree"73 :key="node.id"74 :node="node"75 :depth="0"76 :toggle="toggleNode"77 :open="openNode"78 :is-expanded="isNodeExpanded"79 :active-id="activeTab?.nodeId"80 :is-dimmed="isNodeDimmed"81 ></tree-node>82 </ul>83 </div>84 <div class="border-t border-slate-800 bg-slate-900/60 px-4 py-3 text-xs text-slate-400 space-y-1">85 <div class="flex items-center justify-between">86 <span>Open tabs</span>87 <span class="text-slate-300 font-medium">{{ tabs.length }}</span>88 </div>89 <div class="flex items-center justify-between">90 <span>Spec sync</span>91 <span class="text-emerald-300">Mocked</span>92 </div>93 </div>94 </aside>95 <section class="flex-1 flex flex-col overflow-hidden">96 <div class="border-b border-slate-800 bg-slate-900/70">97 <div class="flex items-center overflow-x-auto px-4">98 <button99 v-for="tab in tabs"100 :key="tab.id"101 @click="activateTab(tab.id)"102 class="flex items-center gap-2 px-4 py-3 text-sm transition border-b-2"103 :class="tab.id === activeTabId ? 'border-emerald-400 text-emerald-200 bg-slate-900/80' : 'border-transparent text-slate-400 hover:text-slate-200'"104 >105 <span v-if="tab.icon" class="text-lg leading-none">{{ tab.icon }}</span>106 <span class="whitespace-nowrap">{{ tab.label }}</span>107 <span v-if="tab.id !== 'projects'" @click.stop="closeTab(tab.id)" class="ml-2 text-xs text-slate-500 hover:text-rose-300">✕</span>108 </button>109 </div>110 </div>111 <div class="flex-1 overflow-auto bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">112 <div class="p-6 space-y-6">113 <template v-if="activeTab?.type === 'project-overview'">114 115 <div class="grid gap-6 lg:grid-cols-3">116 <div class="lg:col-span-2 bg-slate-900/70 border border-slate-800 rounded-2xl p-6 shadow-lg shadow-slate-900/60">117 <div class="flex items-start justify-between">118 <div>119 <h2 class="text-xl font-semibold">{{ selectedProject?.name }}</h2>120 <p class="text-sm text-slate-400 mt-1">{{ selectedProject?.description }}</p>121 </div>122 <div class="text-right text-xs text-slate-400 space-y-1">123 <div>Created: <span class="text-slate-200">{{ formatTimestamp(selectedProject?.create_timestamp) }}</span></div>124 <div>Updated: <span class="text-slate-200">{{ formatTimestamp(selectedProject?.update_timestamp) }}</span></div>125 <div>Status: <span :class="selectedProject?.deleted ? 'text-rose-300' : 'text-emerald-300'">{{ selectedProject?.deleted ? 'Archived' : 'Active' }}</span></div>126 </div>127 </div>128 <div class="mt-6 grid gap-4 sm:grid-cols-2">129 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4">130 <div class="text-xs uppercase tracking-wide text-slate-400">Owners</div>131 <ul class="mt-2 space-y-1 text-sm">132 <li v-for="owner in selectedProject?.owners" :key="owner" class="flex items-center gap-2">133 <span class="h-2 w-2 rounded-full bg-emerald-400"></span>134 <span>{{ owner }}</span>135 </li>136 </ul>137 </div>138 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4">139 <div class="text-xs uppercase tracking-wide text-slate-400">Resource footprint</div>140 <dl class="mt-2 grid grid-cols-2 gap-x-4 gap-y-2 text-sm">141 <div v-for="(value, key) in selectedProject?.metrics" :key="key" class="flex items-baseline gap-2">142 <dt class="text-slate-500 capitalize">{{ key }}</dt>143 <dd class="text-slate-200 font-semibold">{{ value }}</dd>144 </div>145 </dl>146 </div>147 </div>148 <div class="mt-6">149 <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wide">Routers (Projects API)</h3>150 <div v-if="selectedProject?.routers?.length" class="mt-3 space-y-3">151 <div v-for="(router, index) in selectedProject.routers" :key="index" class="border border-slate-800 rounded-xl p-4 bg-slate-950/30">152 <div class="flex items-start justify-between gap-4">153 <div>154 <div class="text-sm font-medium text-slate-200">{{ router.type }} router</div>155 <div class="text-xs text-slate-400">Hosts exposed by <code class="font-mono text-emerald-300">/v0/projects</code></div>156 </div>157 <div class="text-xs text-slate-400 space-y-1">158 <div>Headers in: <span class="text-slate-200">{{ displayKeyValue(router.HeadersIn) }}</span></div>159 <div>Headers out: <span class="text-slate-200">{{ displayKeyValue(router.HeadersOut) }}</span></div>160 </div>161 </div>162 <div class="mt-3 text-xs">163 <div class="uppercase tracking-wide text-slate-400">Hosts</div>164 <div class="mt-2 grid gap-2">165 <div v-for="host in router.hosts" :key="host.name" class="flex items-center justify-between bg-slate-900/60 border border-slate-800 rounded-lg px-3 py-2">166 <div class="text-sm text-slate-200">{{ host.name }}</div>167 <div class="text-[10px] text-slate-400">168 Added {{ formatTimestamp(host.creation_timestamp) }} ·169 <span :class="host.verified ? 'text-emerald-300' : 'text-amber-300'">{{ host.verified ? 'Verified' : 'Pending' }}</span>170 </div>171 </div>172 </div>173 </div>174 <div v-if="router.config" class="mt-3 text-xs text-slate-400">175 <div class="uppercase tracking-wide">Edge config</div>176 <div class="mt-1 leading-relaxed text-slate-300">{{ displayKeyValue(router.config) }}</div>177 </div>178 </div>179 </div>180 <div v-else class="mt-3 text-sm text-slate-400 italic">No routers defined yet for this project.</div>181 </div>182 </div>183 <div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-6 shadow-lg shadow-slate-900/60 flex flex-col">184 <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wide">Create project (mock)</h3>185 <p class="text-xs text-slate-400 mt-1">Follows <code class="font-mono text-emerald-300">POST /v0/projects</code> by capturing name and owners.</p>186 <form @submit.prevent="createProject" class="mt-4 space-y-3 text-sm">187 <div>188 <label class="text-xs uppercase tracking-wide text-slate-400">Project name</label>189 <input v-model="projectForm.name" type="text" required class="mt-1 w-full rounded-lg bg-slate-950/60 border border-slate-800 px-3 py-2 focus:outline-none focus:border-emerald-500/60" placeholder="New project name" />190 </div>191 <div>192 <label class="text-xs uppercase tracking-wide text-slate-400">Owners (comma separated)</label>193 <input v-model="projectForm.owners" type="text" class="mt-1 w-full rounded-lg bg-slate-950/60 border border-slate-800 px-3 py-2 focus:outline-none focus:border-emerald-500/60" placeholder="ops@hola.cloud, dev@hola.cloud" />194 </div>195 <div>196 <label class="text-xs uppercase tracking-wide text-slate-400">Edge host (optional)</label>197 <input v-model="projectForm.routerHost" type="text" class="mt-1 w-full rounded-lg bg-slate-950/60 border border-slate-800 px-3 py-2 focus:outline-none focus:border-emerald-500/60" placeholder="project.hola.cloud" />198 </div>199 <button type="submit" class="w-full py-2 rounded-lg bg-emerald-500/20 border border-emerald-500/50 text-emerald-200 font-medium hover:bg-emerald-500/30 transition">Create mock project</button>200 </form>201 <div class="mt-4 text-xs text-slate-400">202 <div class="uppercase tracking-wide text-slate-500">Projects API operations</div>203 <ul class="mt-2 space-y-1">204 <li v-for="operation in projectSpec.operations" :key="operation.path + operation.method" class="flex items-center justify-between bg-slate-950/50 border border-slate-800 rounded-lg px-3 py-2">205 <span class="flex items-center gap-2 text-[11px]">206 <span :class="['px-2 py-0.5 rounded-full border text-[10px]', methodBadgeClass(operation.method)]">{{ operation.method }}</span>207 <code class="text-slate-200 text-xs">{{ operation.path }}</code>208 </span>209 <span class="text-[10px] text-slate-400">{{ operation.description }}</span>210 </li>211 </ul>212 </div>213 </div>214 </div>215 <div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-6 shadow-lg shadow-slate-900/60">216 <div class="flex items-center justify-between">217 <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wide">Resources in {{ selectedProject?.name }}</h3>218 <div class="text-xs text-slate-400">{{ activeProjectResources.length }} resources mapped from OpenAPI specs</div>219 </div>220 <div class="mt-4 overflow-auto">221 <table class="min-w-full text-sm">222 <thead class="bg-slate-900/80 text-slate-400 text-xs uppercase tracking-wide">223 <tr>224 <th class="text-left px-3 py-2 font-medium">Service</th>225 <th class="text-left px-3 py-2 font-medium">Resource</th>226 <th class="text-left px-3 py-2 font-medium">Type</th>227 </tr>228 </thead>229 <tbody class="divide-y divide-slate-800/80">230 <tr v-for="resource in activeProjectResources" :key="resource.id" class="hover:bg-slate-900/60">231 <td class="px-3 py-2 text-slate-200">{{ resource.service }}</td>232 <td class="px-3 py-2 text-emerald-200">233 <button @click="openResourceFromList(resource.id)" class="hover:underline">{{ resource.label }}</button>234 </td>235 <td class="px-3 py-2 text-slate-400">{{ resource.type }}</td>236 </tr>237 </tbody>238 </table>239 </div>240 </div>241 <div class="grid gap-6 xl:grid-cols-3">242 <div v-for="service in serviceCards" :key="service.id" class="bg-slate-900/70 border border-slate-800 rounded-2xl p-5 shadow-lg shadow-slate-900/60">243 <div class="flex items-start justify-between gap-4">244 <div>245 <div class="text-sm font-semibold text-slate-200">{{ service.name }}</div>246 <div class="text-xs text-slate-400 mt-1">{{ service.summary }}</div>247 </div>248 <div class="text-xs text-emerald-300">{{ service.operations.length }} endpoints</div>249 </div>250 <ul class="mt-4 space-y-2 text-xs text-slate-400">251 <li v-for="operation in service.operations.slice(0,3)" :key="operation.method + operation.path" class="flex items-center gap-2">252 <span :class="['px-2 py-0.5 rounded-full border', methodBadgeClass(operation.method)]">{{ operation.method }}</span>253 <code class="text-slate-200 text-xs">{{ operation.path }}</code>254 </li>255 </ul>256 <button @click="openServiceById(service.id)" class="mt-4 inline-flex items-center gap-2 text-xs text-emerald-300 hover:text-emerald-200">Inspect service →</button>257 </div>258 </div>259 260 </template>261 <template v-else-if="activeTab?.type === 'service'">262 263 <div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-6 shadow-lg shadow-slate-900/60">264 <div class="flex items-start justify-between gap-4">265 <div>266 <h2 class="text-xl font-semibold">{{ activeTab.detail.name }}</h2>267 <p class="text-sm text-slate-400 mt-1">{{ activeTab.detail.summary }}</p>268 </div>269 <div class="text-xs text-slate-400">270 <div class="uppercase tracking-wide">Service metrics</div>271 <div class="mt-2 grid gap-1">272 <div v-for="(value, key) in activeTab.detail.metrics" :key="key" class="flex items-center gap-2">273 <span class="text-slate-500 capitalize">{{ key }}</span>274 <span class="text-slate-200">{{ value }}</span>275 </div>276 </div>277 </div>278 </div>279 <div class="mt-4 text-sm text-slate-300">{{ activeTab.detail.description }}</div>280 <div class="mt-6 grid gap-6 lg:grid-cols-2">281 <div>282 <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wide">Key endpoints</h3>283 <ul class="mt-3 space-y-2 text-sm">284 <li v-for="operation in activeTab.detail.operations" :key="operation.method + operation.path" class="flex items-start gap-3 bg-slate-950/40 border border-slate-800 rounded-xl px-3 py-2">285 <span :class="['mt-0.5 px-2 py-0.5 rounded-full border text-[10px]', methodBadgeClass(operation.method)]">{{ operation.method }}</span>286 <div>287 <code class="text-emerald-200 text-xs">{{ operation.path }}</code>288 <div class="text-xs text-slate-400">{{ operation.description }}</div>289 </div>290 </li>291 </ul>292 </div>293 <div>294 <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wide">Schema highlights</h3>295 <div class="mt-3 space-y-3">296 <div v-for="schema in activeTab.detail.schemas" :key="schema.name" class="bg-slate-950/40 border border-slate-800 rounded-xl p-4">297 <div class="text-sm font-medium text-slate-200">{{ schema.name }}</div>298 <div class="text-xs text-slate-400">{{ schema.description }}</div>299 <ul class="mt-2 space-y-1 text-xs text-slate-400">300 <li v-for="field in schema.fields" :key="field.name" class="flex items-start gap-2">301 <span class="font-mono text-slate-300">{{ field.name }}</span>302 <span class="text-slate-500">({{ field.type }})</span>303 <span class="text-slate-400">{{ field.description }}</span>304 </li>305 </ul>306 </div>307 </div>308 </div>309 </div>310 <div class="mt-6">311 <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wide">Insights</h3>312 <ul class="mt-3 space-y-2 text-sm text-slate-300">313 <li v-for="(insight, index) in activeTab.detail.insights" :key="index" class="flex items-start gap-2">314 <span class="text-emerald-400">•</span>315 <span>{{ insight }}</span>316 </li>317 </ul>318 </div>319 <div v-if="activeTab.detail.sampleCalls" class="mt-6">320 <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wide">Sample requests</h3>321 <div class="mt-3 grid gap-3 md:grid-cols-2">322 <div v-for="sample in activeTab.detail.sampleCalls" :key="sample.title" class="bg-slate-950/40 border border-slate-800 rounded-xl p-4">323 <div class="text-xs uppercase tracking-wide text-slate-400">{{ sample.title }}</div>324 <div class="mt-2 text-xs text-slate-300">325 <div><span :class="['px-2 py-0.5 rounded-full border text-[10px]', methodBadgeClass(sample.method)]">{{ sample.method }}</span> <code class="ml-2 text-emerald-200">{{ sample.url }}</code></div>326 <div v-if="sample.notes" class="mt-2 text-slate-400">{{ sample.notes }}</div>327 </div>328 </div>329 </div>330 </div>331 </div>332 333 </template>334 <template v-else-if="activeTab">335 336 <div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-6 shadow-lg shadow-slate-900/60 space-y-6">337 <div class="flex items-start justify-between gap-4">338 <div>339 <h2 class="text-xl font-semibold">{{ activeTab.label }}</h2>340 <p class="text-sm text-slate-400 mt-1">{{ activeTab.detail.summary }}</p>341 </div>342 <div class="text-right text-xs text-slate-400 space-y-1">343 <div>Service: <span class="text-slate-200 font-medium">{{ activeTab.detail.service }}</span></div>344 <div>Resource type: <span class="text-slate-200">{{ activeTab.detail.resourceType || 'Resource' }}</span></div>345 <div v-if="activeTab.detail.projects?.length">Projects: <span class="text-emerald-300">{{ activeTab.detail.projects.join(', ') }}</span></div>346 </div>347 </div>348 <div v-if="activeTab.detail.metrics" class="grid gap-3 sm:grid-cols-3">349 <div v-for="(value, key) in activeTab.detail.metrics" :key="key" class="bg-slate-950/40 border border-slate-800 rounded-xl px-4 py-3">350 <div class="text-xs uppercase tracking-wide text-slate-400">{{ key }}</div>351 <div class="mt-1 text-lg font-semibold text-slate-200">{{ value }}</div>352 </div>353 </div>354 <div v-if="activeTab.detail.operations?.length">355 <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wide">API operations</h3>356 <table class="mt-3 w-full text-sm">357 <thead class="bg-slate-900/80 text-xs uppercase tracking-wide text-slate-400">358 <tr>359 <th class="text-left px-3 py-2 font-medium">Method</th>360 <th class="text-left px-3 py-2 font-medium">Path</th>361 <th class="text-left px-3 py-2 font-medium">Description</th>362 </tr>363 </thead>364 <tbody class="divide-y divide-slate-800/80">365 <tr v-for="operation in activeTab.detail.operations" :key="operation.method + operation.path" class="hover:bg-slate-900/60">366 <td class="px-3 py-2">367 <span :class="['px-2 py-0.5 rounded-full border text-[10px]', methodBadgeClass(operation.method)]">{{ operation.method }}</span>368 </td>369 <td class="px-3 py-2 text-emerald-200 font-mono text-xs">{{ operation.path }}</td>370 <td class="px-3 py-2 text-slate-300 text-sm">{{ operation.description }}</td>371 </tr>372 </tbody>373 </table>374 </div>375 <div v-if="activeTab.detail.schemas?.length" class="space-y-4">376 <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wide">Schema</h3>377 <div class="grid gap-4 md:grid-cols-2">378 <div v-for="schema in activeTab.detail.schemas" :key="schema.name" class="bg-slate-950/40 border border-slate-800 rounded-xl p-4">379 <div class="text-sm font-medium text-slate-200">{{ schema.name }}</div>380 <div class="text-xs text-slate-400">{{ schema.description }}</div>381 <ul class="mt-2 space-y-1 text-xs text-slate-400">382 <li v-for="field in schema.fields" :key="field.name" class="flex items-start gap-2">383 <span class="font-mono text-slate-300">{{ field.name }}</span>384 <span class="text-slate-500">({{ field.type }})</span>385 <span>{{ field.description }}</span>386 </li>387 </ul>388 </div>389 </div>390 </div>391 <div v-if="activeTab.detail.mockData" class="space-y-2">392 <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wide">Mock data (aligned with spec)</h3>393 <pre class="bg-slate-950/60 border border-slate-800 rounded-xl p-4 text-xs text-emerald-200 overflow-auto max-h-72">{{ formatJSON(activeTab.detail.mockData) }}</pre>394 </div>395 <div v-if="activeTab.detail.notes?.length">396 <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wide">Notes</h3>397 <ul class="mt-2 space-y-2 text-sm text-slate-300">398 <li v-for="(note, index) in activeTab.detail.notes" :key="index" class="flex items-start gap-2">399 <span class="text-emerald-400">•</span>400 <span>{{ note }}</span>401 </li>402 </ul>403 </div>404 </div>405 406 </template>407 </div>408 </div>409 <div class="border-t border-slate-800 bg-slate-900/70 px-6 py-3 text-xs">410 <div class="flex items-center justify-between text-slate-400">411 <div class="flex items-center gap-3">412 <span class="uppercase tracking-wide text-[10px] text-slate-500">Live telemetry (mocked)</span>413 <span class="h-2 w-2 rounded-full bg-emerald-400 animate-pulse"></span>414 </div>415 <div class="text-slate-500">Following OpenAPI paths across services to plan future integrations.</div>416 </div>417 <div class="mt-2 grid gap-1 lg:grid-cols-2 xl:grid-cols-3 text-[11px] text-slate-400 font-mono">418 <div v-for="event in telemetry" :key="event.time + event.text" class="flex items-center gap-2 bg-slate-950/40 border border-slate-800 rounded-lg px-3 py-2">419 <span class="text-emerald-300">{{ event.time }}</span>420 <span>{{ event.text }}</span>421 </div>422 </div>423 </div>424 </section>425 </main>426 </div>427 428 <template id="tree-node-template">429 <li :class="{ 'mt-1': depth > 0 }">430 <div431 class="flex items-center gap-2 pr-2 py-1.5 rounded-lg cursor-pointer transition"432 :class="[433 node.id === activeId ? 'bg-slate-800/80 ring-1 ring-slate-700/80' : 'hover:bg-slate-800/60',434 dimmed ? 'text-slate-500/80' : 'text-slate-200'435 ]"436 @click="open(node)"437 >438 <button439 v-if="hasChildren"440 @click.stop="toggle(node)"441 class="h-6 w-6 flex items-center justify-center text-slate-500 hover:text-slate-200"442 >443 <span v-if="isExpanded(node)">▾</span>444 <span v-else>▸</span>445 </button>446 <span v-else class="h-6 w-6"></span>447 <div class="flex-1 flex items-center gap-2">448 <span v-if="node.icon" class="text-lg leading-none">{{ node.icon }}</span>449 <span class="text-sm font-medium">{{ node.label }}</span>450 <span v-if="node.badge" class="px-2 py-0.5 rounded-full border border-slate-700/80 text-[10px] uppercase tracking-wide text-slate-400 bg-slate-900/80">{{ node.badge }}</span>451 </div>452 <span v-if="node.projects && node.projects.length" class="text-[10px] text-slate-500">{{ node.projects.length }}P</span>453 </div>454 <ul v-if="hasChildren && isExpanded(node)" class="ml-4 border-l border-slate-800/70 pl-2">455 <tree-node456 v-for="child in node.children"457 :key="child.id"458 :node="child"459 :depth="depth + 1"460 :toggle="toggle"461 :open="open"462 :is-expanded="isExpanded"463 :active-id="activeId"464 :is-dimmed="isDimmed"465 ></tree-node>466 </ul>467 </li>468 </template>469 470 <script>471 const { createApp } = Vue;472 473 const projectCatalog = [474 {475 id: "proj-aurora",476 name: "Aurora Retail",477 description: "Primary production workloads for the Aurora e-commerce storefront.",478 create_timestamp: 1677628800,479 update_timestamp: 1701302400,480 deleted: false,481 owners: ["aurora@hola.cloud", "ops@hola.cloud"],482 auth: { enabled: true },483 routers: [484 {485 type: "edge",486 hosts: [487 { name: "aurora.hola.cloud", creation_timestamp: 1677801600, verified: true },488 { name: "aurora-alt.hola.cloud", creation_timestamp: 1680480000, verified: false }489 ],490 HeadersIn: { "x-request-id": "propagate" },491 HeadersOut: { "strict-transport-security": "max-age=63072000" },492 config: { rate_limit: "180 rps", cors: "https://aurora.hola.cloud", caching: "static assets 30m" }493 }494 ],495 metrics: { buckets: 2, databases: 1, collections: 3, lambdas: 3, loggers: 1, queues: 1, configs: 4 }496 },497 {498 id: "proj-nebula",499 name: "Nebula AI Lab",500 description: "Research environment handling ML datasets, experiments and telemetry.",501 create_timestamp: 1675123200,502 update_timestamp: 1700697600,503 deleted: false,504 owners: ["nebula@hola.cloud"],505 auth: { enabled: false },506 routers: [507 {508 type: "edge",509 hosts: [{ name: "nebula.hola.cloud", creation_timestamp: 1675382400, verified: true }],510 HeadersIn: { "x-lab-mode": "beta" },511 HeadersOut: { "cache-control": "no-store" },512 config: { rate_limit: "120 rps", mtls: "internal only" }513 }514 ],515 metrics: { buckets: 1, databases: 1, collections: 2, lambdas: 2, loggers: 1, queues: 2, configs: 2 }516 },517 {518 id: "proj-sandbox",519 name: "Sandbox",520 description: "Isolated QA environment used for experiments and previews.",521 create_timestamp: 1682899200,522 update_timestamp: 1698796800,523 deleted: false,524 owners: ["sandbox@hola.cloud"],525 auth: { enabled: true },526 routers: [],527 metrics: { buckets: 1, databases: 0, collections: 0, lambdas: 1, loggers: 0, queues: 1, configs: 1 }528 }529 ];530 531 const serviceTree = [532 {533 id: "service:files",534 label: "Files",535 icon: "📦",536 badge: "Service",537 type: "service",538 service: "Files",539 defaultExpanded: true,540 detail: {541 type: "service",542 nodeId: "service:files",543 name: "Files (Object Storage)",544 service: "Files",545 summary: "Bucket and object lifecycle management based on the Files API specification.",546 description: "Supports bucket CRUD plus wildcard object routes for upload, download, metadata and listing.",547 metrics: { totalBuckets: 3, objectPrefixes: 12, lifecyclePolicies: 2 },548 insights: [549 "Wildcard routes such as /files/* and /list/* align with the tree navigation in this console.",550 "Bucket schema links each object store to its owning project for governance.",551 "File schema exposes MD5 and SHA256 digests that the UI can surface before downloads."552 ],553 operations: [554 { method: "GET", path: "/v1/buckets", description: "List buckets in scope." },555 { method: "POST", path: "/v1/buckets", description: "Create a bucket with CreateBucketInputV1." },556 { method: "GET", path: "/v1/buckets/{bucket_id}", description: "Fetch bucket metadata." },557 { method: "PATCH", path: "/v1/buckets/{bucket_id}", description: "Update bucket properties." },558 { method: "DELETE", path: "/v1/buckets/{bucket_id}", description: "Delete a bucket." },559 { method: "GET", path: "/v1/buckets/{bucket_id}/list/*", description: "List objects in a prefix." },560 { method: "GET", path: "/v1/buckets/{bucket_id}/files/*", description: "Download an object stream." },561 { method: "PUT", path: "/v1/buckets/{bucket_id}/files/*", description: "Upload or overwrite an object." },562 { method: "DELETE", path: "/v1/buckets/{bucket_id}/files/*", description: "Remove an object." },563 { method: "HEAD", path: "/v1/buckets/{bucket_id}/files/*", description: "Inspect object metadata." }564 ],565 schemas: [566 {567 name: "Bucket",568 description: "Bucket record from the Files API.",569 fields: [570 { name: "id", type: "string", description: "Unique bucket identifier." },571 { name: "name", type: "string", description: "Human readable bucket name." },572 { name: "project_id", type: "string", description: "Owning project id." },573 { name: "owners", type: "array<string>", description: "Maintainers of the bucket." },574 { name: "description", type: "string", description: "Optional notes." },575 { name: "created_timestamp", type: "number", description: "Epoch of creation." }576 ]577 },578 {579 name: "File",580 description: "Metadata returned after uploads.",581 fields: [582 { name: "id", type: "string", description: "Object identifier." },583 { name: "bucket", type: "string", description: "Bucket id for the object." },584 { name: "name", type: "string", description: "Key including prefix." },585 { name: "mime_type", type: "string", description: "Stored MIME type." },586 { name: "size", type: "number", description: "Object size in bytes." },587 { name: "hash_md5", type: "string", description: "MD5 digest." },588 { name: "hash_sha256", type: "string", description: "SHA256 digest." },589 { name: "status", type: "string", description: "Processing state." }590 ]591 }592 ],593 sampleCalls: [594 { method: "GET", url: "/v1/buckets/{bucket_id}/list/*?prefix=products/", title: "List bucket contents", notes: "Drives tree-like navigation for object prefixes." },595 { method: "PUT", url: "/v1/buckets/{bucket_id}/files/catalog/sku.json", title: "Upload product data", notes: "Returns File schema payload for confirmation." }596 ]597 },598 children: [599 {600 id: "files:buckets",601 label: "Buckets",602 badge: "Group",603 type: "group",604 service: "Files",605 detail: {606 type: "group",607 service: "Files",608 resourceType: "Collection",609 summary: "Buckets exposed by /v1/buckets.",610 operations: [611 { method: "GET", path: "/v1/buckets", description: "Enumerate buckets." },612 { method: "POST", path: "/v1/buckets", description: "Create bucket." }613 ],614 mockData: { total: 3 }615 },616 children: [617 {618 id: "files:bucket:product-assets",619 label: "Bucket product-assets",620 badge: "Bucket",621 icon: "🪣",622 type: "resource",623 service: "Files",624 projects: ["proj-aurora"],625 detail: {626 type: "resource",627 service: "Files",628 resourceType: "Bucket",629 summary: "Primary storefront assets for Aurora Retail.",630 projects: ["proj-aurora"],631 metrics: { objects: "4.8k", storageGB: 56.4, versioning: "Enabled" },632 operations: [633 { method: "GET", path: "/v1/buckets/{bucket_id}", description: "Retrieve bucket metadata." },634 { method: "PATCH", path: "/v1/buckets/{bucket_id}", description: "Update description or owners." },635 { method: "DELETE", path: "/v1/buckets/{bucket_id}", description: "Delete bucket when retiring service." },636 { method: "GET", path: "/v1/buckets/{bucket_id}/list/*", description: "List objects for folder navigation." }637 ],638 schemas: [639 {640 name: "Bucket snapshot",641 description: "Contextualized bucket record for product-assets.",642 fields: [643 { name: "id", type: "string", description: "bucket-product-assets" },644 { name: "project_id", type: "string", description: "proj-aurora" },645 { name: "owners", type: "array<string>", description: "aurora@hola.cloud, media@hola.cloud" }646 ]647 }648 ],649 mockData: {650 id: "bucket-product-assets",651 name: "product-assets",652 description: "Images and attachments for Aurora retail storefront.",653 project_id: "proj-aurora",654 owners: ["aurora@hola.cloud", "media@hola.cloud"],655 created_timestamp: 1661990400656 },657 notes: ["Versioning is coordinated with CDN headers configured in Projects API routers."]658 },659 children: [660 {661 id: "files:bucket:product-assets:files",662 label: "files/*",663 badge: "Objects",664 icon: "🗂️",665 type: "resource",666 service: "Files",667 projects: ["proj-aurora"],668 detail: {669 type: "resource",670 service: "Files",671 resourceType: "Object prefix",672 summary: "Wildcard object operations under product-assets.",673 projects: ["proj-aurora"],674 operations: [675 { method: "GET", path: "/v1/buckets/{bucket_id}/files/*", description: "Download object contents." },676 { method: "PUT", path: "/v1/buckets/{bucket_id}/files/*", description: "Upload object bytes." },677 { method: "DELETE", path: "/v1/buckets/{bucket_id}/files/*", description: "Delete object." },678 { method: "HEAD", path: "/v1/buckets/{bucket_id}/files/*", description: "Check metadata only." }679 ],680 mockData: {681 bucket: "bucket-product-assets",682 name: "products/banner.png",683 mime_type: "image/png",684 size: 458923,685 hash_md5: "ec4ed6b0e91b0b1d6aab032d7c2c47cb",686 hash_sha256: "6f2ea1f6f7b62a9af7133185abfa6622f2fe38572be407fb2a8d23c28dbd9c71",687 status: "uploaded",688 created_timestamp: 1695888000,689 updated_timestamp: 1698888000690 },691 notes: ["HEAD route allows preview without downloading full asset."]692 }693 },694 {695 id: "files:bucket:product-assets:list",696 label: "list/*",697 badge: "Listing",698 icon: "🧭",699 type: "resource",700 service: "Files",701 projects: ["proj-aurora"],702 detail: {703 type: "resource",704 service: "Files",705 resourceType: "Listing",706 summary: "Prefix listing used to populate folder view.",707 projects: ["proj-aurora"],708 operations: [709 { method: "GET", path: "/v1/buckets/{bucket_id}/list/*", description: "Enumerate objects under a prefix." }710 ],711 mockData: {712 prefix: "products/",713 objects: [714 { name: "products/banner.png", size: 458923 },715 { name: "products/specs.pdf", size: 1290432 }716 ],717 next_page_token: "eyJvZmZzZXQiOjJ9"718 }719 }720 }721 ]722 },723 {724 id: "files:bucket:ml-datasets",725 label: "Bucket ml-datasets",726 badge: "Bucket",727 icon: "🪣",728 type: "resource",729 service: "Files",730 projects: ["proj-nebula"],731 detail: {732 type: "resource",733 service: "Files",734 resourceType: "Bucket",735 summary: "Machine learning datasets and checkpoints for Nebula AI Lab.",736 projects: ["proj-nebula"],737 metrics: { objects: "12.3k", storageGB: 320.5, lifecycle: "Coldline after 30d" },738 operations: [739 { method: "GET", path: "/v1/buckets/{bucket_id}", description: "Read bucket metadata." },740 { method: "PATCH", path: "/v1/buckets/{bucket_id}", description: "Adjust lifecycle for archived datasets." },741 { method: "GET", path: "/v1/buckets/{bucket_id}/list/*", description: "List dataset files." }742 ],743 mockData: {744 id: "bucket-ml-datasets",745 name: "ml-datasets",746 project_id: "proj-nebula",747 owners: ["nebula@hola.cloud"],748 description: "Training corpora and model artifacts.",749 created_timestamp: 1656633600750 },751 notes: ["Uploads rely on the same wildcard file endpoints as product-assets."]752 }753 },754 {755 id: "files:bucket:raw-logs",756 label: "Bucket raw-logs",757 badge: "Bucket",758 icon: "🪣",759 type: "resource",760 service: "Files",761 projects: ["proj-aurora", "proj-sandbox"],762 detail: {763 type: "resource",764 service: "Files",765 resourceType: "Bucket",766 summary: "Holds exported audit logs shared across Aurora and Sandbox.",767 projects: ["proj-aurora", "proj-sandbox"],768 metrics: { objects: "2.1k", storageGB: 18.2, retention: "14 days" },769 operations: [770 { method: "GET", path: "/v1/buckets/{bucket_id}", description: "Inspect log bucket metadata." },771 { method: "DELETE", path: "/v1/buckets/{bucket_id}", description: "Allow clean-up in QA environment." }772 ],773 mockData: {774 id: "bucket-raw-logs",775 name: "raw-logs",776 project_id: "proj-aurora",777 owners: ["ops@hola.cloud"],778 description: "Daily ingestion from InstantLogs streaming.",779 created_timestamp: 1688515200780 }781 }782 }783 ]784 }785 ]786 },787 {788 id: "service:inceptiondb",789 label: "InceptionDB",790 icon: "🧮",791 badge: "Service",792 type: "service",793 service: "InceptionDB",794 defaultExpanded: true,795 detail: {796 type: "service",797 nodeId: "service:inceptiondb",798 name: "InceptionDB (Document Database)",799 service: "InceptionDB",800 summary: "Document database with RPC-style collection operations.",801 description: "The spec exposes database CRUD plus RPC endpoints for collection inserts, queries, indexes and streaming writes.",802 metrics: { databases: 2, collections: 5, streamingInsert: true },803 insights: [804 "Collection RPCs like :find and :insert map directly to UI actions.",805 "Owners and API keys are first-class fields in getDatabaseResponse, simplifying access management.",806 "Streaming inserts (:insertStream, :insertFullduplex) influence how the console will expose ingestion tools."807 ],808 operations: [809 { method: "GET", path: "/v1/databases", description: "List databases." },810 { method: "POST", path: "/v1/databases", description: "Create database (createDatabaseRequest)." },811 { method: "GET", path: "/v1/databases/{databaseId}", description: "Fetch database metadata." },812 { method: "PATCH", path: "/v1/databases/{databaseId}", description: "Modify database properties." },813 { method: "DELETE", path: "/v1/databases/{databaseId}", description: "Drop database." },814 { method: "POST", path: "/v1/databases/{databaseId}/collections", description: "Create collection." },815 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:find", description: "Query documents." },816 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:insert", description: "Insert documents." },817 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:createIndex", description: "Create index." },818 { method: "POST", path: "/v1/databases/{databaseId}:createApiKey", description: "Manage database API keys." }819 ],820 schemas: [821 {822 name: "createDatabaseRequest",823 description: "Payload to provision a database.",824 fields: [825 { name: "name", type: "string", description: "Database name." }826 ]827 },828 {829 name: "getDatabaseResponse",830 description: "Database metadata including owners and API keys.",831 fields: [832 { name: "id", type: "string", description: "Database id." },833 { name: "name", type: "string", description: "Database name." },834 { name: "owners", type: "array<string>", description: "Database owners." },835 { name: "api_keys", type: "array<ApiKey>", description: "Credentials for integrations." }836 ]837 },838 {839 name: "CollectionResponse",840 description: "Collection metadata returned by RPC operations.",841 fields: [842 { name: "name", type: "string", description: "Collection name." },843 { name: "total", type: "number", description: "Document count." },844 { name: "indexes", type: "number", description: "Number of indexes." }845 ]846 }847 ]848 },849 children: [850 {851 id: "inception:db:main",852 label: "Database main",853 badge: "Database",854 icon: "🗄️",855 type: "resource",856 service: "InceptionDB",857 projects: ["proj-aurora"],858 detail: {859 type: "resource",860 service: "InceptionDB",861 resourceType: "Database",862 summary: "Aurora production database with product and user collections.",863 projects: ["proj-aurora"],864 metrics: { collections: 3, apiKeys: 2, owners: 2 },865 operations: [866 { method: "GET", path: "/v1/databases/{databaseId}", description: "Inspect database metadata." },867 { method: "POST", path: "/v1/databases/{databaseId}/collections", description: "Create a collection." },868 { method: "POST", path: "/v1/databases/{databaseId}:createApiKey", description: "Issue new API key." },869 { method: "POST", path: "/v1/databases/{databaseId}:deleteApiKey", description: "Revoke API key." },870 { method: "POST", path: "/v1/databases/{databaseId}:addOwner", description: "Add owner to database." },871 { method: "POST", path: "/v1/databases/{databaseId}:deleteOwner", description: "Remove owner." }872 ],873 mockData: {874 id: "db-main",875 name: "main",876 owners: ["aurora@hola.cloud", "ops@hola.cloud"],877 api_keys: [878 { name: "dashboard", key: "AKIA-aurora-dashboard", creation_date: "2022-09-12T14:21:00Z" },879 { name: "etl", key: "AKIA-aurora-etl", creation_date: "2023-02-04T09:45:00Z" }880 ],881 creation_date: "2021-11-01T12:00:00Z"882 }883 },884 children: [885 {886 id: "inception:db:main:collection:products",887 label: "Collection products",888 badge: "Collection",889 icon: "📦",890 type: "resource",891 service: "InceptionDB",892 projects: ["proj-aurora"],893 detail: {894 type: "resource",895 service: "InceptionDB",896 resourceType: "Collection",897 summary: "Product catalog documents for Aurora storefront.",898 projects: ["proj-aurora"],899 metrics: { documents: 3421, indexes: 3, defaults: "price=0" },900 operations: [901 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:find", description: "Query catalog items." },902 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:insert", description: "Insert new products." },903 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:patch", description: "Update documents." },904 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:remove", description: "Delete documents by filter." },905 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:createIndex", description: "Create compound index." },906 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:listIndexes", description: "List existing indexes." }907 ],908 mockData: {909 name: "products",910 defaults: { currency: "USD", status: "active" },911 indexes: ["sku_unique", "category_status"],912 total: 3421913 },914 notes: [915 "Streaming ingest available via :insertStream for bulk imports.",916 "Indexes derived from dropIndex/listIndexes endpoints in spec."917 ]918 }919 },920 {921 id: "inception:db:main:collection:users",922 label: "Collection users",923 badge: "Collection",924 icon: "👤",925 type: "resource",926 service: "InceptionDB",927 projects: ["proj-aurora"],928 detail: {929 type: "resource",930 service: "InceptionDB",931 resourceType: "Collection",932 summary: "Customer accounts, authentication state and preferences.",933 projects: ["proj-aurora"],934 metrics: { documents: 89432, indexes: 4, defaults: "locale=en-US" },935 operations: [936 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:find", description: "Query users by email." },937 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:setDefaults", description: "Configure default fields." },938 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:size", description: "Fetch collection size metrics." }939 ],940 mockData: {941 name: "users",942 defaults: { locale: "en-US", marketingOptIn: false },943 indexes: ["email_unique", "created_at"],944 total: 89432945 }946 }947 }948 ]949 },950 {951 id: "inception:db:analytics",952 label: "Database analytics",953 badge: "Database",954 icon: "📊",955 type: "resource",956 service: "InceptionDB",957 projects: ["proj-nebula"],958 detail: {959 type: "resource",960 service: "InceptionDB",961 resourceType: "Database",962 summary: "Nebula AI experiment metadata.",963 projects: ["proj-nebula"],964 metrics: { collections: 2, apiKeys: 1, owners: 1 },965 operations: [966 { method: "GET", path: "/v1/databases/{databaseId}", description: "Review database metadata." },967 { method: "POST", path: "/v1/databases/{databaseId}/collections", description: "Create experiment collection." }968 ],969 mockData: {970 id: "db-analytics",971 name: "analytics",972 owners: ["nebula@hola.cloud"],973 api_keys: [{ name: "ml-dashboard", key: "AKIA-nebula-dashboard", creation_date: "2022-05-10T09:00:00Z" }],974 creation_date: "2022-05-01T10:00:00Z"975 }976 },977 children: [978 {979 id: "inception:db:analytics:collection:events",980 label: "Collection events",981 badge: "Collection",982 icon: "📈",983 type: "resource",984 service: "InceptionDB",985 projects: ["proj-nebula"],986 detail: {987 type: "resource",988 service: "InceptionDB",989 resourceType: "Collection",990 summary: "Experiment telemetry events for ML runs.",991 projects: ["proj-nebula"],992 operations: [993 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:insertStream", description: "Stream events from trainers." },994 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:getIndex", description: "Inspect index definitions." }995 ],996 mockData: {997 name: "events",998 total: 540987,999 indexes: ["run_id", "timestamp"],1000 defaults: { run_status: "running" }1001 }1002 }1003 },1004 {1005 id: "inception:db:analytics:collection:models",1006 label: "Collection models",1007 badge: "Collection",1008 icon: "🧠",1009 type: "resource",1010 service: "InceptionDB",1011 projects: ["proj-nebula"],1012 detail: {1013 type: "resource",1014 service: "InceptionDB",1015 resourceType: "Collection",1016 summary: "Model registry metadata for Nebula AI.",1017 projects: ["proj-nebula"],1018 operations: [1019 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:insert", description: "Register new model version." },1020 { method: "POST", path: "/v1/databases/{databaseId}/collections/{collectionName}:dropIndex", description: "Drop stale index." }1021 ],1022 mockData: {1023 name: "models",1024 defaults: { stage: "staging" },1025 indexes: ["model_name_stage"],1026 total: 841027 }1028 }1029 }1030 ]1031 }1032 ]1033 },1034 {1035 id: "service:config",1036 label: "Configuration",1037 icon: "⚙️",1038 badge: "Service",1039 type: "service",1040 service: "Configuration",1041 defaultExpanded: true,1042 detail: {1043 type: "service",1044 nodeId: "service:config",1045 name: "Configuration Service",1046 service: "Configuration",1047 summary: "Centralized configuration store using Thing and PatchThingInput schemas.",1048 description: "Provides collection-style /v0/configs endpoints plus /v1/config for consolidated runtime snapshots.",1049 metrics: { configs: 5, globalSnapshot: true },1050 insights: [1051 "Entries field is flexible JSON, so the console renders rich editors.",1052 "PATCH endpoint reuses PatchThingInput, enabling partial updates without clobbering entries.",1053 "/v1/config returns merged configuration for runtime diagnostics."1054 ],1055 operations: [1056 { method: "GET", path: "/v0/configs", description: "List configs." },1057 { method: "POST", path: "/v0/configs", description: "Create config." },1058 { method: "GET", path: "/v0/configs/{configId}", description: "Fetch config entries." },1059 { method: "PATCH", path: "/v0/configs/{configId}", description: "Partial update using PatchThingInput." },1060 { method: "DELETE", path: "/v0/configs/{configId}", description: "Delete config." },1061 { method: "GET", path: "/v1/config", description: "Fetch runtime aggregated config." }1062 ],1063 schemas: [1064 {1065 name: "Thing",1066 description: "Configuration document with entries map.",1067 fields: [1068 { name: "id", type: "string", description: "Config identifier." },1069 { name: "entries", type: "object", description: "Arbitrary configuration key-value pairs." }1070 ]1071 },1072 {1073 name: "PatchThingInput",1074 description: "Fields accepted by PATCH operations.",1075 fields: [1076 { name: "name", type: "string", description: "Optional display name." },1077 { name: "description", type: "string", description: "Documentation string." }1078 ]1079 }1080 ]1081 },1082 children: [1083 {1084 id: "config:core-platform",1085 label: "Config core-platform",1086 badge: "Config",1087 icon: "🧩",1088 type: "resource",1089 service: "Configuration",1090 projects: ["proj-aurora"],1091 detail: {1092 type: "resource",1093 service: "Configuration",1094 resourceType: "Thing",1095 summary: "Platform wide settings (feature toggles, endpoints).",1096 projects: ["proj-aurora"],1097 operations: [1098 { method: "GET", path: "/v0/configs/{configId}", description: "View config entries." },1099 { method: "PATCH", path: "/v0/configs/{configId}", description: "Update specific fields." },1100 { method: "DELETE", path: "/v0/configs/{configId}", description: "Delete config if deprecated." }1101 ],1102 mockData: {1103 id: "core-platform",1104 entries: {1105 searchEndpoint: "https://aurora.hola.cloud/search",1106 enableRecommendations: true,1107 fallbackLocale: "en"1108 }1109 },1110 notes: ["Entries map mirrors Thing schema from the spec."]1111 }1112 },1113 {1114 id: "config:feature-flags",1115 label: "Config feature-flags",1116 badge: "Config",1117 icon: "🚦",1118 type: "resource",1119 service: "Configuration",1120 projects: ["proj-aurora", "proj-nebula"],1121 detail: {1122 type: "resource",1123 service: "Configuration",1124 resourceType: "Thing",1125 summary: "Shared feature toggles between Aurora and Nebula.",1126 projects: ["proj-aurora", "proj-nebula"],1127 operations: [1128 { method: "GET", path: "/v0/configs/{configId}", description: "View toggles." },1129 { method: "PATCH", path: "/v0/configs/{configId}", description: "Update toggles using PatchThingInput." }1130 ],1131 mockData: {1132 id: "feature-flags",1133 entries: {1134 enableAIBundles: false,1135 enableRealTimeLogs: true,1136 experiments: ["guided-search", "cart-boost"]1137 }1138 }1139 }1140 },1141 {1142 id: "config:sandbox",1143 label: "Config sandbox",1144 badge: "Config",1145 icon: "🧪",1146 type: "resource",1147 service: "Configuration",1148 projects: ["proj-sandbox"],1149 detail: {1150 type: "resource",1151 service: "Configuration",1152 resourceType: "Thing",1153 summary: "QA specific overrides for Sandbox environment.",1154 projects: ["proj-sandbox"],1155 operations: [1156 { method: "GET", path: "/v0/configs/{configId}", description: "Review QA configuration." },1157 { method: "DELETE", path: "/v0/configs/{configId}", description: "Reset sandbox config." }1158 ],1159 mockData: {1160 id: "sandbox",1161 entries: {1162 paymentGateway: "staging",1163 enableSmokeTests: true1164 }1165 }1166 }1167 }1168 ]1169 },1170 {1171 id: "service:lambdas",1172 label: "Lambdas",1173 icon: "λ",1174 badge: "Service",1175 type: "service",1176 service: "Lambdas",1177 defaultExpanded: true,1178 detail: {1179 type: "service",1180 nodeId: "service:lambdas",1181 name: "Serverless Lambdas",1182 service: "Lambdas",1183 summary: "Serverless functions managed by the Lambda API.",1184 description: "Expose CRUD for functions plus execution endpoints (/api/v0/run/{lambda_id}) and tenant aware mux routes.",1185 metrics: { lambdas: 5, runtimes: ["python", "node"].join(', ') },1186 insights: [1187 "Path property from Lambda schema drives tree navigation by HTTP path segments.",1188 "/mux/{owner_id}/* indicates multi-tenant routing in the runtime layer.",1189 "Run endpoints allow direct invocation from the console without deploying clients."1190 ],1191 operations: [1192 { method: "GET", path: "/api/v0/lambdas", description: "List deployed lambdas." },1193 { method: "POST", path: "/api/v0/lambdas", description: "Create lambda (CreateLambdaReq)." },1194 { method: "GET", path: "/api/v0/lambdas/{lambda_id}", description: "Get lambda metadata." },1195 { method: "PATCH", path: "/api/v0/lambdas/{lambda_id}", description: "Modify lambda definition." },1196 { method: "DELETE", path: "/api/v0/lambdas/{lambda_id}", description: "Delete lambda." },1197 { method: "POST", path: "/api/v0/run/{lambda_id}", description: "Invoke lambda via management API." },1198 { method: "POST", path: "/run/{lambda_id}", description: "Public invocation endpoint." },1199 { method: "GET", path: "/mux/{owner_id}/*", description: "Mux routing entry point." }1200 ],1201 schemas: [1202 {1203 name: "Lambda",1204 description: "Lambda definition from spec.",1205 fields: [1206 { name: "id", type: "string", description: "Lambda id." },1207 { name: "name", type: "string", description: "Display name." },1208 { name: "path", type: "string", description: "HTTP path served by lambda." },1209 { name: "method", type: "string", description: "HTTP method." },1210 { name: "language", type: "string", description: "Runtime language." },1211 { name: "code", type: "string", description: "Source code." }1212 ]1213 },1214 {1215 name: "CreateLambdaReq",1216 description: "Fields required to create lambda.",1217 fields: [1218 { name: "name", type: "string", description: "Lambda name." },1219 { name: "path", type: "string", description: "Routing path." },1220 { name: "method", type: "string", description: "HTTP method." }1221 ]1222 },1223 {1224 name: "ModifyLambdaRequest",1225 description: "Partial fields for updates.",1226 fields: [1227 { name: "code", type: "string", description: "Updated source code." },1228 { name: "language", type: "string", description: "Runtime language." }1229 ]1230 }1231 ]1232 },1233 children: [1234 {1235 id: "lambda:path-root",1236 label: "/",1237 badge: "Path",1238 icon: "🌐",1239 type: "group",1240 service: "Lambdas",1241 detail: {1242 type: "group",1243 service: "Lambdas",1244 summary: "Path tree derived from Lambda.path values.",1245 operations: [1246 { method: "GET", path: "/api/v0/lambdas", description: "List and filter by path prefix." }1247 ]1248 },1249 children: [1250 {1251 id: "lambda:path-api",1252 label: "api",1253 badge: "Path",1254 type: "group",1255 service: "Lambdas",1256 children: [1257 {1258 id: "lambda:path-api-animals",1259 label: "animals",1260 badge: "Path",1261 type: "group",1262 service: "Lambdas",1263 children: [1264 {1265 id: "lambda:path-api-animals-param",1266 label: "{animal_id}",1267 badge: "Path",1268 type: "group",1269 service: "Lambdas",1270 children: [1271 {1272 id: "lambda:history",1273 label: "history",1274 badge: "Lambda",1275 icon: "📜",1276 type: "resource",1277 service: "Lambdas",1278 projects: ["proj-aurora"],1279 detail: {1280 type: "resource",1281 service: "Lambdas",1282 resourceType: "Lambda",1283 summary: "Returns historical sightings for a specific animal id.",1284 projects: ["proj-aurora"],1285 metrics: { method: "GET", language: "python3.11", latencyMsP99: 180 },1286 operations: [1287 { method: "GET", path: "/api/v0/lambdas/{lambda_id}", description: "Get lambda metadata." },1288 { method: "PATCH", path: "/api/v0/lambdas/{lambda_id}", description: "Update lambda code or config." },1289 { method: "DELETE", path: "/api/v0/lambdas/{lambda_id}", description: "Delete lambda if deprecated." },1290 { method: "POST", path: "/api/v0/run/{lambda_id}", description: "Execute lambda on demand." }1291 ],1292 mockData: {1293 id: "lambda-history",1294 name: "animal-history",1295 path: "/api/animals/{animal_id}/history",1296 method: "GET",1297 language: "python",1298 owner: "aurora@hola.cloud",1299 project_id: "proj-aurora",1300 code: "def handler(event, ctx)",1301 return :"{'history': []}",1302 created_timestamp: 16798752001303 },1304 notes: ["Path hierarchy matches console navigation from legacy UI screenshot."]1305 }1306 }1307 ]1308 }1309 ]1310 },1311 {1312 id: "lambda:path-api-files",1313 label: "files",1314 badge: "Path",1315 type: "group",1316 service: "Lambdas",1317 children: [1318 {1319 id: "lambda:path-api-files-ingest",1320 label: "ingest",1321 badge: "Lambda",1322 icon: "⬆️",1323 type: "resource",1324 service: "Lambdas",1325 projects: ["proj-nebula"],1326 detail: {1327 type: "resource",1328 service: "Lambdas",1329 resourceType: "Lambda",1330 summary: "Receives dataset uploads and forwards to ml-datasets bucket.",1331 projects: ["proj-nebula"],1332 metrics: { method: "POST", language: "node18", invocations24h: 128 },1333 operations: [1334 { method: "GET", path: "/api/v0/lambdas/{lambda_id}", description: "Get lambda metadata." },1335 { method: "PATCH", path: "/api/v0/lambdas/{lambda_id}", description: "Deploy new version." },1336 { method: "POST", path: "/api/v0/run/{lambda_id}", description: "Test ingest with sample payload." }1337 ],1338 mockData: {1339 id: "lambda-ingest",1340 name: "files-ingest",1341 path: "/api/files/ingest",1342 method: "POST",1343 language: "node",1344 owner: "nebula@hola.cloud",1345 project_id: "proj-nebula",1346 code: "exports.handler = async (event) => ({ status: 'queued' });",1347 created_timestamp: 16809120001348 }1349 }1350 }1351 ]1352 }1353 ]1354 }1355 ]1356 },1357 {1358 id: "lambda:management",1359 label: "Management API",1360 badge: "Endpoints",1361 icon: "🛠️",1362 type: "resource",1363 service: "Lambdas",1364 detail: {1365 type: "resource",1366 service: "Lambdas",1367 resourceType: "Documentation",1368 summary: "Summary of management endpoints defined in the Lambda spec.",1369 operations: [1370 { method: "GET", path: "/api/v0/lambdas", description: "List functions." },1371 { method: "POST", path: "/api/v0/lambdas", description: "Create lambda." },1372 { method: "POST", path: "/api/v0/run/{lambda_id}", description: "Invoke lambda." },1373 { method: "GET", path: "/mux/{owner_id}/*", description: "Mux runtime entry." }1374 ]1375 }1376 }1377 ]1378 },1379 {1380 id: "service:loggers",1381 label: "Loggers",1382 icon: "🪵",1383 badge: "Service",1384 type: "service",1385 service: "InstantLogs",1386 defaultExpanded: true,1387 detail: {1388 type: "service",1389 nodeId: "service:loggers",1390 name: "Instant Logs",1391 service: "InstantLogs",1392 summary: "Real-time logging service with ingest, filtering and API key management.",1393 description: "OpenAPI paths cover logger CRUD, API key issuance, filter creation and ingest endpoints.",1394 metrics: { loggers: 2, ingestEndpoints: 2, filters: 3 },1395 insights: [1396 "LoggerEntry schema surfaces usage metrics (bytes, ingests) for dashboards.",1397 "Ownership is managed via /v1/loggers/{loggerId}/owners endpoints.",1398 "Stats endpoint (/v1/loggers/{loggerId}/stats) will back the console analytics charts."1399 ],1400 operations: [1401 { method: "POST", path: "/v1/loggers", description: "Create logger (createLoggerInput)." },1402 { method: "GET", path: "/v1/loggers/{loggerId}", description: "Get logger metadata." },1403 { method: "DELETE", path: "/v1/loggers/{loggerId}", description: "Delete logger." },1404 { method: "POST", path: "/v1/loggers/{loggerId}/apiKeys", description: "Create API key." },1405 { method: "DELETE", path: "/v1/loggers/{loggerId}/apiKeys/{apiKey}", description: "Delete API key." },1406 { method: "POST", path: "/v1/loggers/{loggerId}/filter", description: "Create log filter." },1407 { method: "POST", path: "/v1/loggers/{loggerId}/ingest", description: "Ingest log entry." },1408 { method: "POST", path: "/v1/loggers/{loggerId}/owners", description: "Add owner." },1409 { method: "DELETE", path: "/v1/loggers/{loggerId}/owners/{ownerId}", description: "Remove owner." },1410 { method: "GET", path: "/v1/loggers/{loggerId}/stats", description: "Retrieve usage statistics." }1411 ],1412 schemas: [1413 {1414 name: "LoggerEntry",1415 description: "Logger definition including usage metrics.",1416 fields: [1417 { name: "id", type: "string", description: "Logger id." },1418 { name: "name", type: "string", description: "Logger name." },1419 { name: "owners", type: "array<string>", description: "Logger owners." },1420 { name: "api_keys", type: "array<ApiKey>", description: "API keys." },1421 { name: "usage", type: "LoggerUsage", description: "Aggregated usage counters." }1422 ]1423 },1424 {1425 name: "ApiKey",1426 description: "API key record for logger access.",1427 fields: [1428 { name: "name", type: "string", description: "Key display name." },1429 { name: "key", type: "string", description: "Public identifier." },1430 { name: "creation_date", type: "string", description: "ISO creation date." }1431 ]1432 },1433 {1434 name: "LoggerUsage",1435 description: "Usage counters returned by stats endpoint.",1436 fields: [1437 { name: "bytes_received", type: "number", description: "Bytes ingested." },1438 { name: "bytes_sent", type: "number", description: "Bytes returned to clients." },1439 { name: "bytes_filtered", type: "number", description: "Bytes after filters." },1440 { name: "num_ingests", type: "number", description: "Ingest operations." },1441 { name: "num_filters", type: "number", description: "Active filters." }1442 ]1443 }1444 ]1445 },1446 children: [1447 {1448 id: "logger:main",1449 label: "Main logger",1450 badge: "Logger",1451 icon: "📑",1452 type: "resource",1453 service: "InstantLogs",1454 projects: ["proj-aurora"],1455 detail: {1456 type: "resource",1457 service: "InstantLogs",1458 resourceType: "Logger",1459 summary: "Primary production logger capturing API gateway and backend traces.",1460 projects: ["proj-aurora"],1461 metrics: { apiKeys: 2, owners: 2, filters: 2 },1462 operations: [1463 { method: "GET", path: "/v1/loggers/{loggerId}", description: "Get logger metadata." },1464 { method: "POST", path: "/v1/loggers/{loggerId}/stats", description: "(spec) Stats retrieval uses GET." },1465 { method: "POST", path: "/v1/loggers/{loggerId}/ingest", description: "Ingest a log line." }1466 ],1467 mockData: {1468 id: "logger-main",1469 name: "main",1470 owners: ["aurora@hola.cloud", "ops@hola.cloud"],1471 usage: {1472 bytes_received: 982304923,1473 bytes_sent: 4323402,1474 bytes_filtered: 30400432,1475 num_ingests: 53492,1476 num_filters: 31477 }1478 }1479 },1480 children: [1481 {1482 id: "logger:main:api-keys",1483 label: "API Keys",1484 badge: "Access",1485 icon: "🔑",1486 type: "resource",1487 service: "InstantLogs",1488 projects: ["proj-aurora"],1489 detail: {1490 type: "resource",1491 service: "InstantLogs",1492 resourceType: "API Keys",1493 summary: "Manage API keys via /v1/loggers/{loggerId}/apiKeys endpoints.",1494 projects: ["proj-aurora"],1495 operations: [1496 { method: "POST", path: "/v1/loggers/{loggerId}/apiKeys", description: "Issue key (createApiKeyInput)." },1497 { method: "DELETE", path: "/v1/loggers/{loggerId}/apiKeys/{apiKey}", description: "Revoke key." }1498 ],1499 mockData: {1500 keys: [1501 { name: "grafana", key: "LOG-aurora-grafana", creation_date: "2023-03-01T08:00:00Z" },1502 { name: "etl", key: "LOG-aurora-etl", creation_date: "2023-05-19T12:30:00Z" }1503 ]1504 }1505 }1506 },1507 {1508 id: "logger:main:filters",1509 label: "Filters",1510 badge: "Filtering",1511 icon: "🎚️",1512 type: "resource",1513 service: "InstantLogs",1514 projects: ["proj-aurora"],1515 detail: {1516 type: "resource",1517 service: "InstantLogs",1518 resourceType: "Filter",1519 summary: "Saved queries created via /v1/loggers/{loggerId}/filter.",1520 projects: ["proj-aurora"],1521 operations: [1522 { method: "POST", path: "/v1/loggers/{loggerId}/filter", description: "Create filter definition." }1523 ],1524 mockData: {1525 filters: [1526 { name: "errors", query: "severity>=ERROR" },1527 { name: "cart", query: 'path:"/cart"' }1528 ]1529 }1530 }1531 },1532 {1533 id: "logger:main:owners",1534 label: "Owners",1535 badge: "Access",1536 icon: "👥",1537 type: "resource",1538 service: "InstantLogs",1539 projects: ["proj-aurora"],1540 detail: {1541 type: "resource",1542 service: "InstantLogs",1543 resourceType: "Ownership",1544 summary: "Collaborator management using /owners endpoints.",1545 projects: ["proj-aurora"],1546 operations: [1547 { method: "POST", path: "/v1/loggers/{loggerId}/owners", description: "Add owner." },1548 { method: "DELETE", path: "/v1/loggers/{loggerId}/owners/{ownerId}", description: "Remove owner." }1549 ],1550 mockData: {1551 owners: ["aurora@hola.cloud", "ops@hola.cloud"]1552 }1553 }1554 }1555 ]1556 },1557 {1558 id: "logger:secondary",1559 label: "Secondary logger",1560 badge: "Logger",1561 icon: "📋",1562 type: "resource",1563 service: "InstantLogs",1564 projects: ["proj-nebula"],1565 detail: {1566 type: "resource",1567 service: "InstantLogs",1568 resourceType: "Logger",1569 summary: "Nebula experiment logger for ML training output.",1570 projects: ["proj-nebula"],1571 metrics: { apiKeys: 1, owners: 1, filters: 1 },1572 operations: [1573 { method: "GET", path: "/v1/loggers/{loggerId}", description: "Inspect logger." },1574 { method: "POST", path: "/v1/loggers/{loggerId}/ingest", description: "Ingest training logs." }1575 ],1576 mockData: {1577 id: "logger-nebula",1578 name: "nebula-traces",1579 owners: ["nebula@hola.cloud"],1580 usage: {1581 bytes_received: 24300943,1582 bytes_sent: 503244,1583 bytes_filtered: 100324,1584 num_ingests: 1243,1585 num_filters: 11586 }1587 }1588 }1589 }1590 ]1591 },1592 {1593 id: "service:queues",1594 label: "Queues",1595 icon: "🛰️",1596 badge: "Service",1597 type: "service",1598 service: "Tailon",1599 defaultExpanded: true,1600 detail: {1601 type: "service",1602 nodeId: "service:queues",1603 name: "Tailon Queues",1604 service: "Tailon",1605 summary: "Streaming queue service described by Tailon OpenAPI spec.",1606 description: "Supports queue CRUD plus read/write RPC endpoints exposed as /v1/queues/{queue_id}:read and :write.",1607 metrics: { queues: 2, clients: 3 },1608 insights: [1609 "CreateQueueInput schema matches minimal payload (just name) simplifying UI form.",1610 "Read/Write RPC style endpoints fit into console action buttons for streaming connectors.",1611 "/v1/clients endpoint (spec) will populate consumer diagnostics in future iterations."1612 ],1613 operations: [1614 { method: "GET", path: "/v1/queues", description: "List queues." },1615 { method: "POST", path: "/v1/queues", description: "Create queue (CreateQueueInput)." },1616 { method: "GET", path: "/v1/queues/{queue_id}", description: "Fetch queue metadata." },1617 { method: "DELETE", path: "/v1/queues/{queue_id}", description: "Delete queue." },1618 { method: "POST", path: "/v1/queues/{queue_id}:write", description: "Write messages to queue." },1619 { method: "POST", path: "/v1/queues/{queue_id}:read", description: "Consume messages from queue." }1620 ],1621 schemas: [1622 {1623 name: "CreateQueueInput",1624 description: "Payload to create queue.",1625 fields: [1626 { name: "name", type: "string", description: "Queue name." }1627 ]1628 },1629 {1630 name: "GlueAuthentication",1631 description: "Authentication envelope referenced by Tailon spec.",1632 fields: [1633 { name: "user", type: "object", description: "User payload (implementation defined)." },1634 { name: "session", type: "object", description: "Session information." }1635 ]1636 }1637 ]1638 },1639 children: [1640 {1641 id: "queue:realtime-audit",1642 label: "Queue realtime-audit",1643 badge: "Queue",1644 icon: "📡",1645 type: "resource",1646 service: "Tailon",1647 projects: ["proj-aurora"],1648 detail: {1649 type: "resource",1650 service: "Tailon",1651 resourceType: "Queue",1652 summary: "Feeds InstantLogs with high priority audit events.",1653 projects: ["proj-aurora"],1654 metrics: { producers: 2, consumers: 1, depth: "approx 120" },1655 operations: [1656 { method: "GET", path: "/v1/queues/{queue_id}", description: "Inspect queue metadata." },1657 { method: "POST", path: "/v1/queues/{queue_id}:write", description: "Write audit event." },1658 { method: "POST", path: "/v1/queues/{queue_id}:read", description: "Consume events." }1659 ],1660 mockData: {1661 id: "queue-realtime-audit",1662 name: "realtime-audit",1663 created: "2023-01-10T11:00:00Z",1664 retentionSeconds: 36001665 }1666 }1667 },1668 {1669 id: "queue:metrics-stream",1670 label: "Queue metrics-stream",1671 badge: "Queue",1672 icon: "📈",1673 type: "resource",1674 service: "Tailon",1675 projects: ["proj-nebula", "proj-sandbox"],1676 detail: {1677 type: "resource",1678 service: "Tailon",1679 resourceType: "Queue",1680 summary: "Streams ML training metrics to dashboards and sandbox.",1681 projects: ["proj-nebula", "proj-sandbox"],1682 metrics: { producers: 3, consumers: 2, depth: "approx 45" },1683 operations: [1684 { method: "GET", path: "/v1/queues/{queue_id}", description: "Inspect queue metadata." },1685 { method: "POST", path: "/v1/queues/{queue_id}:write", description: "Stream metric message." }1686 ],1687 mockData: {1688 id: "queue-metrics-stream",1689 name: "metrics-stream",1690 created: "2022-08-05T09:30:00Z",1691 retentionSeconds: 9001692 }1693 }1694 }1695 ]1696 }1697 ];1698 1699 const telemetryStream = [1700 { time: "10:24:18", text: "Fetched /v0/projects to seed mock project list" },1701 { time: "10:24:22", text: "Inspected Files spec (/v1/buckets, /v1/buckets/{bucket_id})" },1702 { time: "10:24:25", text: "Parsed InceptionDB collections RPC endpoints for UI actions" },1703 { time: "10:24:29", text: "Mapped Lambda run endpoints (/api/v0/run/{lambda_id}) to tabs" },1704 { time: "10:24:34", text: "Logged InstantLogs API keys operations for logger nodes" },1705 { time: "10:24:37", text: "Queued Tailon stream endpoints (/v1/queues/{queue_id}:read)" }1706 ];1707 1708 createApp({1709 data() {1710 return {1711 projects: projectCatalog,1712 selectedProjectId: projectCatalog[0]?.id || null,1713 tree: serviceTree,1714 tabs: [1715 {1716 id: 'projects',1717 nodeId: 'projects',1718 label: 'Projects',1719 icon: '🗂️',1720 type: 'project-overview',1721 detail: {}1722 }1723 ],1724 activeTabId: 'projects',1725 resourceSearch: '',1726 expandedNodes: {},1727 projectForm: { name: '', owners: '', routerHost: '' },1728 projectSpec: {1729 operations: [1730 { method: 'GET', path: '/v0/projects', description: 'List projects' },1731 { method: 'POST', path: '/v0/projects', description: 'Create project' },1732 { method: 'GET', path: '/v0/projects/{project_id}', description: 'Retrieve project' },1733 { method: 'DELETE', path: '/v0/projects/{project_id}', description: 'Delete project' }1734 ],1735 schemas: [1736 {1737 name: 'Project',1738 fields: [1739 { name: 'id', type: 'string', description: 'Project id' },1740 { name: 'name', type: 'string', description: 'Project name' },1741 { name: 'owners', type: 'array<string>', description: 'Project owners' },1742 { name: 'auth.enabled', type: 'boolean', description: 'Auth toggle' }1743 ]1744 },1745 {1746 name: 'ProjectCreateInput',1747 fields: [1748 { name: 'name', type: 'string', description: 'Desired name' }1749 ]1750 },1751 {1752 name: 'ProjectRouter',1753 fields: [1754 { name: 'type', type: 'string', description: 'Router type' },1755 { name: 'hosts', type: 'array<Host>', description: 'Domains exposed' }1756 ]1757 }1758 ]1759 },1760 telemetry: telemetryStream1761 };1762 },1763 computed: {1764 selectedProject() {1765 return this.projects.find(project => project.id === this.selectedProjectId) || null;1766 },1767 activeTab() {1768 return this.tabs.find(tab => tab.id === this.activeTabId) || null;1769 },1770 filteredTree() {1771 return this.filterTree(this.tree, this.resourceSearch);1772 },1773 activeProjectResources() {1774 if (!this.selectedProjectId) return [];1775 return this.collectProjectResources(this.selectedProjectId);1776 },1777 serviceCards() {1778 return this.tree1779 .filter(node => node.type === 'service')1780 .map(node => ({1781 id: node.id,1782 name: node.label,1783 summary: node.detail?.summary || '',1784 operations: node.detail?.operations || []1785 }));1786 }1787 },1788 created() {1789 this.initializeExpansion();1790 },1791 methods: {1792 initializeExpansion() {1793 const expand = node => {1794 if (node.defaultExpanded || node.type === 'service') {1795 this.expandedNodes[node.id] = true;1796 }1797 if (Array.isArray(node.children)) {1798 node.children.forEach(expand);1799 }1800 };1801 this.tree.forEach(expand);1802 },1803 openProjectManager() {1804 this.activateTab('projects');1805 },1806 toggleNode(node) {1807 if (!node.children || !node.children.length) return;1808 const realNode = this.findNodeById(node.id) || node;1809 this.expandedNodes[realNode.id] = !this.expandedNodes[realNode.id];1810 },1811 openNode(node) {1812 const realNode = this.findNodeById(node.id) || node;1813 if (!realNode.detail && realNode.children) {1814 this.toggleNode(realNode);1815 return;1816 }1817 const detail = realNode.detail ? { ...realNode.detail } : { type: 'resource', summary: realNode.label };1818 if (realNode.projects) {1819 detail.projects = [...realNode.projects];1820 }1821 if (!detail.service && realNode.service) {1822 detail.service = realNode.service;1823 }1824 let tab = this.tabs.find(tab => tab.id === realNode.id);1825 if (!tab) {1826 tab = {1827 id: realNode.id,1828 nodeId: realNode.id,1829 label: realNode.label,1830 icon: realNode.icon || '',1831 type: detail.type || 'resource',1832 detail1833 };1834 this.tabs.push(tab);1835 } else {1836 tab.label = realNode.label;1837 tab.icon = realNode.icon || tab.icon;1838 tab.type = detail.type || tab.type;1839 tab.detail = detail;1840 }1841 this.activeTabId = tab.id;1842 },1843 openResourceFromList(nodeId) {1844 const node = this.findNodeById(nodeId);1845 if (node) {1846 this.openNode(node);1847 }1848 },1849 openServiceById(serviceId) {1850 const node = this.findNodeById(serviceId);1851 if (node) {1852 this.openNode(node);1853 }1854 },1855 findNodeById(nodeId) {1856 let found = null;1857 const search = nodes => {1858 for (const node of nodes) {1859 if (node.id === nodeId) {1860 found = node;1861 return;1862 }1863 if (node.children) {1864 search(node.children);1865 if (found) return;1866 }1867 }1868 };1869 search(this.tree);1870 return found;1871 },1872 isNodeExpanded(node) {1873 return !!this.expandedNodes[node.id];1874 },1875 isNodeDimmed(node) {1876 if (!node.projects || !node.projects.length) return false;1877 if (!this.selectedProjectId) return false;1878 return !node.projects.includes(this.selectedProjectId);1879 },1880 activateTab(tabId) {1881 this.activeTabId = tabId;1882 },1883 closeTab(tabId) {1884 if (tabId === 'projects') return;1885 const index = this.tabs.findIndex(tab => tab.id === tabId);1886 if (index >= 0) {1887 this.tabs.splice(index, 1);1888 if (this.activeTabId === tabId) {1889 const fallback = this.tabs[index] || this.tabs[index - 1] || this.tabs[0];1890 this.activeTabId = fallback ? fallback.id : null;1891 }1892 }1893 },1894 createProject() {1895 if (!this.projectForm.name.trim()) {1896 return;1897 }1898 const timestamp = Math.floor(Date.now() / 1000);1899 const slug = this.projectForm.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-');1900 const owners = this.projectForm.owners1901 .split(',')1902 .map(value => value.trim())1903 .filter(Boolean);1904 const routerHost = this.projectForm.routerHost.trim();1905 const newProject = {1906 id: `proj-${slug}-${timestamp}`,1907 name: this.projectForm.name.trim(),1908 description: 'New project created from mock form.',1909 create_timestamp: timestamp,1910 update_timestamp: timestamp,1911 deleted: false,1912 owners: owners.length ? owners : ['ops@hola.cloud'],1913 auth: { enabled: true },1914 routers: routerHost1915 ? [1916 {1917 type: 'edge',1918 hosts: [1919 { name: routerHost, creation_timestamp: timestamp, verified: false }1920 ],1921 HeadersIn: { 'x-forwarded-host': routerHost },1922 HeadersOut: { 'cache-control': 'public, max-age=60' },1923 config: { rate_limit: '60 rps' }1924 }1925 ]1926 : [],1927 metrics: { buckets: 0, databases: 0, collections: 0, lambdas: 0, loggers: 0, queues: 0, configs: 0 }1928 };1929 this.projects.push(newProject);1930 this.projectForm = { name: '', owners: '', routerHost: '' };1931 this.selectedProjectId = newProject.id;1932 this.activateTab('projects');1933 },1934 methodBadgeClass(method) {1935 const palette = {1936 GET: 'bg-emerald-500/10 border-emerald-500/40 text-emerald-300',1937 POST: 'bg-sky-500/10 border-sky-500/40 text-sky-300',1938 PUT: 'bg-indigo-500/10 border-indigo-500/40 text-indigo-300',1939 PATCH: 'bg-violet-500/10 border-violet-500/40 text-violet-300',1940 DELETE: 'bg-rose-500/10 border-rose-500/40 text-rose-300',1941 HEAD: 'bg-slate-500/20 border-slate-500/40 text-slate-200'1942 };1943 return palette[method?.toUpperCase()] || 'bg-slate-800/50 border-slate-700/60 text-slate-300';1944 },1945 formatTimestamp(value) {1946 if (!value) return '—';1947 if (typeof value === 'string') {1948 return value.split('T')[0];1949 }1950 const date = new Date(value * 1000);1951 if (Number.isNaN(date.getTime())) return '—';1952 return date.toISOString().split('T')[0];1953 },1954 displayKeyValue(value) {1955 if (value == null) return '—';1956 if (Array.isArray(value)) {1957 return value.join(', ');1958 }1959 if (typeof value === 'object') {1960 return Object.entries(value)1961 .map(([key, val]) => `${key}: ${typeof val === 'object' ? JSON.stringify(val) : val}`)1962 .join(' · ');1963 }1964 return String(value);1965 },1966 formatJSON(data) {1967 return JSON.stringify(data, null, 2);1968 },1969 filterTree(nodes, query) {1970 if (!query || !query.trim()) {1971 return nodes;1972 }1973 const q = query.toLowerCase();1974 const walk = list => {1975 const result = [];1976 for (const node of list) {1977 const labelMatch = node.label.toLowerCase().includes(q);1978 const summaryMatch = node.detail?.summary?.toLowerCase().includes(q);1979 const childMatches = node.children ? walk(node.children) : [];1980 if (labelMatch || summaryMatch || childMatches.length) {1981 result.push({ ...node, children: childMatches });1982 }1983 }1984 return result;1985 };1986 return walk(nodes);1987 },1988 collectProjectResources(projectId) {1989 const collected = [];1990 const traverse = node => {1991 if (node.projects && node.projects.includes(projectId) && node.detail && node.detail.type !== 'service') {1992 collected.push({1993 id: node.id,1994 label: node.label,1995 type: node.detail.resourceType || node.badge || 'Resource',1996 service: node.service || (node.detail?.service || 'Service')1997 });1998 }1999 if (node.children) {2000 node.children.forEach(traverse);2001 }2002 };2003 this.tree.forEach(traverse);2004 return collected;2005 }2006 }2007 }).component('tree-node', {2008 props: ['node', 'depth', 'toggle', 'open', 'isExpanded', 'activeId', 'isDimmed'],2009 computed: {2010 hasChildren() {2011 return Array.isArray(this.node.children) && this.node.children.length > 0;2012 },2013 dimmed() {2014 return this.isDimmed ? this.isDimmed(this.node) : false;2015 }2016 },2017 template: '#tree-node-template'2018 }).mount('#app');2019 </script>2020 </body>2021 </html>2022 Enlace
El enlace para compartir es:

