Gracias, Wikipedia.
ShareCode
Permalink: http://www.treeweb.es/u/974/ 01/02/2011

ShareCode

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: