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 — Mock</title>7 <script src="https://cdn.tailwindcss.com"></script>8 <script>9 tailwind.config = {10 darkMode: "class",11 theme: {12 extend: {13 colors: {14 brand: {15 DEFAULT: "#2fe6a9",16 100: "#dcfce7",17 200: "#bbf7d0",18 300: "#86efac",19 400: "#4ade80",20 500: "#22c55e",21 600: "#16a34a",22 },23 },24 fontFamily: {25 display: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"],26 },27 },28 },29 };30 </script>31 </head>32 <body class="bg-slate-950 text-slate-100 font-display">33 <div id="app" class="min-h-screen flex">34 <aside class="w-72 bg-slate-950 border-r border-slate-900/70 flex flex-col">35 <div class="p-6 flex items-center gap-3 border-b border-slate-900/70">36 <div class="w-10 h-10 rounded-lg bg-brand-500/10 border border-brand-500/30 flex items-center justify-center text-2xl">🌐</div>37 <div>38 <div class="text-lg font-semibold tracking-tight">hola.cloud</div>39 <div class="text-xs uppercase text-slate-400">Unified console</div>40 </div>41 </div>42 <div class="px-6 py-4">43 <label class="text-xs uppercase tracking-widest text-slate-500 block mb-2">Project</label>44 <select45 v-model="selectedProjectId"46 class="w-full bg-slate-900 border border-slate-800 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"47 >48 <option v-for="project in projects" :key="project.id" :value="project.id">{{ project.name }}</option>49 </select>50 </div>51 <div class="px-6 pb-2 text-xs uppercase tracking-widest text-slate-500">Services</div>52 <nav class="flex-1 overflow-y-auto px-2 pb-6 space-y-1">53 <button54 v-for="service in services"55 :key="service.id"56 @click="activeService = service.id"57 class="w-full flex items-center gap-3 px-4 py-3 rounded-lg transition border border-transparent"58 :class="activeService === service.id ? 'bg-slate-900/80 border-brand-500/40 shadow-inner shadow-brand-500/10' : 'hover:bg-slate-900/40 text-slate-300'"59 >60 <span class="text-xl leading-none">{{ service.icon }}</span>61 <div class="text-left">62 <div class="text-sm font-medium">{{ service.name }}</div>63 <div class="text-[0.7rem] uppercase tracking-widest text-slate-500">{{ service.subtitle }}</div>64 </div>65 </button>66 </nav>67 <div class="border-t border-slate-900/70 p-6 space-y-3 text-sm">68 <div class="flex items-center gap-3">69 <div class="w-10 h-10 rounded-full bg-gradient-to-br from-brand-500 to-emerald-500 text-slate-950 flex items-center justify-center font-semibold">AZ</div>70 <div>71 <div class="font-semibold">Alicia Zamora</div>72 <div class="text-xs text-slate-500">Principal Cloud Engineer</div>73 </div>74 </div>75 <button class="w-full flex items-center justify-center gap-2 border border-slate-800 rounded-md px-3 py-2 text-xs uppercase tracking-widest hover:border-brand-500/40 hover:text-brand-400 transition">76 <span class="text-sm">⏻</span> Logout77 </button>78 </div>79 </aside>80 <div class="flex-1 flex flex-col">81 <header class="border-b border-slate-900/70 px-8 py-5 flex items-center justify-between bg-slate-950/70 backdrop-blur">82 <div>83 <div class="text-xs uppercase tracking-widest text-slate-500">Current project</div>84 <div class="text-xl font-semibold" v-if="selectedProject">{{ selectedProject.name }}</div>85 <div class="text-sm text-slate-500" v-if="selectedProject">Owned by {{ selectedProject.owners.join(', ') }}</div>86 </div>87 <div class="flex items-center gap-4">88 <div class="flex items-center gap-2 bg-slate-900 border border-slate-800 rounded-md px-3 py-2 text-xs uppercase tracking-widest">89 <span class="text-slate-500">Environment</span>90 <button class="px-2 py-1 rounded bg-slate-800/80">Staging</button>91 <button class="px-2 py-1 rounded bg-brand-500/20 border border-brand-500/40 text-brand-300">Production</button>92 </div>93 <div class="relative">94 <span class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500">🔍</span>95 <input96 type="text"97 placeholder="Quick search (⌘ + K)"98 class="bg-slate-900 border border-slate-800 rounded-md pl-9 pr-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500/60 placeholder:text-slate-600"99 />100 </div>101 <button class="flex items-center gap-2 bg-brand-500/10 border border-brand-500/40 text-brand-300 px-3 py-2 rounded-md text-sm hover:bg-brand-500/20 transition">102 <span>➕</span> New resource103 </button>104 </div>105 </header>106 <div class="flex-1 flex flex-col overflow-hidden">107 <main class="flex-1 overflow-y-auto px-8 py-6 space-y-6">108 <section v-if="activeService === 'overview'" class="space-y-6">109 <div class="grid grid-cols-1 xl:grid-cols-3 gap-6">110 <div class="xl:col-span-2 bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-5">111 <div class="flex items-center justify-between">112 <div>113 <h2 class="text-lg font-semibold">Project health snapshot</h2>114 <p class="text-sm text-slate-400">High-level view of resources tied to {{ selectedProject.name }}.</p>115 </div>116 <span class="px-3 py-1 rounded-full text-xs uppercase tracking-widest"117 :class="selectedProject.status === 'healthy' ? 'bg-emerald-500/10 text-emerald-300 border border-emerald-400/40' : selectedProject.status === 'degraded' ? 'bg-amber-500/10 text-amber-300 border border-amber-400/40' : 'bg-rose-500/10 text-rose-300 border border-rose-400/40'">118 {{ selectedProject.status }}119 </span>120 </div>121 <div class="grid grid-cols-1 md:grid-cols-2 gap-4">122 <div class="bg-slate-950/60 border border-slate-800 rounded-xl p-4">123 <div class="flex items-center justify-between text-sm text-slate-400 mb-2">124 <span>Databases</span>125 <span>{{ projectMetrics.databases }} total</span>126 </div>127 <div class="text-2xl font-semibold mb-1">{{ projectMetrics.collections }} collections</div>128 <p class="text-xs text-slate-500">{{ projectMetrics.documents }} docs · {{ projectMetrics.storage }}</p>129 </div>130 <div class="bg-slate-950/60 border border-slate-800 rounded-xl p-4">131 <div class="flex items-center justify-between text-sm text-slate-400 mb-2">132 <span>Functions</span>133 <span>{{ projectMetrics.lambdas }} deployed</span>134 </div>135 <div class="text-2xl font-semibold mb-1">{{ projectMetrics.invocations }} invocations/24h</div>136 <p class="text-xs text-slate-500">Success rate {{ projectMetrics.lambdaSuccess }} · Median {{ projectMetrics.lambdaLatency }}</p>137 </div>138 <div class="bg-slate-950/60 border border-slate-800 rounded-xl p-4">139 <div class="flex items-center justify-between text-sm text-slate-400 mb-2">140 <span>Storage</span>141 <span>{{ projectMetrics.buckets }} buckets</span>142 </div>143 <div class="text-2xl font-semibold mb-1">{{ projectMetrics.objects }} objects</div>144 <p class="text-xs text-slate-500">{{ projectMetrics.storageUsage }}</p>145 </div>146 <div class="bg-slate-950/60 border border-slate-800 rounded-xl p-4">147 <div class="flex items-center justify-between text-sm text-slate-400 mb-2">148 <span>Streaming</span>149 <span>{{ projectMetrics.queues }} queues</span>150 </div>151 <div class="text-2xl font-semibold mb-1">{{ projectMetrics.queueThroughput }}</div>152 <p class="text-xs text-slate-500">{{ projectMetrics.queueLag }}</p>153 </div>154 </div>155 <div>156 <h3 class="text-sm uppercase tracking-widest text-slate-500 mb-3">Activity timeline</h3>157 <ol class="space-y-3 text-sm">158 <li v-for="event in selectedProject.timeline" :key="event.time" class="flex items-start gap-3">159 <div class="w-2 h-2 rounded-full mt-1" :class="event.type === 'deploy' ? 'bg-brand-400' : event.type === 'alert' ? 'bg-amber-400' : 'bg-slate-500'"></div>160 <div>161 <div class="text-slate-300">{{ event.title }}</div>162 <div class="text-xs text-slate-500">{{ event.time }} · {{ event.actor }}</div>163 </div>164 </li>165 </ol>166 </div>167 </div>168 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-5">169 <div class="flex items-center justify-between">170 <h2 class="text-lg font-semibold">API coverage</h2>171 <span class="text-xs text-slate-500">Based on OpenAPI specs</span>172 </div>173 <div class="space-y-4">174 <div175 v-for="summary in openApiSummaries"176 :key="summary.id"177 class="bg-slate-950/60 border border-slate-800 rounded-xl p-4"178 >179 <div class="flex items-center justify-between mb-2">180 <div>181 <div class="text-sm font-semibold text-slate-200">{{ summary.name }}</div>182 <div class="text-xs text-slate-500">{{ summary.description }}</div>183 </div>184 <span class="text-xs bg-slate-800/70 px-2 py-1 rounded">{{ summary.version }}</span>185 </div>186 <ul class="text-xs text-slate-400 space-y-1">187 <li v-for="endpoint in summary.keyEndpoints" :key="endpoint.path + endpoint.method" class="flex items-center gap-2">188 <span class="px-2 py-0.5 rounded-full border border-slate-700 uppercase tracking-widest" :class="methodColor(endpoint.method)">{{ endpoint.method }}</span>189 <code class="font-mono text-slate-300">{{ endpoint.path }}</code>190 <span class="text-slate-500">— {{ endpoint.note }}</span>191 </li>192 </ul>193 </div>194 </div>195 </div>196 </div>197 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6">198 <div class="flex items-center justify-between mb-5">199 <h2 class="text-lg font-semibold">Operational checklist</h2>200 <button class="text-xs uppercase tracking-widest text-brand-300 flex items-center gap-2">201 View playbooks <span>→</span>202 </button>203 </div>204 <div class="grid grid-cols-1 md:grid-cols-3 gap-4">205 <div class="bg-slate-950/60 border border-slate-800 rounded-xl p-4 space-y-3">206 <div class="flex items-center justify-between text-sm">207 <span class="text-slate-400">Config baselines</span>208 <span class="text-brand-300">{{ projectConfigs.length }} sets</span>209 </div>210 <p class="text-xs text-slate-500">Validate sensitive keys and feature flags before next deployment.</p>211 <button class="w-full text-xs uppercase tracking-widest border border-slate-800 rounded-md py-2 hover:border-brand-500/40 hover:text-brand-300">Review configs</button>212 </div>213 <div class="bg-slate-950/60 border border-slate-800 rounded-xl p-4 space-y-3">214 <div class="flex items-center justify-between text-sm">215 <span class="text-slate-400">Data retention</span>216 <span class="text-brand-300">{{ projectMetrics.collections }} collections</span>217 </div>218 <p class="text-xs text-slate-500">Confirm TTL indexes in InceptionDB align with compliance policies.</p>219 <button class="w-full text-xs uppercase tracking-widest border border-slate-800 rounded-md py-2 hover:border-brand-500/40 hover:text-brand-300">Audit indexes</button>220 </div>221 <div class="bg-slate-950/60 border border-slate-800 rounded-xl p-4 space-y-3">222 <div class="flex items-center justify-between text-sm">223 <span class="text-slate-400">Runtime coverage</span>224 <span class="text-brand-300">{{ projectMetrics.lambdas }} functions</span>225 </div>226 <p class="text-xs text-slate-500">Run smoke tests against the latest lambda deployments.</p>227 <button class="w-full text-xs uppercase tracking-widest border border-slate-800 rounded-md py-2 hover:border-brand-500/40 hover:text-brand-300">Open runbook</button>228 </div>229 </div>230 </div>231 </section>232 233 <section v-if="activeService === 'projects'" class="space-y-6">234 <div class="flex items-center justify-between">235 <div>236 <h1 class="text-2xl font-semibold">Projects</h1>237 <p class="text-sm text-slate-400">Create and orchestrate Hola Cloud projects. Each resource belongs to a project.</p>238 </div>239 <button class="flex items-center gap-2 border border-brand-500/40 bg-brand-500/10 text-brand-200 px-4 py-2 rounded-md text-sm">240 ➕ New project241 </button>242 </div>243 <div class="grid grid-cols-1 xl:grid-cols-3 gap-6">244 <div class="xl:col-span-1 space-y-4">245 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl">246 <div class="px-5 py-4 border-b border-slate-800 text-xs uppercase tracking-widest text-slate-500">Project catalog</div>247 <div class="divide-y divide-slate-800">248 <button249 v-for="project in projects"250 :key="project.id"251 @click="selectedProjectId = project.id"252 class="w-full text-left px-5 py-4 hover:bg-slate-900/40 transition"253 :class="selectedProjectId === project.id ? 'bg-slate-900/70 border-l-2 border-brand-400/60' : ''"254 >255 <div class="flex items-center justify-between">256 <div>257 <div class="font-semibold text-slate-200">{{ project.name }}</div>258 <div class="text-xs text-slate-500">{{ project.id }}</div>259 </div>260 <span class="text-xs px-2 py-0.5 rounded-full border border-slate-800" :class="project.status === 'healthy' ? 'text-emerald-300 border-emerald-400/40' : project.status === 'degraded' ? 'text-amber-300 border-amber-400/40' : 'text-rose-300 border-rose-400/40'">{{ project.status }}</span>261 </div>262 <div class="mt-3 flex flex-wrap gap-2 text-[0.65rem] uppercase tracking-widest text-slate-500">263 <span v-for="tag in project.tags" :key="tag" class="px-2 py-0.5 rounded bg-slate-900 border border-slate-800">{{ tag }}</span>264 </div>265 </button>266 </div>267 </div>268 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-5 space-y-4">269 <div class="text-sm font-semibold">Lifecycle guardrails</div>270 <p class="text-xs text-slate-500">Configure approvals, deletion workflows and project templates to streamline onboarding.</p>271 <div class="space-y-2">272 <label class="flex items-center justify-between text-xs uppercase tracking-widest text-slate-500">273 Require approval274 <input type="checkbox" checked class="accent-brand-400" />275 </label>276 <label class="flex items-center justify-between text-xs uppercase tracking-widest text-slate-500">277 Lock prod routers278 <input type="checkbox" checked class="accent-brand-400" />279 </label>280 <label class="flex items-center justify-between text-xs uppercase tracking-widest text-slate-500">281 Daily drift report282 <input type="checkbox" class="accent-brand-400" />283 </label>284 </div>285 </div>286 </div>287 <div class="xl:col-span-2 space-y-6" v-if="selectedProject">288 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-4">289 <div class="flex items-center justify-between">290 <div>291 <h2 class="text-xl font-semibold">{{ selectedProject.name }}</h2>292 <p class="text-sm text-slate-400">Created {{ formatUnix(selectedProject.create_timestamp) }} · Updated {{ formatUnix(selectedProject.update_timestamp) }}</p>293 </div>294 <div class="flex gap-2">295 <button class="border border-slate-800 rounded-md px-3 py-2 text-xs uppercase tracking-widest hover:border-brand-500/40 hover:text-brand-300">Edit metadata</button>296 <button class="border border-rose-500/40 text-rose-300 bg-rose-500/10 rounded-md px-3 py-2 text-xs uppercase tracking-widest">Archive</button>297 </div>298 </div>299 <div class="grid grid-cols-1 md:grid-cols-2 gap-4">300 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">301 <div class="text-xs uppercase tracking-widest text-slate-500 mb-2">Owners</div>302 <ul class="text-sm space-y-1">303 <li v-for="owner in selectedProject.owners" :key="owner" class="flex items-center justify-between">304 <span>{{ owner }}</span>305 <button class="text-xs text-slate-500 hover:text-brand-300">Make primary</button>306 </li>307 </ul>308 <button class="mt-3 w-full text-xs uppercase tracking-widest border border-slate-800 rounded-md py-2 hover:border-brand-500/40 hover:text-brand-300">Add owner</button>309 </div>310 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">311 <div class="text-xs uppercase tracking-widest text-slate-500 mb-2">Authentication</div>312 <div class="flex items-center justify-between">313 <div>314 <div class="text-sm font-semibold">Project-level auth</div>315 <div class="text-xs text-slate-500">{{ selectedProject.auth.enabled ? 'Enabled for all endpoints' : 'Disabled' }}</div>316 </div>317 <label class="relative inline-flex items-center cursor-pointer">318 <input type="checkbox" class="sr-only peer" :checked="selectedProject.auth.enabled" />319 <div class="w-11 h-6 bg-slate-800 rounded-full peer peer-checked:bg-brand-500/60"></div>320 <span class="ml-3 text-xs uppercase tracking-widest text-slate-500">Toggle</span>321 </label>322 </div>323 <div class="mt-3 text-xs text-slate-500">Use /v0/projects/{project_id} to update auth and routers.</div>324 </div>325 </div>326 </div>327 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl">328 <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between">329 <div>330 <div class="text-sm font-semibold">Routers</div>331 <div class="text-xs text-slate-500">Mirrors /v0/projects/{project_id} payload</div>332 </div>333 <button class="text-xs uppercase tracking-widest text-brand-300 flex items-center gap-2">Add route <span>→</span></button>334 </div>335 <div class="divide-y divide-slate-800">336 <div v-for="router in selectedProject.routers" :key="router.type" class="px-6 py-5 space-y-4">337 <div class="flex items-center justify-between">338 <div>339 <div class="text-sm font-semibold text-slate-200">{{ router.type }} router</div>340 <div class="text-xs text-slate-500">{{ router.hosts.length }} hosts · {{ objectKeys(router.config).join(', ') }} config keys</div>341 </div>342 <div class="flex gap-2">343 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Edit</button>344 <button class="text-xs uppercase tracking-widest border border-rose-500/40 text-rose-300 bg-rose-500/10 rounded-md px-3 py-1.5">Disable</button>345 </div>346 </div>347 <div class="grid md:grid-cols-2 gap-4">348 <div>349 <div class="text-xs uppercase tracking-widest text-slate-500 mb-2">Hosts</div>350 <div class="space-y-2">351 <div v-for="host in router.hosts" :key="host.name" class="flex items-center justify-between bg-slate-950/40 border border-slate-800 rounded-lg px-3 py-2 text-sm">352 <div>353 <div>{{ host.name }}</div>354 <div class="text-xs text-slate-500">Added {{ formatUnix(host.creation_timestamp) }}</div>355 </div>356 <span class="text-xs px-2 py-0.5 rounded-full border" :class="host.verified ? 'text-emerald-300 border-emerald-400/40' : 'text-amber-300 border-amber-400/40'">{{ host.verified ? 'verified' : 'pending' }}</span>357 </div>358 </div>359 </div>360 <div>361 <div class="text-xs uppercase tracking-widest text-slate-500 mb-2">Headers</div>362 <div class="bg-slate-950/40 border border-slate-800 rounded-lg p-3 text-xs text-slate-400 space-y-2">363 <div>364 <div class="uppercase tracking-widest text-slate-500">Inbound</div>365 <ul class="space-y-1">366 <li v-for="(value, key) in router.HeadersIn" :key="key" class="flex justify-between"><span>{{ key }}</span><span class="text-slate-300">{{ value }}</span></li>367 </ul>368 </div>369 <div>370 <div class="uppercase tracking-widest text-slate-500">Outbound</div>371 <ul class="space-y-1">372 <li v-for="(value, key) in router.HeadersOut" :key="key" class="flex justify-between"><span>{{ key }}</span><span class="text-slate-300">{{ value }}</span></li>373 </ul>374 </div>375 </div>376 </div>377 </div>378 </div>379 </div>380 </div>381 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6">382 <div class="flex items-center justify-between mb-4">383 <div>384 <div class="text-sm font-semibold">Automation templates</div>385 <div class="text-xs text-slate-500">Jumpstart provisioning flows for this project.</div>386 </div>387 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-2 hover:border-brand-500/40 hover:text-brand-300">Create blueprint</button>388 </div>389 <div class="grid md:grid-cols-3 gap-4 text-xs text-slate-400">390 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4 space-y-2">391 <div class="text-sm font-semibold text-slate-200">Realtime stack</div>392 <p>Includes InstantLogs, Tailon queue and lambda consumer wiring.</p>393 </div>394 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4 space-y-2">395 <div class="text-sm font-semibold text-slate-200">Data warehouse</div>396 <p>Provision InceptionDB cluster and file export buckets.</p>397 </div>398 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4 space-y-2">399 <div class="text-sm font-semibold text-slate-200">Edge API</div>400 <p>Route traffic to lambda functions secured with project auth.</p>401 </div>402 </div>403 </div>404 </div>405 </div>406 </section>407 408 <section v-if="activeService === 'config'" class="space-y-6">409 <div class="flex items-center justify-between">410 <div>411 <h1 class="text-2xl font-semibold">Configuration registry</h1>412 <p class="text-sm text-slate-400">Map configuration items from /v0/configs and /v0/configs/{configId}.</p>413 </div>414 <button class="flex items-center gap-2 border border-brand-500/40 bg-brand-500/10 text-brand-200 px-4 py-2 rounded-md text-sm">➕ New config</button>415 </div>416 <div class="grid grid-cols-1 xl:grid-cols-4 gap-6">417 <div class="xl:col-span-1 bg-slate-900/60 border border-slate-800 rounded-2xl overflow-hidden">418 <div class="px-5 py-4 border-b border-slate-800 text-xs uppercase tracking-widest text-slate-500">Config sets</div>419 <div class="divide-y divide-slate-800">420 <button421 v-for="config in projectConfigs"422 :key="config.id"423 @click="selectedConfigId = config.id"424 class="w-full text-left px-5 py-4 hover:bg-slate-900/40 transition"425 :class="activeConfig && activeConfig.id === config.id ? 'bg-slate-900/70 border-l-2 border-brand-400/60' : ''"426 >427 <div class="font-semibold text-slate-200">{{ config.name }}</div>428 <div class="text-xs text-slate-500 truncate">{{ config.description }}</div>429 <div class="mt-2 text-[0.65rem] uppercase tracking-widest text-slate-500">{{ Object.keys(config.entries).length }} entries</div>430 </button>431 <div v-if="projectConfigs.length === 0" class="px-5 py-12 text-center text-sm text-slate-500">No configs yet for this project.</div>432 </div>433 </div>434 <div class="xl:col-span-3 space-y-6" v-if="activeConfig">435 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-5">436 <div class="flex items-center justify-between">437 <div>438 <h2 class="text-xl font-semibold">{{ activeConfig.name }}</h2>439 <p class="text-sm text-slate-400">Last updated {{ activeConfig.updated }} · ID {{ activeConfig.id }}</p>440 </div>441 <div class="flex gap-2">442 <button class="border border-slate-800 rounded-md px-3 py-2 text-xs uppercase tracking-widest hover:border-brand-500/40 hover:text-brand-300">Edit</button>443 <button class="border border-rose-500/40 text-rose-300 bg-rose-500/10 rounded-md px-3 py-2 text-xs uppercase tracking-widest">Delete</button>444 </div>445 </div>446 <div class="grid md:grid-cols-2 gap-4">447 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">448 <div class="text-xs uppercase tracking-widest text-slate-500 mb-2">Description</div>449 <p class="text-sm text-slate-300">{{ activeConfig.description }}</p>450 </div>451 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">452 <div class="text-xs uppercase tracking-widest text-slate-500 mb-2">Reference</div>453 <p class="text-xs text-slate-400">Use PATCH /v0/configs/{{ activeConfig.id }} to update keys incrementally.</p>454 </div>455 </div>456 <div>457 <div class="flex items-center justify-between mb-3">458 <div class="text-xs uppercase tracking-widest text-slate-500">Entries</div>459 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Add entry</button>460 </div>461 <div class="border border-slate-800 rounded-xl overflow-hidden">462 <table class="min-w-full text-sm">463 <thead class="bg-slate-950/70 text-slate-400 text-xs uppercase tracking-widest">464 <tr>465 <th class="px-4 py-3 text-left">Key</th>466 <th class="px-4 py-3 text-left">Value</th>467 <th class="px-4 py-3 text-left">Notes</th>468 </tr>469 </thead>470 <tbody class="divide-y divide-slate-800">471 <tr v-for="entry in configEntries" :key="entry.key" class="hover:bg-slate-900/40">472 <td class="px-4 py-3 font-mono text-slate-300">{{ entry.key }}</td>473 <td class="px-4 py-3 text-slate-200">{{ entry.value }}</td>474 <td class="px-4 py-3 text-xs text-slate-500">{{ entry.note }}</td>475 </tr>476 </tbody>477 </table>478 </div>479 </div>480 </div>481 <div class="grid md:grid-cols-2 gap-6">482 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-4">483 <div class="flex items-center justify-between">484 <div>485 <div class="text-sm font-semibold">Version history</div>486 <div class="text-xs text-slate-500">Snapshots stored in files bucket</div>487 </div>488 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Compare</button>489 </div>490 <ol class="text-sm text-slate-300 space-y-2">491 <li v-for="version in activeConfig.history" :key="version.id" class="flex items-center justify-between">492 <span>{{ version.label }}</span>493 <span class="text-xs text-slate-500">{{ version.at }}</span>494 </li>495 </ol>496 </div>497 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-4">498 <div>499 <div class="text-sm font-semibold">Deployment guardrails</div>500 <div class="text-xs text-slate-500">Validate config before lambda rollout.</div>501 </div>502 <div class="space-y-3 text-xs text-slate-400">503 <label class="flex items-center justify-between uppercase tracking-widest">504 Require schema check505 <input type="checkbox" checked class="accent-brand-400" />506 </label>507 <label class="flex items-center justify-between uppercase tracking-widest">508 Lock edits after deploy509 <input type="checkbox" class="accent-brand-400" />510 </label>511 <label class="flex items-center justify-between uppercase tracking-widest">512 Notify owners513 <input type="checkbox" checked class="accent-brand-400" />514 </label>515 </div>516 </div>517 </div>518 </div>519 </div>520 </section>521 522 <section v-if="activeService === 'files'" class="space-y-6">523 <div class="flex items-center justify-between">524 <div>525 <h1 class="text-2xl font-semibold">Object storage</h1>526 <p class="text-sm text-slate-400">Manage buckets via /v1/buckets and files under /v1/buckets/{bucket_id}/files.</p>527 </div>528 <button class="flex items-center gap-2 border border-brand-500/40 bg-brand-500/10 text-brand-200 px-4 py-2 rounded-md text-sm">➕ Create bucket</button>529 </div>530 <div class="grid grid-cols-1 xl:grid-cols-4 gap-6">531 <div class="xl:col-span-1 bg-slate-900/60 border border-slate-800 rounded-2xl overflow-hidden">532 <div class="px-5 py-4 border-b border-slate-800 text-xs uppercase tracking-widest text-slate-500">Buckets</div>533 <div class="divide-y divide-slate-800">534 <button535 v-for="bucket in projectBuckets"536 :key="bucket.id"537 @click="selectedBucketId = bucket.id"538 class="w-full text-left px-5 py-4 hover:bg-slate-900/40 transition"539 :class="activeBucket && activeBucket.id === bucket.id ? 'bg-slate-900/70 border-l-2 border-brand-400/60' : ''"540 >541 <div class="font-semibold text-slate-200">{{ bucket.name }}</div>542 <div class="text-xs text-slate-500">{{ bucket.description }}</div>543 <div class="mt-2 text-[0.65rem] uppercase tracking-widest text-slate-500">{{ formatUnix(bucket.created_timestamp) }}</div>544 </button>545 <div v-if="projectBuckets.length === 0" class="px-5 py-12 text-center text-sm text-slate-500">No buckets for this project.</div>546 </div>547 </div>548 <div class="xl:col-span-3 space-y-6" v-if="activeBucket">549 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-4">550 <div class="flex items-center justify-between">551 <div>552 <h2 class="text-xl font-semibold">{{ activeBucket.name }}</h2>553 <p class="text-sm text-slate-400">ID {{ activeBucket.id }} · Owned by {{ activeBucket.owners.join(', ') }}</p>554 </div>555 <div class="flex gap-2">556 <button class="border border-slate-800 rounded-md px-3 py-2 text-xs uppercase tracking-widest hover:border-brand-500/40 hover:text-brand-300">Lifecycle</button>557 <button class="border border-rose-500/40 text-rose-300 bg-rose-500/10 rounded-md px-3 py-2 text-xs uppercase tracking-widest">Delete bucket</button>558 </div>559 </div>560 <div class="grid md:grid-cols-3 gap-4 text-sm">561 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">562 <div class="text-xs uppercase tracking-widest text-slate-500">Objects</div>563 <div class="text-xl font-semibold">{{ bucketMetrics.objectCount }}</div>564 <div class="text-xs text-slate-500">{{ bucketMetrics.totalSize }}</div>565 </div>566 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">567 <div class="text-xs uppercase tracking-widest text-slate-500">Storage class</div>568 <div class="text-xl font-semibold">{{ bucketMetrics.storageClass }}</div>569 <div class="text-xs text-slate-500">Replicated across 3 zones</div>570 </div>571 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">572 <div class="text-xs uppercase tracking-widest text-slate-500">Versioning</div>573 <div class="text-xl font-semibold">{{ bucketMetrics.versioning }}</div>574 <div class="text-xs text-slate-500">Latest change {{ bucketMetrics.lastModified }}</div>575 </div>576 </div>577 <div class="border border-slate-800 rounded-xl overflow-hidden">578 <table class="min-w-full text-sm">579 <thead class="bg-slate-950/70 text-slate-400 text-xs uppercase tracking-widest">580 <tr>581 <th class="px-4 py-3 text-left">Object</th>582 <th class="px-4 py-3 text-left">Size</th>583 <th class="px-4 py-3 text-left">Checksum</th>584 <th class="px-4 py-3 text-left">Updated</th>585 <th class="px-4 py-3 text-left">Status</th>586 </tr>587 </thead>588 <tbody class="divide-y divide-slate-800">589 <tr v-for="file in bucketFiles" :key="file.id" class="hover:bg-slate-900/40">590 <td class="px-4 py-3 font-mono text-slate-300">{{ file.name }}</td>591 <td class="px-4 py-3">{{ formatBytes(file.size) }}</td>592 <td class="px-4 py-3 text-xs text-slate-500">md5 {{ file.hash_md5 }}</td>593 <td class="px-4 py-3 text-xs text-slate-500">{{ formatUnix(file.updated_timestamp) }}</td>594 <td class="px-4 py-3">595 <span class="px-2 py-0.5 rounded-full border text-xs"596 :class="file.status === 'available' ? 'text-emerald-300 border-emerald-400/40' : file.status === 'processing' ? 'text-amber-300 border-amber-400/40' : 'text-rose-300 border-rose-400/40'">597 {{ file.status }}598 </span>599 </td>600 </tr>601 </tbody>602 </table>603 </div>604 <div class="flex flex-col lg:flex-row gap-4 text-sm">605 <div class="flex-1 bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-2">606 <div class="text-xs uppercase tracking-widest text-slate-500">Upload preview</div>607 <div class="border border-dashed border-slate-800 rounded-lg p-6 text-center text-slate-500">608 Drag & drop files or call POST /v1/buckets/{{ activeBucket.id }}/files/*609 </div>610 </div>611 <div class="w-full lg:w-72 bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">612 <div class="text-xs uppercase tracking-widest text-slate-500">Access policy</div>613 <p>Attached to project policy <span class="text-brand-300">{{ selectedProject.name }}</span>.</p>614 <ul class="text-xs text-slate-400 space-y-2">615 <li>• Signed URL expiry: 15 minutes</li>616 <li>• Encryption: AES-256</li>617 <li>• Replication: EU → US</li>618 </ul>619 </div>620 </div>621 </div>622 </div>623 </div>624 </section>625 626 <section v-if="activeService === 'inceptiondb'" class="space-y-6">627 <div class="flex items-center justify-between">628 <div>629 <h1 class="text-2xl font-semibold">InceptionDB</h1>630 <p class="text-sm text-slate-400">Document database control plane using /v1/databases and nested collection endpoints.</p>631 </div>632 <div class="flex gap-2">633 <button class="flex items-center gap-2 border border-brand-500/40 bg-brand-500/10 text-brand-200 px-4 py-2 rounded-md text-sm">➕ Create database</button>634 <button class="flex items-center gap-2 border border-slate-800 rounded-md px-4 py-2 text-sm hover:border-brand-500/40 hover:text-brand-300">Docs</button>635 </div>636 </div>637 <div class="grid grid-cols-1 xl:grid-cols-4 gap-6">638 <div class="xl:col-span-1 bg-slate-900/60 border border-slate-800 rounded-2xl overflow-hidden">639 <div class="px-5 py-4 border-b border-slate-800 text-xs uppercase tracking-widest text-slate-500">Databases</div>640 <div class="divide-y divide-slate-800">641 <button642 v-for="database in projectDatabases"643 :key="database.id"644 @click="selectedDatabaseId = database.id"645 class="w-full text-left px-5 py-4 hover:bg-slate-900/40 transition"646 :class="activeDatabase && activeDatabase.id === database.id ? 'bg-slate-900/70 border-l-2 border-brand-400/60' : ''"647 >648 <div class="flex items-center justify-between">649 <div>650 <div class="font-semibold text-slate-200">{{ database.name }}</div>651 <div class="text-xs text-slate-500">{{ database.id }}</div>652 </div>653 <div class="text-xs text-slate-500 text-right">654 {{ database.collections.length }} collections655 <div>{{ formatNumber(database.usage.documents) }} docs</div>656 </div>657 </div>658 </button>659 <div v-if="projectDatabases.length === 0" class="px-5 py-12 text-center text-sm text-slate-500">No databases yet. POST /v1/databases to create.</div>660 </div>661 </div>662 <div class="xl:col-span-3 space-y-6" v-if="activeDatabase">663 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-5">664 <div class="flex items-center justify-between">665 <div>666 <h2 class="text-xl font-semibold">{{ activeDatabase.name }}</h2>667 <p class="text-sm text-slate-400">Created {{ activeDatabase.creation_date }} · Owners {{ activeDatabase.owners.join(', ') }}</p>668 </div>669 <div class="flex gap-2">670 <button class="border border-slate-800 rounded-md px-3 py-2 text-xs uppercase tracking-widest hover:border-brand-500/40 hover:text-brand-300">Rotate API key</button>671 <button class="border border-rose-500/40 text-rose-300 bg-rose-500/10 rounded-md px-3 py-2 text-xs uppercase tracking-widest">Delete database</button>672 </div>673 </div>674 <div class="grid md:grid-cols-4 gap-4 text-sm">675 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">676 <div class="text-xs uppercase tracking-widest text-slate-500">Documents</div>677 <div class="text-xl font-semibold">{{ formatNumber(activeDatabase.usage.documents) }}</div>678 <div class="text-xs text-slate-500">{{ activeDatabase.collections.length }} collections</div>679 </div>680 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">681 <div class="text-xs uppercase tracking-widest text-slate-500">Storage</div>682 <div class="text-xl font-semibold">{{ activeDatabase.usage.storage }}</div>683 <div class="text-xs text-slate-500">{{ activeDatabase.usage.indexes }} indexes</div>684 </div>685 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">686 <div class="text-xs uppercase tracking-widest text-slate-500">Reads</div>687 <div class="text-xl font-semibold">{{ activeDatabase.usage.readOps }}/min</div>688 <div class="text-xs text-slate-500">Last 15 minutes</div>689 </div>690 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">691 <div class="text-xs uppercase tracking-widest text-slate-500">Writes</div>692 <div class="text-xl font-semibold">{{ activeDatabase.usage.writeOps }}/min</div>693 <div class="text-xs text-slate-500">Streaming ingestion</div>694 </div>695 </div>696 <div>697 <div class="flex flex-wrap items-center gap-3 mb-4">698 <button699 v-for="tab in databaseTabs"700 :key="tab.id"701 @click="databaseTab = tab.id"702 class="px-4 py-2 rounded-md text-xs uppercase tracking-widest border"703 :class="databaseTab === tab.id ? 'border-brand-500/40 bg-brand-500/10 text-brand-200' : 'border-slate-800 text-slate-400 hover:text-slate-200'"704 >705 {{ tab.label }}706 </button>707 <span class="text-xs text-slate-500">Operations reference: {{ databaseTabSummary }}</span>708 </div>709 <div v-if="databaseTab === 'overview'" class="grid md:grid-cols-2 gap-4 text-sm">710 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">711 <div class="text-xs uppercase tracking-widest text-slate-500">Recent indexes</div>712 <ul class="space-y-2">713 <li v-for="index in activeDatabase.indexHighlights" :key="index.name" class="flex items-center justify-between">714 <div>715 <div class="font-mono text-slate-300">{{ index.name }}</div>716 <div class="text-xs text-slate-500">{{ index.type }} · fields {{ index.fields.join(', ') }}</div>717 </div>718 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-2 py-1 hover:border-brand-500/40 hover:text-brand-300">Inspect</button>719 </li>720 </ul>721 </div>722 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">723 <div class="text-xs uppercase tracking-widest text-slate-500">Stream ingestion</div>724 <p>Use POST /v1/databases/{{ activeDatabase.id }}/collections/{collection}:insertStream to push batched events.</p>725 <div class="bg-slate-900/70 border border-slate-800 rounded-lg p-4 font-mono text-xs text-slate-400">726 curl -X POST 727 <br /> https://api.hola.cloud/inceptiondb/v1/databases/{{ activeDatabase.id }}/collections/events:insertStream728 </div>729 </div>730 </div>731 <div v-if="databaseTab === 'collections'" class="space-y-4">732 <div class="flex items-center justify-between">733 <div class="text-xs uppercase tracking-widest text-slate-500">Collections</div>734 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Create collection</button>735 </div>736 <div class="border border-slate-800 rounded-xl overflow-hidden">737 <table class="min-w-full text-sm">738 <thead class="bg-slate-950/70 text-slate-400 text-xs uppercase tracking-widest">739 <tr>740 <th class="px-4 py-3 text-left">Name</th>741 <th class="px-4 py-3 text-left">Documents</th>742 <th class="px-4 py-3 text-left">Indexes</th>743 <th class="px-4 py-3 text-left">Defaults</th>744 <th class="px-4 py-3 text-left">Actions</th>745 </tr>746 </thead>747 <tbody class="divide-y divide-slate-800">748 <tr v-for="collection in activeDatabase.collections" :key="collection.name" class="hover:bg-slate-900/40">749 <td class="px-4 py-3 text-slate-200">{{ collection.name }}</td>750 <td class="px-4 py-3">{{ formatNumber(collection.total) }}</td>751 <td class="px-4 py-3 text-xs text-slate-500">{{ collection.indexes.length }} indexes</td>752 <td class="px-4 py-3 text-xs text-slate-500">{{ summarizeDefaults(collection.defaults) }}</td>753 <td class="px-4 py-3 text-xs text-brand-300 space-x-2">754 <button>Find</button>755 <button>Set defaults</button>756 <button>Drop</button>757 </td>758 </tr>759 </tbody>760 </table>761 </div>762 </div>763 <div v-if="databaseTab === 'apiKeys'" class="space-y-4">764 <div class="flex items-center justify-between">765 <div class="text-xs uppercase tracking-widest text-slate-500">API Keys</div>766 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Create key</button>767 </div>768 <div class="border border-slate-800 rounded-xl overflow-hidden">769 <table class="min-w-full text-sm">770 <thead class="bg-slate-950/70 text-slate-400 text-xs uppercase tracking-widest">771 <tr>772 <th class="px-4 py-3 text-left">Name</th>773 <th class="px-4 py-3 text-left">Key</th>774 <th class="px-4 py-3 text-left">Created</th>775 <th class="px-4 py-3 text-left">Secret</th>776 </tr>777 </thead>778 <tbody class="divide-y divide-slate-800">779 <tr v-for="apiKey in activeDatabase.api_keys" :key="apiKey.key" class="hover:bg-slate-900/40">780 <td class="px-4 py-3 text-slate-200">{{ apiKey.name }}</td>781 <td class="px-4 py-3 font-mono text-slate-300">{{ apiKey.key }}</td>782 <td class="px-4 py-3 text-xs text-slate-500">{{ apiKey.creation_date }}</td>783 <td class="px-4 py-3 text-xs text-slate-500">{{ apiKey.secret || '••••••••' }}</td>784 </tr>785 </tbody>786 </table>787 </div>788 </div>789 <div v-if="databaseTab === 'owners'" class="space-y-4">790 <div class="flex items-center justify-between">791 <div class="text-xs uppercase tracking-widest text-slate-500">Owners</div>792 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Add owner</button>793 </div>794 <ul class="space-y-2 text-sm">795 <li v-for="owner in activeDatabase.owners" :key="owner" class="flex items-center justify-between bg-slate-950/40 border border-slate-800 rounded-lg px-3 py-2">796 <div>797 <div class="text-slate-200">{{ owner }}</div>798 <div class="text-xs text-slate-500">Remove via POST /v1/databases/{{ activeDatabase.id }}:deleteOwner</div>799 </div>800 <button class="text-xs uppercase tracking-widest border border-rose-500/40 text-rose-300 bg-rose-500/10 rounded-md px-2 py-1">Remove</button>801 </li>802 </ul>803 </div>804 </div>805 </div>806 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-4">807 <div class="flex items-center justify-between">808 <div>809 <div class="text-sm font-semibold">Query rehearsal</div>810 <div class="text-xs text-slate-500">Staged Find / Insert payloads</div>811 </div>812 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Save preset</button>813 </div>814 <div class="grid md:grid-cols-2 gap-4 text-xs">815 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-2">816 <div class="uppercase tracking-widest text-slate-500">Find</div>817 <pre class="whitespace-pre-wrap text-slate-300">{{ JSON.stringify(activeDatabase.sampleQueries.find, null, 2) }}</pre>818 </div>819 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-2">820 <div class="uppercase tracking-widest text-slate-500">Insert</div>821 <pre class="whitespace-pre-wrap text-slate-300">{{ JSON.stringify(activeDatabase.sampleQueries.insert, null, 2) }}</pre>822 </div>823 </div>824 </div>825 </div>826 </div>827 </section>828 829 <section v-if="activeService === 'instantlogs'" class="space-y-6">830 <div class="flex items-center justify-between">831 <div>832 <h1 class="text-2xl font-semibold">InstantLogs</h1>833 <p class="text-sm text-slate-400">Real-time log streaming via /v1/loggers, ingestion, filters and stats.</p>834 </div>835 <div class="flex gap-2">836 <button class="flex items-center gap-2 border border-brand-500/40 bg-brand-500/10 text-brand-200 px-4 py-2 rounded-md text-sm">➕ Create logger</button>837 <button class="flex items-center gap-2 border border-slate-800 rounded-md px-4 py-2 text-sm hover:border-brand-500/40 hover:text-brand-300">Tail stream</button>838 </div>839 </div>840 <div class="grid grid-cols-1 xl:grid-cols-4 gap-6">841 <div class="xl:col-span-1 bg-slate-900/60 border border-slate-800 rounded-2xl overflow-hidden">842 <div class="px-5 py-4 border-b border-slate-800 text-xs uppercase tracking-widest text-slate-500">Loggers</div>843 <div class="divide-y divide-slate-800">844 <button845 v-for="logger in projectLoggers"846 :key="logger.id"847 @click="selectedLoggerId = logger.id"848 class="w-full text-left px-5 py-4 hover:bg-slate-900/40 transition"849 :class="activeLogger && activeLogger.id === logger.id ? 'bg-slate-900/70 border-l-2 border-brand-400/60' : ''"850 >851 <div class="font-semibold text-slate-200">{{ logger.name }}</div>852 <div class="text-xs text-slate-500">{{ logger.id }}</div>853 <div class="mt-2 text-[0.65rem] uppercase tracking-widest text-slate-500">{{ logger.owners.length }} owners · {{ formatNumber(logger.usage.bytes_received) }} bytes</div>854 </button>855 <div v-if="projectLoggers.length === 0" class="px-5 py-12 text-center text-sm text-slate-500">No loggers yet. POST /v1/loggers to provision.</div>856 </div>857 </div>858 <div class="xl:col-span-3 space-y-6" v-if="activeLogger">859 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-5">860 <div class="flex items-center justify-between">861 <div>862 <h2 class="text-xl font-semibold">{{ activeLogger.name }}</h2>863 <p class="text-sm text-slate-400">Created {{ activeLogger.creation_date }} · Owners {{ activeLogger.owners.join(', ') }}</p>864 </div>865 <div class="flex gap-2">866 <button class="border border-slate-800 rounded-md px-3 py-2 text-xs uppercase tracking-widest hover:border-brand-500/40 hover:text-brand-300">Create API key</button>867 <button class="border border-slate-800 rounded-md px-3 py-2 text-xs uppercase tracking-widest hover:border-brand-500/40 hover:text-brand-300">Manage filters</button>868 </div>869 </div>870 <div class="grid md:grid-cols-4 gap-4 text-sm">871 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">872 <div class="text-xs uppercase tracking-widest text-slate-500">Bytes received</div>873 <div class="text-xl font-semibold">{{ formatBytes(activeLogger.usage.bytes_received) }}</div>874 <div class="text-xs text-slate-500">{{ activeLogger.usage.num_ingests }} ingests</div>875 </div>876 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">877 <div class="text-xs uppercase tracking-widest text-slate-500">Bytes filtered</div>878 <div class="text-xl font-semibold">{{ formatBytes(activeLogger.usage.bytes_filtered) }}</div>879 <div class="text-xs text-slate-500">{{ activeLogger.usage.num_filters }} filters</div>880 </div>881 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">882 <div class="text-xs uppercase tracking-widest text-slate-500">Bytes sent</div>883 <div class="text-xl font-semibold">{{ formatBytes(activeLogger.usage.bytes_sent) }}</div>884 <div class="text-xs text-slate-500">Live tail</div>885 </div>886 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">887 <div class="text-xs uppercase tracking-widest text-slate-500">API keys</div>888 <div class="text-xl font-semibold">{{ activeLogger.api_keys.length }}</div>889 <div class="text-xs text-slate-500">/v1/loggers/{{ activeLogger.id }}/apiKeys</div>890 </div>891 </div>892 <div class="flex flex-wrap items-center gap-3">893 <div class="text-xs uppercase tracking-widest text-slate-500">Timeframe</div>894 <button895 v-for="range in logRanges"896 :key="range.id"897 @click="loggerRange = range.id"898 class="px-3 py-1 rounded-md text-xs uppercase tracking-widest border"899 :class="loggerRange === range.id ? 'border-brand-500/40 bg-brand-500/10 text-brand-200' : 'border-slate-800 text-slate-400 hover:text-slate-200'"900 >901 {{ range.label }}902 </button>903 <span class="text-xs text-slate-500">Stats: /v1/loggers/{{ activeLogger.id }}/stats</span>904 </div>905 <div class="grid md:grid-cols-2 gap-4 text-sm">906 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">907 <div class="text-xs uppercase tracking-widest text-slate-500">Filters</div>908 <ul class="space-y-2">909 <li v-for="filter in activeLogger.filters" :key="filter.name" class="bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2">910 <div class="flex items-center justify-between text-xs text-slate-400">911 <span>{{ filter.name }}</span>912 <span>Last run {{ filter.lastRun }}</span>913 </div>914 <div class="text-xs font-mono text-brand-200">{{ filter.expression }}</div>915 </li>916 </ul>917 </div>918 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">919 <div class="text-xs uppercase tracking-widest text-slate-500">API Keys</div>920 <div class="border border-slate-800 rounded-lg overflow-hidden">921 <table class="min-w-full text-xs">922 <thead class="bg-slate-950/70 text-slate-400 uppercase tracking-widest">923 <tr>924 <th class="px-3 py-2 text-left">Name</th>925 <th class="px-3 py-2 text-left">Key</th>926 <th class="px-3 py-2 text-left">Created</th>927 </tr>928 </thead>929 <tbody class="divide-y divide-slate-800">930 <tr v-for="apiKey in activeLogger.api_keys" :key="apiKey.key" class="hover:bg-slate-900/40">931 <td class="px-3 py-2 text-slate-300">{{ apiKey.name }}</td>932 <td class="px-3 py-2 font-mono text-slate-400">{{ apiKey.key }}</td>933 <td class="px-3 py-2 text-slate-500">{{ apiKey.creation_date }}</td>934 </tr>935 </tbody>936 </table>937 </div>938 </div>939 </div>940 </div>941 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-4">942 <div class="flex items-center justify-between">943 <div>944 <div class="text-sm font-semibold">Live tail</div>945 <div class="text-xs text-slate-500">Ingest via POST /v1/loggers/{{ activeLogger.id }}/ingest</div>946 </div>947 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Pause</button>948 </div>949 <div class="bg-slate-950/40 border border-slate-800 rounded-xl divide-y divide-slate-900/70 text-xs font-mono">950 <div v-for="entry in activeLogger.recent" :key="entry.timestamp" class="px-4 py-2 flex items-start gap-3">951 <span class="px-2 py-0.5 rounded border border-slate-800" :class="entry.level === 'ERROR' ? 'text-rose-300 border-rose-400/40' : entry.level === 'WARN' ? 'text-amber-300 border-amber-400/40' : 'text-emerald-300 border-emerald-400/40'">{{ entry.level }}</span>952 <div>953 <div class="text-slate-400">{{ entry.timestamp }} · {{ entry.source }}</div>954 <div class="text-slate-200">{{ entry.message }}</div>955 </div>956 </div>957 </div>958 </div>959 </div>960 </div>961 </section>962 963 <section v-if="activeService === 'lambda'" class="space-y-6">964 <div class="flex items-center justify-between">965 <div>966 <h1 class="text-2xl font-semibold">Lambda</h1>967 <p class="text-sm text-slate-400">Manage serverless functions with /api/v0/lambdas and /api/v0/run/{lambda_id}.</p>968 </div>969 <div class="flex gap-2">970 <button class="flex items-center gap-2 border border-brand-500/40 bg-brand-500/10 text-brand-200 px-4 py-2 rounded-md text-sm">➕ Deploy function</button>971 <button class="flex items-center gap-2 border border-slate-800 rounded-md px-4 py-2 text-sm hover:border-brand-500/40 hover:text-brand-300">View execution plans</button>972 </div>973 </div>974 <div class="grid grid-cols-1 xl:grid-cols-4 gap-6">975 <div class="xl:col-span-1 bg-slate-900/60 border border-slate-800 rounded-2xl overflow-hidden">976 <div class="px-5 py-4 border-b border-slate-800 text-xs uppercase tracking-widest text-slate-500">Functions</div>977 <div class="divide-y divide-slate-800">978 <button979 v-for="lambda in projectLambdas"980 :key="lambda.id"981 @click="selectedLambdaId = lambda.id"982 class="w-full text-left px-5 py-4 hover:bg-slate-900/40 transition"983 :class="activeLambda && activeLambda.id === lambda.id ? 'bg-slate-900/70 border-l-2 border-brand-400/60' : ''"984 >985 <div class="font-semibold text-slate-200">{{ lambda.name }}</div>986 <div class="text-xs text-slate-500">{{ lambda.path }} · {{ lambda.method }}</div>987 <div class="mt-2 text-[0.65rem] uppercase tracking-widest text-slate-500">{{ lambda.language }} · {{ formatUnix(lambda.created_timestamp) }}</div>988 </button>989 <div v-if="projectLambdas.length === 0" class="px-5 py-12 text-center text-sm text-slate-500">No functions. POST /api/v0/lambdas to create.</div>990 </div>991 </div>992 <div class="xl:col-span-3 space-y-6" v-if="activeLambda">993 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-5">994 <div class="flex items-center justify-between">995 <div>996 <h2 class="text-xl font-semibold">{{ activeLambda.name }}</h2>997 <p class="text-sm text-slate-400">{{ activeLambda.method }} {{ activeLambda.path }} · Runtime {{ activeLambda.language }}</p>998 </div>999 <div class="flex gap-2">1000 <button class="border border-slate-800 rounded-md px-3 py-2 text-xs uppercase tracking-widest hover:border-brand-500/40 hover:text-brand-300">Publish version</button>1001 <button class="border border-rose-500/40 text-rose-300 bg-rose-500/10 rounded-md px-3 py-2 text-xs uppercase tracking-widest">Disable</button>1002 </div>1003 </div>1004 <div class="grid md:grid-cols-4 gap-4 text-sm">1005 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">1006 <div class="text-xs uppercase tracking-widest text-slate-500">Invocations</div>1007 <div class="text-xl font-semibold">{{ activeLambda.metrics.invocations }}</div>1008 <div class="text-xs text-slate-500">Last 24h</div>1009 </div>1010 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">1011 <div class="text-xs uppercase tracking-widest text-slate-500">Success rate</div>1012 <div class="text-xl font-semibold">{{ activeLambda.metrics.successRate }}</div>1013 <div class="text-xs text-slate-500">{{ activeLambda.metrics.failures }} failures</div>1014 </div>1015 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">1016 <div class="text-xs uppercase tracking-widest text-slate-500">Latency</div>1017 <div class="text-xl font-semibold">{{ activeLambda.metrics.p95 }}</div>1018 <div class="text-xs text-slate-500">Avg {{ activeLambda.metrics.avg }}</div>1019 </div>1020 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">1021 <div class="text-xs uppercase tracking-widest text-slate-500">Bindings</div>1022 <div class="text-xl font-semibold">{{ activeLambda.bindings.length }}</div>1023 <div class="text-xs text-slate-500">Queues & routers</div>1024 </div>1025 </div>1026 <div class="flex flex-wrap items-center gap-3 mb-4">1027 <button1028 v-for="tab in lambdaTabs"1029 :key="tab.id"1030 @click="lambdaTab = tab.id"1031 class="px-4 py-2 rounded-md text-xs uppercase tracking-widest border"1032 :class="lambdaTab === tab.id ? 'border-brand-500/40 bg-brand-500/10 text-brand-200' : 'border-slate-800 text-slate-400 hover:text-slate-200'"1033 >1034 {{ tab.label }}1035 </button>1036 <span class="text-xs text-slate-500">Endpoints: {{ lambdaTabSummary }}</span>1037 </div>1038 <div v-if="lambdaTab === 'overview'" class="grid md:grid-cols-2 gap-4 text-sm">1039 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">1040 <div class="text-xs uppercase tracking-widest text-slate-500">Bindings</div>1041 <ul class="space-y-2">1042 <li v-for="binding in activeLambda.bindings" :key="binding.name" class="flex items-center justify-between bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2">1043 <div>1044 <div class="text-slate-200">{{ binding.name }}</div>1045 <div class="text-xs text-slate-500">{{ binding.type }}</div>1046 </div>1047 <span class="text-xs text-slate-500">{{ binding.detail }}</span>1048 </li>1049 </ul>1050 </div>1051 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">1052 <div class="text-xs uppercase tracking-widest text-slate-500">Triggers</div>1053 <ul class="space-y-2">1054 <li v-for="trigger in activeLambda.triggers" :key="trigger" class="bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-xs text-slate-300">{{ trigger }}</li>1055 </ul>1056 </div>1057 </div>1058 <div v-if="lambdaTab === 'code'" class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 text-xs text-slate-300 font-mono overflow-auto">1059 <pre class="whitespace-pre">{{ activeLambda.code }}</pre>1060 </div>1061 <div v-if="lambdaTab === 'test'" class="grid md:grid-cols-2 gap-4 text-sm">1062 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">1063 <div class="text-xs uppercase tracking-widest text-slate-500">Payload</div>1064 <textarea class="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 font-mono text-xs text-slate-300" rows="8">{{ activeLambda.testPayload }}</textarea>1065 <button class="w-full text-xs uppercase tracking-widest border border-brand-500/40 bg-brand-500/10 text-brand-200 rounded-md py-2">Run POST /api/v0/run/{{ activeLambda.id }}</button>1066 </div>1067 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">1068 <div class="text-xs uppercase tracking-widest text-slate-500">Result preview</div>1069 <pre class="whitespace-pre-wrap font-mono text-xs text-slate-300">{{ activeLambda.testResult }}</pre>1070 </div>1071 </div>1072 <div v-if="lambdaTab === 'versions'" class="space-y-3 text-sm">1073 <div class="flex items-center justify-between">1074 <div class="text-xs uppercase tracking-widest text-slate-500">Versions</div>1075 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Promote</button>1076 </div>1077 <div class="border border-slate-800 rounded-xl overflow-hidden">1078 <table class="min-w-full text-xs">1079 <thead class="bg-slate-950/70 text-slate-400 uppercase tracking-widest">1080 <tr>1081 <th class="px-3 py-2 text-left">Version</th>1082 <th class="px-3 py-2 text-left">Published</th>1083 <th class="px-3 py-2 text-left">Checksum</th>1084 <th class="px-3 py-2 text-left">Notes</th>1085 </tr>1086 </thead>1087 <tbody class="divide-y divide-slate-800">1088 <tr v-for="version in activeLambda.versions" :key="version.id" class="hover:bg-slate-900/40">1089 <td class="px-3 py-2 text-slate-300">{{ version.id }}</td>1090 <td class="px-3 py-2 text-slate-500">{{ version.published }}</td>1091 <td class="px-3 py-2 text-slate-500">{{ version.hash }}</td>1092 <td class="px-3 py-2 text-slate-400">{{ version.notes }}</td>1093 </tr>1094 </tbody>1095 </table>1096 </div>1097 </div>1098 </div>1099 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-4">1100 <div class="flex items-center justify-between">1101 <div>1102 <div class="text-sm font-semibold">Integration canvas</div>1103 <div class="text-xs text-slate-500">Link lambda to queues, routers and configs.</div>1104 </div>1105 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Add binding</button>1106 </div>1107 <div class="grid md:grid-cols-3 gap-4 text-xs text-slate-400">1108 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-2">1109 <div class="text-sm font-semibold text-slate-200">Config</div>1110 <p>Reads from {{ activeLambda.integration.config }}</p>1111 </div>1112 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-2">1113 <div class="text-sm font-semibold text-slate-200">Queue trigger</div>1114 <p>{{ activeLambda.integration.queue }}</p>1115 </div>1116 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-2">1117 <div class="text-sm font-semibold text-slate-200">Edge route</div>1118 <p>{{ activeLambda.integration.route }}</p>1119 </div>1120 </div>1121 </div>1122 </div>1123 </div>1124 </section>1125 1126 <section v-if="activeService === 'tailon'" class="space-y-6">1127 <div class="flex items-center justify-between">1128 <div>1129 <h1 class="text-2xl font-semibold">Tailon</h1>1130 <p class="text-sm text-slate-400">Queue management via /v1/queues, /v1/queues/{queue_id}:read and :write.</p>1131 </div>1132 <div class="flex gap-2">1133 <button class="flex items-center gap-2 border border-brand-500/40 bg-brand-500/10 text-brand-200 px-4 py-2 rounded-md text-sm">➕ Create queue</button>1134 <button class="flex items-center gap-2 border border-slate-800 rounded-md px-4 py-2 text-sm hover:border-brand-500/40 hover:text-brand-300">Connect client</button>1135 </div>1136 </div>1137 <div class="grid grid-cols-1 xl:grid-cols-4 gap-6">1138 <div class="xl:col-span-1 bg-slate-900/60 border border-slate-800 rounded-2xl overflow-hidden">1139 <div class="px-5 py-4 border-b border-slate-800 text-xs uppercase tracking-widest text-slate-500">Queues</div>1140 <div class="divide-y divide-slate-800">1141 <button1142 v-for="queue in projectQueues"1143 :key="queue.id"1144 @click="selectedQueueId = queue.id"1145 class="w-full text-left px-5 py-4 hover:bg-slate-900/40 transition"1146 :class="activeQueue && activeQueue.id === queue.id ? 'bg-slate-900/70 border-l-2 border-brand-400/60' : ''"1147 >1148 <div class="font-semibold text-slate-200">{{ queue.name }}</div>1149 <div class="text-xs text-slate-500">{{ queue.id }}</div>1150 <div class="mt-2 text-[0.65rem] uppercase tracking-widest text-slate-500">Ready {{ queue.metrics.ready }} · Inflight {{ queue.metrics.inflight }}</div>1151 </button>1152 <div v-if="projectQueues.length === 0" class="px-5 py-12 text-center text-sm text-slate-500">No queues yet. Use POST /v1/queues.</div>1153 </div>1154 </div>1155 <div class="xl:col-span-3 space-y-6" v-if="activeQueue">1156 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-5">1157 <div class="flex items-center justify-between">1158 <div>1159 <h2 class="text-xl font-semibold">{{ activeQueue.name }}</h2>1160 <p class="text-sm text-slate-400">Ready {{ activeQueue.metrics.ready }} · Inflight {{ activeQueue.metrics.inflight }} · Dead {{ activeQueue.metrics.dead }}</p>1161 </div>1162 <div class="flex gap-2">1163 <button class="border border-slate-800 rounded-md px-3 py-2 text-xs uppercase tracking-widest hover:border-brand-500/40 hover:text-brand-300">Purge</button>1164 <button class="border border-rose-500/40 text-rose-300 bg-rose-500/10 rounded-md px-3 py-2 text-xs uppercase tracking-widest">Delete</button>1165 </div>1166 </div>1167 <div class="grid md:grid-cols-4 gap-4 text-sm">1168 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">1169 <div class="text-xs uppercase tracking-widest text-slate-500">Throughput</div>1170 <div class="text-xl font-semibold">{{ activeQueue.metrics.rateIn }}/s in</div>1171 <div class="text-xs text-slate-500">{{ activeQueue.metrics.rateOut }}/s out</div>1172 </div>1173 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">1174 <div class="text-xs uppercase tracking-widest text-slate-500">Visibility</div>1175 <div class="text-xl font-semibold">{{ activeQueue.metrics.visibility }}s</div>1176 <div class="text-xs text-slate-500">Timeout</div>1177 </div>1178 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">1179 <div class="text-xs uppercase tracking-widest text-slate-500">DLQ</div>1180 <div class="text-xl font-semibold">{{ activeQueue.deadLetter }}</div>1181 <div class="text-xs text-slate-500">Linked queue</div>1182 </div>1183 <div class="bg-slate-950/50 border border-slate-800 rounded-xl p-4">1184 <div class="text-xs uppercase tracking-widest text-slate-500">Clients</div>1185 <div class="text-xl font-semibold">{{ activeQueue.clients.length }}</div>1186 <div class="text-xs text-slate-500">/v1/clients</div>1187 </div>1188 </div>1189 <div class="flex flex-wrap items-center gap-3 mb-4">1190 <button1191 v-for="tab in queueTabs"1192 :key="tab.id"1193 @click="queueTab = tab.id"1194 class="px-4 py-2 rounded-md text-xs uppercase tracking-widest border"1195 :class="queueTab === tab.id ? 'border-brand-500/40 bg-brand-500/10 text-brand-200' : 'border-slate-800 text-slate-400 hover:text-slate-200'"1196 >1197 {{ tab.label }}1198 </button>1199 <span class="text-xs text-slate-500">Endpoint: {{ queueTabSummary }}</span>1200 </div>1201 <div v-if="queueTab === 'overview'" class="grid md:grid-cols-2 gap-4 text-sm">1202 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">1203 <div class="text-xs uppercase tracking-widest text-slate-500">Message schema</div>1204 <pre class="whitespace-pre-wrap text-xs text-slate-300 font-mono">{{ JSON.stringify(activeQueue.sampleMessage, null, 2) }}</pre>1205 </div>1206 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">1207 <div class="text-xs uppercase tracking-widest text-slate-500">Publishing</div>1208 <p>Use POST /v1/queues/{{ activeQueue.id }}:write to enqueue messages.</p>1209 <div class="bg-slate-900/70 border border-slate-800 rounded-lg p-4 font-mono text-xs text-slate-400">1210 tailon write {{ activeQueue.id }} < payload.json1211 </div>1212 </div>1213 </div>1214 <div v-if="queueTab === 'consumers'" class="space-y-3 text-sm">1215 <div class="flex items-center justify-between">1216 <div class="text-xs uppercase tracking-widest text-slate-500">Consumers</div>1217 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Register client</button>1218 </div>1219 <div class="border border-slate-800 rounded-xl overflow-hidden">1220 <table class="min-w-full text-xs">1221 <thead class="bg-slate-950/70 text-slate-400 uppercase tracking-widest">1222 <tr>1223 <th class="px-3 py-2 text-left">Client</th>1224 <th class="px-3 py-2 text-left">Last seen</th>1225 <th class="px-3 py-2 text-left">Lag</th>1226 <th class="px-3 py-2 text-left">Ack rate</th>1227 </tr>1228 </thead>1229 <tbody class="divide-y divide-slate-800">1230 <tr v-for="client in activeQueue.clients" :key="client.id" class="hover:bg-slate-900/40">1231 <td class="px-3 py-2 text-slate-300">{{ client.name }}</td>1232 <td class="px-3 py-2 text-slate-500">{{ client.lastSeen }}</td>1233 <td class="px-3 py-2 text-slate-500">{{ client.lag }}</td>1234 <td class="px-3 py-2 text-slate-500">{{ client.ackRate }}</td>1235 </tr>1236 </tbody>1237 </table>1238 </div>1239 </div>1240 <div v-if="queueTab === 'messages'" class="grid md:grid-cols-2 gap-4 text-sm">1241 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">1242 <div class="text-xs uppercase tracking-widest text-slate-500">Pending messages</div>1243 <ul class="space-y-2">1244 <li v-for="message in activeQueue.pending" :key="message.id" class="bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-xs">1245 <div class="text-slate-400">{{ message.id }} · {{ message.enqueued }}</div>1246 <div class="text-slate-200">{{ message.body }}</div>1247 </li>1248 </ul>1249 </div>1250 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-3">1251 <div class="text-xs uppercase tracking-widest text-slate-500">Acknowledge</div>1252 <p>Use POST /v1/queues/{{ activeQueue.id }}:read to pull messages, then POST :write to requeue.</p>1253 <button class="w-full text-xs uppercase tracking-widest border border-brand-500/40 bg-brand-500/10 text-brand-200 rounded-md py-2">Simulate consumer</button>1254 </div>1255 </div>1256 </div>1257 <div class="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-4">1258 <div class="flex items-center justify-between">1259 <div>1260 <div class="text-sm font-semibold">Queue topology</div>1261 <div class="text-xs text-slate-500">Shows producers and consumers across projects.</div>1262 </div>1263 <button class="text-xs uppercase tracking-widest border border-slate-800 rounded-md px-3 py-1.5 hover:border-brand-500/40 hover:text-brand-300">Export</button>1264 </div>1265 <div class="grid md:grid-cols-3 gap-4 text-xs text-slate-400">1266 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-2">1267 <div class="text-sm font-semibold text-slate-200">Producers</div>1268 <ul class="space-y-1">1269 <li v-for="producer in activeQueue.producers" :key="producer" class="bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-1">{{ producer }}</li>1270 </ul>1271 </div>1272 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-2">1273 <div class="text-sm font-semibold text-slate-200">Consumers</div>1274 <ul class="space-y-1">1275 <li v-for="consumer in activeQueue.consumerGroups" :key="consumer" class="bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-1">{{ consumer }}</li>1276 </ul>1277 </div>1278 <div class="bg-slate-950/40 border border-slate-800 rounded-xl p-4 space-y-2">1279 <div class="text-sm font-semibold text-slate-200">Routing</div>1280 <p>Linked to project {{ selectedProject.name }} edge router for fan-out.</p>1281 </div>1282 </div>1283 </div>1284 </div>1285 </div>1286 </section>1287 </main>1288 <section class="border-t border-slate-900/70 bg-slate-950/80">1289 <div class="px-8 py-4 flex items-center justify-between">1290 <div>1291 <div class="text-sm font-semibold text-slate-200">Live operations feed</div>1292 <div class="text-xs text-slate-500">Aggregated from InstantLogs & Tailon consumers</div>1293 </div>1294 <div class="flex items-center gap-3 text-xs text-slate-500">1295 <span>Auto-scroll</span>1296 <label class="relative inline-flex items-center cursor-pointer">1297 <input type="checkbox" checked class="sr-only peer" />1298 <div class="w-10 h-5 bg-slate-800 rounded-full peer peer-checked:bg-brand-500/60"></div>1299 </label>1300 </div>1301 </div>1302 <div class="px-8 pb-5 overflow-x-hidden">1303 <div class="bg-slate-950/60 border border-slate-900 rounded-2xl p-4 font-mono text-xs text-slate-400 max-h-40 overflow-y-auto space-y-2">1304 <div v-for="entry in globalActivity" :key="entry.timestamp" class="flex items-start gap-3">1305 <span class="px-2 py-0.5 rounded border border-slate-800" :class="entry.type === 'deploy' ? 'text-brand-300 border-brand-500/30' : entry.type === 'alert' ? 'text-amber-300 border-amber-400/40' : 'text-slate-300 border-slate-700'">{{ entry.type.toUpperCase() }}</span>1306 <div>1307 <div class="text-slate-300">{{ entry.message }}</div>1308 <div class="text-slate-500">{{ entry.timestamp }} · {{ entry.actor }}</div>1309 </div>1310 </div>1311 </div>1312 </div>1313 </section>1314 </div>1315 </div>1316 </div>1317 <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>1318 <script>1319 const { createApp, computed } = Vue;1320 createApp({1321 data() {1322 return {1323 services: [1324 { id: 'overview', name: 'Overview', subtitle: 'Project pulse', icon: '✨' },1325 { id: 'projects', name: 'Projects', subtitle: 'Routing & auth', icon: '🗂️' },1326 { id: 'config', name: 'Config', subtitle: 'Key/Value sets', icon: '⚙️' },1327 { id: 'files', name: 'Files', subtitle: 'Object storage', icon: '📦' },1328 { id: 'inceptiondb', name: 'InceptionDB', subtitle: 'Document DB', icon: '🗄️' },1329 { id: 'instantlogs', name: 'InstantLogs', subtitle: 'Realtime logs', icon: '📈' },1330 { id: 'lambda', name: 'Lambda', subtitle: 'Functions', icon: 'λ' },1331 { id: 'tailon', name: 'Tailon', subtitle: 'Queues', icon: '🛰️' },1332 ],1333 activeService: 'overview',1334 projects: [1335 {1336 id: 'proj-analytics',1337 name: 'Analytics Platform',1338 owners: ['ana@hola.cloud', 'ben@hola.cloud'],1339 create_timestamp: 1675209600,1340 update_timestamp: 1704076800,1341 auth: { enabled: true },1342 routers: [1343 {1344 type: 'edge',1345 hosts: [1346 { name: 'analytics.hola.cloud', creation_timestamp: 1669852800, verified: true },1347 { name: 'analytics.internal', creation_timestamp: 1677801600, verified: false },1348 ],1349 HeadersIn: { 'x-trace-id': 'propagate', 'x-project-id': 'proj-analytics' },1350 HeadersOut: { 'cache-control': 'no-store', 'x-runtime': 'lambda' },1351 config: { upstream: 'https://edge.analytics.svc', cors: 'enabled' },1352 },1353 {1354 type: 'admin',1355 hosts: [1356 { name: 'admin.analytics.hola.cloud', creation_timestamp: 1672617600, verified: true },1357 ],1358 HeadersIn: { authorization: 'jwt', 'x-tenant': 'analytics' },1359 HeadersOut: { 'strict-transport-security': 'max-age=63072000' },1360 config: { upstream: 'https://admin.analytics.svc', cors: 'internal-only' },1361 },1362 ],1363 status: 'healthy',1364 tags: ['prod', 'edge', 'data'],1365 timeline: [1366 { time: '2024-01-08 09:42', title: 'Segment lambda deployed', actor: 'CI Pipeline', type: 'deploy' },1367 { time: '2024-01-08 08:15', title: 'New InceptionDB index', actor: 'ana@hola.cloud', type: 'change' },1368 { time: '2024-01-07 22:04', title: 'Alert resolved: queue lag', actor: 'oncall', type: 'alert' },1369 ],1370 },1371 {1372 id: 'proj-commerce',1373 name: 'Commerce Experience',1374 owners: ['carla@hola.cloud', 'dave@hola.cloud'],1375 create_timestamp: 1661990400,1376 update_timestamp: 1703986800,1377 auth: { enabled: false },1378 routers: [1379 {1380 type: 'edge',1381 hosts: [1382 { name: 'shop.hola.cloud', creation_timestamp: 1664582400, verified: true },1383 ],1384 HeadersIn: { 'x-request-id': 'forward' },1385 HeadersOut: { 'cache-control': 'max-age=60' },1386 config: { upstream: 'https://commerce.api', cors: 'enabled' },1387 },1388 ],1389 status: 'degraded',1390 tags: ['staging', 'priority'],1391 timeline: [1392 { time: '2024-01-06 17:30', title: 'Checkout logger filters updated', actor: 'dave@hola.cloud', type: 'change' },1393 { time: '2024-01-05 11:12', title: 'Assets bucket lifecycle edited', actor: 'carla@hola.cloud', type: 'change' },1394 ],1395 },1396 {1397 id: 'proj-sandbox',1398 name: 'Sandbox',1399 owners: ['labs@hola.cloud'],1400 create_timestamp: 1688160000,1401 update_timestamp: 1703814000,1402 auth: { enabled: true },1403 routers: [1404 {1405 type: 'edge',1406 hosts: [1407 { name: 'sandbox.hola.cloud', creation_timestamp: 1685577600, verified: false },1408 ],1409 HeadersIn: { 'x-experiment': 'beta' },1410 HeadersOut: { 'cache-control': 'no-store' },1411 config: { upstream: 'https://sandbox.api', cors: 'enabled' },1412 },1413 ],1414 status: 'healthy',1415 tags: ['testing'],1416 timeline: [1417 { time: '2024-01-03 13:00', title: 'Feature flag toggled', actor: 'labs@hola.cloud', type: 'change' },1418 ],1419 },1420 ],1421 selectedProjectId: 'proj-analytics',1422 openApiSummaries: [1423 {1424 id: 'projects',1425 name: 'Projects API',1426 description: 'Provision and update projects, routers and auth.',1427 version: 'v0',1428 keyEndpoints: [1429 { method: 'GET', path: '/v0/projects', note: 'List projects' },1430 { method: 'POST', path: '/v0/projects', note: 'Create project' },1431 { method: 'GET', path: '/v0/projects/{project_id}', note: 'Inspect project detail' },1432 ],1433 },1434 {1435 id: 'config',1436 name: 'Config API',1437 description: 'Configuration registry for key/value sets.',1438 version: 'v0',1439 keyEndpoints: [1440 { method: 'GET', path: '/v0/configs', note: 'List config items' },1441 { method: 'POST', path: '/v0/configs', note: 'Create config' },1442 { method: 'PATCH', path: '/v0/configs/{configId}', note: 'Update fields' },1443 ],1444 },1445 {1446 id: 'files',1447 name: 'Files API',1448 description: 'Buckets, file metadata and uploads.',1449 version: 'v1',1450 keyEndpoints: [1451 { method: 'GET', path: '/v1/buckets', note: 'List buckets' },1452 { method: 'POST', path: '/v1/buckets', note: 'Create bucket' },1453 { method: 'GET', path: '/v1/buckets/{bucket_id}', note: 'Bucket details' },1454 ],1455 },1456 {1457 id: 'inceptiondb',1458 name: 'InceptionDB API',1459 description: 'Database, collections, indexes and keys.',1460 version: 'v1',1461 keyEndpoints: [1462 { method: 'GET', path: '/v1/databases', note: 'List databases' },1463 { method: 'POST', path: '/v1/databases/{databaseId}/collections', note: 'Create collection' },1464 { method: 'POST', path: '/v1/databases/{databaseId}:createApiKey', note: 'Generate API key' },1465 ],1466 },1467 {1468 id: 'instantlogs',1469 name: 'InstantLogs API',1470 description: 'Loggers, API keys, owners and stats.',1471 version: 'v1',1472 keyEndpoints: [1473 { method: 'POST', path: '/v1/loggers', note: 'Create logger' },1474 { method: 'GET', path: '/v1/loggers/{loggerId}', note: 'Describe logger' },1475 { method: 'POST', path: '/v1/loggers/{loggerId}/ingest', note: 'Ingest entries' },1476 ],1477 },1478 {1479 id: 'lambda',1480 name: 'Lambda API',1481 description: 'Functions and on-demand execution.',1482 version: 'v0',1483 keyEndpoints: [1484 { method: 'GET', path: '/api/v0/lambdas', note: 'List functions' },1485 { method: 'POST', path: '/api/v0/lambdas', note: 'Create function' },1486 { method: 'POST', path: '/api/v0/run/{lambda_id}', note: 'Invoke function' },1487 ],1488 },1489 {1490 id: 'tailon',1491 name: 'Tailon API',1492 description: 'Queues, read/write operations and clients.',1493 version: 'v1',1494 keyEndpoints: [1495 { method: 'GET', path: '/v1/queues', note: 'List queues' },1496 { method: 'POST', path: '/v1/queues', note: 'Create queue' },1497 { method: 'POST', path: '/v1/queues/{queue_id}:read', note: 'Consume messages' },1498 ],1499 },1500 ],1501 configSets: [1502 {1503 id: 'cfg-analytics-app',1504 project_id: 'proj-analytics',1505 name: 'app-settings',1506 description: 'Feature flags and UI toggles for the analytics portal.',1507 updated: '2024-01-07 18:20',1508 entries: {1509 'feature.recommendations': 'enabled',1510 'sessions.max': 5,1511 'support.chat': 'beta',1512 },1513 history: [1514 { id: 'v5', label: 'v5 · Added chat toggle', at: '2024-01-07 18:20' },1515 { id: 'v4', label: 'v4 · Raised sessions', at: '2023-12-18 09:00' },1516 { id: 'v3', label: 'v3 · Baseline', at: '2023-11-05 14:30' },1517 ],1518 notes: {1519 'feature.recommendations': 'Backed by segmentation lambda',1520 },1521 },1522 {1523 id: 'cfg-analytics-db',1524 project_id: 'proj-analytics',1525 name: 'db-connections',1526 description: 'Connection strings for data ingestion and read replicas.',1527 updated: '2024-01-06 10:02',1528 entries: {1529 primary: 'postgres://analytics-primary.internal:5432/events',1530 replica: 'postgres://analytics-replica.internal:5432/events',1531 poolSize: 32,1532 },1533 history: [1534 { id: 'v8', label: 'v8 · Rotated password', at: '2024-01-06 10:02' },1535 { id: 'v7', label: 'v7 · Increased pool', at: '2023-12-12 16:45' },1536 ],1537 notes: {1538 primary: 'Managed via Hola Secret Manager',1539 },1540 },1541 {1542 id: 'cfg-commerce-payments',1543 project_id: 'proj-commerce',1544 name: 'payment-provider',1545 description: 'Configuration for third-party payment gateway.',1546 updated: '2024-01-05 12:10',1547 entries: {1548 provider: 'Stripe',1549 apiKey: 'sk_live_****',1550 webhookSecret: 'whsec_****',1551 },1552 history: [1553 { id: 'v3', label: 'v3 · Rotated keys', at: '2024-01-05 12:10' },1554 { id: 'v2', label: 'v2 · Added webhook secret', at: '2023-11-22 08:00' },1555 ],1556 notes: {1557 apiKey: 'Rotate via /v0/configs/{configId} PATCH',1558 },1559 },1560 {1561 id: 'cfg-sandbox-labs',1562 project_id: 'proj-sandbox',1563 name: 'experiments',1564 description: 'Experimental toggles used by labs.',1565 updated: '2024-01-03 09:30',1566 entries: {1567 'beta-search': 'enabled',1568 'ai-insights': 'disabled',1569 },1570 history: [1571 { id: 'v2', label: 'v2 · Enabled beta search', at: '2024-01-03 09:30' },1572 ],1573 notes: {},1574 },1575 ],1576 buckets: [1577 {1578 id: 'bkt-analytics-raw',1579 project_id: 'proj-analytics',1580 name: 'analytics-raw',1581 description: 'Raw ingestion events from web and mobile.',1582 created_timestamp: 1667260800,1583 owners: ['ana@hola.cloud', 'storage@hola.cloud'],1584 },1585 {1586 id: 'bkt-analytics-models',1587 project_id: 'proj-analytics',1588 name: 'analytics-models',1589 description: 'ML artifacts and trained models.',1590 created_timestamp: 1675123200,1591 owners: ['ana@hola.cloud'],1592 },1593 {1594 id: 'bkt-commerce-assets',1595 project_id: 'proj-commerce',1596 name: 'commerce-assets',1597 description: 'Static assets and product media.',1598 created_timestamp: 1669852800,1599 owners: ['carla@hola.cloud'],1600 },1601 {1602 id: 'bkt-sandbox-dumps',1603 project_id: 'proj-sandbox',1604 name: 'sandbox-dumps',1605 description: 'Temporary debug snapshots.',1606 created_timestamp: 1685577600,1607 owners: ['labs@hola.cloud'],1608 },1609 ],1610 files: [1611 {1612 id: 'file-raw-20240108',1613 bucket_id: 'bkt-analytics-raw',1614 name: '2024/01/08/events-09.json.gz',1615 size: 98234342,1616 hash_md5: 'c1a4d2f1',1617 hash_sha256: 'f4a8f2b09d3a',1618 updated_timestamp: 1704706800,1619 status: 'available',1620 },1621 {1622 id: 'file-raw-20240107',1623 bucket_id: 'bkt-analytics-raw',1624 name: '2024/01/07/events-22.json.gz',1625 size: 110234234,1626 hash_md5: 'a23b89cd',1627 hash_sha256: 'cc1ad3f004bd',1628 updated_timestamp: 1704620400,1629 status: 'available',1630 },1631 {1632 id: 'file-model-202312',1633 bucket_id: 'bkt-analytics-models',1634 name: 'models/segments-v3.tar.gz',1635 size: 45234567,1636 hash_md5: 'f0ab23cd',1637 hash_sha256: 'ab0234ffcc89',1638 updated_timestamp: 1704062400,1639 status: 'available',1640 },1641 {1642 id: 'file-assets-banner',1643 bucket_id: 'bkt-commerce-assets',1644 name: 'banners/winter-sale.png',1645 size: 1234024,1646 hash_md5: 'dd1caa90',1647 hash_sha256: '120aa4433aa1',1648 updated_timestamp: 1703896800,1649 status: 'processing',1650 },1651 {1652 id: 'file-sandbox-dump',1653 bucket_id: 'bkt-sandbox-dumps',1654 name: 'debug/2024-01-01.zip',1655 size: 8234234,1656 hash_md5: 'ffeeaa11',1657 hash_sha256: 'ff234aa11bb2',1658 updated_timestamp: 1704080400,1659 status: 'available',1660 },1661 ],1662 databases: [1663 {1664 id: 'db-analytics-events',1665 project_id: 'proj-analytics',1666 name: 'analytics-events',1667 creation_date: '2023-02-01T12:00:00Z',1668 owners: ['ana@hola.cloud', 'dataops@hola.cloud'],1669 usage: {1670 documents: 1280345,1671 storage: '48 GB',1672 indexes: 8,1673 readOps: 230,1674 writeOps: 120,1675 },1676 collections: [1677 {1678 name: 'events',1679 total: 1020345,1680 indexes: [1681 { name: 'by_user', type: 'secondary', fields: ['user_id'] },1682 { name: 'by_timestamp', type: 'ttl', fields: ['timestamp'] },1683 ],1684 defaults: { ttl_days: 30, compression: 'zstd' },1685 },1686 {1687 name: 'sessions',1688 total: 260000,1689 indexes: [1690 { name: 'by_session', type: 'primary', fields: ['session_id'] },1691 ],1692 defaults: { ttl_days: 7 },1693 },1694 ],1695 indexHighlights: [1696 { name: 'events_by_user', type: 'secondary', fields: ['user_id', 'country'] },1697 { name: 'events_recent', type: 'ttl', fields: ['timestamp'] },1698 ],1699 api_keys: [1700 { name: 'pipeline', key: 'ANL-PIPE', creation_date: '2023-02-01T12:05:00Z', secret: '••••••••' },1701 { name: 'dashboards', key: 'ANL-DASH', creation_date: '2023-05-14T10:10:00Z', secret: '••••••••' },1702 ],1703 sampleQueries: {1704 find: { filter: { user_id: '123', timestamp: { $gte: '2024-01-01T00:00:00Z' } }, limit: 25 },1705 insert: { documents: [{ user_id: '123', event: 'page_view', timestamp: '2024-01-08T09:42:00Z' }] },1706 },1707 },1708 {1709 id: 'db-commerce-orders',1710 project_id: 'proj-commerce',1711 name: 'commerce-orders',1712 creation_date: '2022-11-15T09:00:00Z',1713 owners: ['carla@hola.cloud'],1714 usage: {1715 documents: 203450,1716 storage: '12 GB',1717 indexes: 5,1718 readOps: 80,1719 writeOps: 65,1720 },1721 collections: [1722 {1723 name: 'orders',1724 total: 180000,1725 indexes: [1726 { name: 'by_customer', type: 'secondary', fields: ['customer_id'] },1727 ],1728 defaults: { ttl_days: 365 },1729 },1730 {1731 name: 'payments',1732 total: 23450,1733 indexes: [1734 { name: 'by_status', type: 'secondary', fields: ['status'] },1735 ],1736 defaults: { ttl_days: 180 },1737 },1738 ],1739 indexHighlights: [1740 { name: 'orders_by_customer', type: 'secondary', fields: ['customer_id'] },1741 ],1742 api_keys: [1743 { name: 'checkout', key: 'COM-CHK', creation_date: '2022-11-15T09:00:00Z', secret: '••••••••' },1744 ],1745 sampleQueries: {1746 find: { filter: { status: 'processing' }, sort: { created_at: -1 }, limit: 50 },1747 insert: { documents: [{ order_id: 'o-123', status: 'pending', total: 49.5 }] },1748 },1749 },1750 ],1751 loggers: [1752 {1753 id: 'log-analytics-edge',1754 project_id: 'proj-analytics',1755 name: 'Analytics Edge',1756 creation_date: '2023-01-05T12:00:00Z',1757 owners: ['ana@hola.cloud', 'observability@hola.cloud'],1758 usage: {1759 bytes_received: 834523456,1760 bytes_filtered: 20423412,1761 bytes_sent: 62345098,1762 num_ingests: 1349,1763 num_filters: 24,1764 },1765 filters: [1766 { name: 'errors-only', expression: "severity >= 'ERROR'", lastRun: '2m ago' },1767 { name: 'slow-queries', expression: 'duration_ms > 500', lastRun: '5m ago' },1768 ],1769 api_keys: [1770 { name: 'edge-browser', key: 'ANL-EDGE-1', creation_date: '2023-01-05T12:05:00Z' },1771 { name: 'edge-workers', key: 'ANL-EDGE-2', creation_date: '2023-06-12T08:00:00Z' },1772 ],1773 recent: [1774 { timestamp: '2024-01-08T09:42:12Z', level: 'INFO', message: 'Processed 524 events batch', source: 'ingest-worker' },1775 { timestamp: '2024-01-08T09:41:55Z', level: 'WARN', message: 'Queue lag at 1200ms', source: 'queue-monitor' },1776 { timestamp: '2024-01-08T09:41:20Z', level: 'ERROR', message: 'Retrying lambda segmentation run', source: 'lambda-runner' },1777 ],1778 },1779 {1780 id: 'log-commerce-checkout',1781 project_id: 'proj-commerce',1782 name: 'Checkout',1783 creation_date: '2022-10-20T10:00:00Z',1784 owners: ['dave@hola.cloud'],1785 usage: {1786 bytes_received: 20345023,1787 bytes_filtered: 234500,1788 bytes_sent: 9234500,1789 num_ingests: 430,1790 num_filters: 6,1791 },1792 filters: [1793 { name: 'payment-errors', expression: "message CONTAINS 'payment_error'", lastRun: '12m ago' },1794 ],1795 api_keys: [1796 { name: 'checkout-edge', key: 'COM-EDGE', creation_date: '2022-10-20T10:05:00Z' },1797 ],1798 recent: [1799 { timestamp: '2024-01-07T22:00:00Z', level: 'INFO', message: 'Checkout page served', source: 'web' },1800 ],1801 },1802 ],1803 lambdas: [1804 {1805 id: 'lam-segmentation',1806 project_id: 'proj-analytics',1807 name: 'segmentation-enricher',1808 path: '/analytics/segment',1809 method: 'POST',1810 language: 'python3.11',1811 owner: 'ana@hola.cloud',1812 created_timestamp: 1685577600,1813 code: `import json1814 1815 def handler(event, context):1816 user = event.get("user_id")1817 traits = lookup_traits(user)1818 return {1819 "statusCode": 200,1820 "body": json.dumps({"user": user, "traits": traits})1821 }`,1822 metrics: {1823 invocations: '342',1824 successRate: '99.3%',1825 failures: 2,1826 p95: '180 ms',1827 avg: '120 ms',1828 },1829 bindings: [1830 { name: 'analytics-router', type: 'Edge route', detail: '/segment' },1831 { name: 'segmentation-queue', type: 'Queue trigger', detail: 'tailon:queue-segmentation' },1832 ],1833 triggers: ['Tailon queue segmentation-jobs', 'Edge route /analytics/segment'],1834 versions: [1835 { id: 'v6', published: '2024-01-07 09:30', hash: 'sha256:12ab', notes: 'Geo enrichment improvements' },1836 { id: 'v5', published: '2023-12-10 14:20', hash: 'sha256:08ff', notes: 'Retry policy update' },1837 ],1838 testPayload: '{"user_id": "user-123", "country": "ES"}',1839 testResult: '{"statusCode": 200, "body": "{\"segment\": \"loyal\"}"}',1840 integration: {1841 config: 'cfg-analytics-app',1842 queue: 'tailon::queue-segmentation',1843 route: 'projects/proj-analytics/routers/edge',1844 },1845 },1846 {1847 id: 'lam-checkout-webhook',1848 project_id: 'proj-commerce',1849 name: 'checkout-webhook',1850 path: '/commerce/webhook',1851 method: 'POST',1852 language: 'node18',1853 owner: 'dave@hola.cloud',1854 created_timestamp: 1669852800,1855 code: `exports.handler = async (event) => {1856 const body = JSON.parse(event.body || '{}');1857 console.log('Webhook received', body.type);1858 return { statusCode: 200, body: JSON.stringify({ ok: true }) };1859 };`,1860 metrics: {1861 invocations: '1.2k',1862 successRate: '97.8%',1863 failures: 12,1864 p95: '240 ms',1865 avg: '180 ms',1866 },1867 bindings: [1868 { name: 'checkout-router', type: 'Edge route', detail: '/payments/webhook' },1869 ],1870 triggers: ['Edge route /commerce/webhook'],1871 versions: [1872 { id: 'v12', published: '2024-01-05 12:00', hash: 'sha256:aa12', notes: 'Handle new payment states' },1873 { id: 'v11', published: '2023-12-01 08:10', hash: 'sha256:bb45', notes: 'Improve logging' },1874 ],1875 testPayload: '{"type": "payment_intent.succeeded"}',1876 testResult: '{"statusCode": 200, "body": "{\"ok\":true}"}',1877 integration: {1878 config: 'cfg-commerce-payments',1879 queue: 'tailon::queue-checkout-webhooks',1880 route: 'projects/proj-commerce/routers/edge',1881 },1882 },1883 ],1884 queues: [1885 {1886 id: 'queue-segmentation',1887 project_id: 'proj-analytics',1888 name: 'segmentation-jobs',1889 metrics: {1890 ready: 245,1891 inflight: 12,1892 dead: 1,1893 rateIn: 42,1894 rateOut: 40,1895 visibility: 45,1896 },1897 deadLetter: 'queue-segmentation-dlq',1898 clients: [1899 { id: 'client-seg-1', name: 'segments-worker-1', lastSeen: '15s ago', lag: '2 msg', ackRate: '98%' },1900 { id: 'client-seg-2', name: 'segments-worker-2', lastSeen: '25s ago', lag: '4 msg', ackRate: '96%' },1901 ],1902 producers: ['lambda::ingest-edge', 'instantlogs::analytics-edge'],1903 consumerGroups: ['segmentation-workers', 'analytics-lab'],1904 sampleMessage: { type: 'segment-request', payload: { user_id: 'user-123', country: 'ES' } },1905 pending: [1906 { id: 'msg-001', body: '{"user_id":"user-123"}', enqueued: '2024-01-08 09:41' },1907 { id: 'msg-002', body: '{"user_id":"user-456"}', enqueued: '2024-01-08 09:40' },1908 ],1909 },1910 {1911 id: 'queue-checkout-webhooks',1912 project_id: 'proj-commerce',1913 name: 'checkout-webhooks',1914 metrics: {1915 ready: 12,1916 inflight: 2,1917 dead: 0,1918 rateIn: 4,1919 rateOut: 4,1920 visibility: 60,1921 },1922 deadLetter: 'queue-checkout-dlq',1923 clients: [1924 { id: 'client-checkout-1', name: 'checkout-worker', lastSeen: '1m ago', lag: '0 msg', ackRate: '99%' },1925 ],1926 producers: ['lambda::checkout-webhook'],1927 consumerGroups: ['checkout-worker'],1928 sampleMessage: { type: 'payment-event', payload: { order_id: 'o-123', status: 'pending' } },1929 pending: [1930 { id: 'msg-cc-001', body: '{"order_id":"o-123"}', enqueued: '2024-01-07 21:55' },1931 ],1932 },1933 ],1934 selectedConfigId: null,1935 selectedBucketId: null,1936 selectedDatabaseId: null,1937 selectedLoggerId: null,1938 selectedLambdaId: null,1939 selectedQueueId: null,1940 databaseTabs: [1941 { id: 'overview', label: 'Overview' },1942 { id: 'collections', label: 'Collections' },1943 { id: 'apiKeys', label: 'API keys' },1944 { id: 'owners', label: 'Owners' },1945 ],1946 databaseTab: 'overview',1947 logRanges: [1948 { id: '15m', label: '15m' },1949 { id: '1h', label: '1h' },1950 { id: '24h', label: '24h' },1951 ],1952 loggerRange: '15m',1953 lambdaTabs: [1954 { id: 'overview', label: 'Overview' },1955 { id: 'code', label: 'Code' },1956 { id: 'test', label: 'Test runner' },1957 { id: 'versions', label: 'Versions' },1958 ],1959 lambdaTab: 'overview',1960 queueTabs: [1961 { id: 'overview', label: 'Overview' },1962 { id: 'consumers', label: 'Consumers' },1963 { id: 'messages', label: 'Messages' },1964 ],1965 queueTab: 'overview',1966 lambdaTabSummary: '',1967 databaseTabSummary: '',1968 queueTabSummary: '',1969 globalActivity: [1970 { timestamp: '09:42:20', type: 'deploy', message: 'Lambda segmentation-enricher promoted to v6', actor: 'CI Pipeline' },1971 { timestamp: '09:41:55', type: 'alert', message: 'Tailon queue segmentation-jobs lag at 15s', actor: 'Queue monitor' },1972 { timestamp: '09:40:33', type: 'info', message: 'InstantLogs Analytics Edge ingest 524 records', actor: 'Log processor' },1973 ],1974 };1975 },1976 computed: {1977 selectedProject() {1978 const fallback = this.projects[0] || null;1979 return this.projects.find((project) => project.id === this.selectedProjectId) || fallback;1980 },1981 projectConfigs() {1982 return this.configSets.filter((config) => config.project_id === this.selectedProjectId);1983 },1984 activeConfig() {1985 if (!this.projectConfigs.length) {1986 return null;1987 }1988 const explicit = this.projectConfigs.find((config) => config.id === this.selectedConfigId);1989 return explicit || this.projectConfigs[0];1990 },1991 configEntries() {1992 if (!this.activeConfig) return [];1993 return Object.entries(this.activeConfig.entries).map(([key, value]) => ({1994 key,1995 value,1996 note: this.activeConfig.notes?.[key] || '—',1997 }));1998 },1999 projectBuckets() {2000 return this.buckets.filter((bucket) => bucket.project_id === this.selectedProjectId);2001 },2002 activeBucket() {2003 if (!this.projectBuckets.length) return null;2004 const explicit = this.projectBuckets.find((bucket) => bucket.id === this.selectedBucketId);2005 return explicit || this.projectBuckets[0];2006 },2007 bucketFiles() {2008 if (!this.activeBucket) return [];2009 return this.files.filter((file) => file.bucket_id === this.activeBucket.id);2010 },2011 bucketMetrics() {2012 if (!this.activeBucket) {2013 return { objectCount: 0, totalSize: '0 B', storageClass: 'standard', versioning: 'disabled', lastModified: '—' };2014 }2015 const files = this.bucketFiles;2016 const totalSize = files.reduce((sum, file) => sum + (file.size || 0), 0);2017 const latest = files.reduce((latestTs, file) => Math.max(latestTs, file.updated_timestamp || 0), 0);2018 return {2019 objectCount: files.length,2020 totalSize: this.formatBytes(totalSize),2021 storageClass: 'standard',2022 versioning: files.length ? 'enabled' : 'disabled',2023 lastModified: latest ? this.formatUnix(latest) : '—',2024 };2025 },2026 projectDatabases() {2027 return this.databases.filter((db) => db.project_id === this.selectedProjectId);2028 },2029 activeDatabase() {2030 if (!this.projectDatabases.length) return null;2031 const explicit = this.projectDatabases.find((db) => db.id === this.selectedDatabaseId);2032 return explicit || this.projectDatabases[0];2033 },2034 projectLoggers() {2035 return this.loggers.filter((logger) => logger.project_id === this.selectedProjectId);2036 },2037 activeLogger() {2038 if (!this.projectLoggers.length) return null;2039 const explicit = this.projectLoggers.find((logger) => logger.id === this.selectedLoggerId);2040 return explicit || this.projectLoggers[0];2041 },2042 projectLambdas() {2043 return this.lambdas.filter((lambda) => lambda.project_id === this.selectedProjectId);2044 },2045 activeLambda() {2046 if (!this.projectLambdas.length) return null;2047 const explicit = this.projectLambdas.find((lambda) => lambda.id === this.selectedLambdaId);2048 return explicit || this.projectLambdas[0];2049 },2050 projectQueues() {2051 return this.queues.filter((queue) => queue.project_id === this.selectedProjectId);2052 },2053 activeQueue() {2054 if (!this.projectQueues.length) return null;2055 const explicit = this.projectQueues.find((queue) => queue.id === this.selectedQueueId);2056 return explicit || this.projectQueues[0];2057 },2058 projectMetrics() {2059 const lambdaStats = this.projectLambdas.reduce(2060 (acc, lambda) => {2061 acc.count += 1;2062 const inv = parseInt(lambda.metrics.invocations.replace(/D/g, ''), 10) || 0;2063 acc.invocations += inv;2064 acc.successRates.push(parseFloat(lambda.metrics.successRate));2065 acc.latencies.push(lambda.metrics.p95);2066 return acc;2067 },2068 { count: 0, invocations: 0, successRates: [], latencies: [] }2069 );2070 const queueStats = this.projectQueues.reduce(2071 (acc, queue) => {2072 acc.count += 1;2073 acc.rateIn += queue.metrics.rateIn;2074 acc.rateOut += queue.metrics.rateOut;2075 acc.lag += queue.metrics.ready;2076 return acc;2077 },2078 { count: 0, rateIn: 0, rateOut: 0, lag: 0 }2079 );2080 const databaseStats = this.projectDatabases.reduce(2081 (acc, db) => {2082 acc.count += 1;2083 acc.collections += db.collections.length;2084 acc.documents += db.usage.documents;2085 acc.storage.push(db.usage.storage);2086 return acc;2087 },2088 { count: 0, collections: 0, documents: 0, storage: [] }2089 );2090 const bucketStats = this.projectBuckets.reduce(2091 (acc, bucket) => {2092 const files = this.files.filter((file) => file.bucket_id === bucket.id);2093 acc.count += 1;2094 acc.objects += files.length;2095 acc.size += files.reduce((sum, file) => sum + file.size, 0);2096 return acc;2097 },2098 { count: 0, objects: 0, size: 0 }2099 );2100 const successAvg = lambdaStats.successRates.length2101 ? (lambdaStats.successRates.reduce((sum, rate) => sum + rate, 0) / lambdaStats.successRates.length).toFixed(1) + '%'2102 : '—';2103 const latency = lambdaStats.latencies.length ? lambdaStats.latencies[0] : '—';2104 return {2105 databases: databaseStats.count,2106 collections: databaseStats.collections,2107 documents: this.formatNumber(databaseStats.documents),2108 storage: databaseStats.storage.join(' · ') || '—',2109 lambdas: lambdaStats.count,2110 invocations: this.formatNumber(lambdaStats.invocations),2111 lambdaSuccess: successAvg,2112 lambdaLatency: latency,2113 buckets: bucketStats.count,2114 objects: this.formatNumber(bucketStats.objects),2115 storageUsage: this.formatBytes(bucketStats.size),2116 queues: queueStats.count,2117 queueThroughput: `${queueStats.rateIn}/s in · ${queueStats.rateOut}/s out`,2118 queueLag: `${queueStats.lag} messages pending`,2119 };2120 },2121 lambdaTabSummaryComputed() {2122 if (!this.activeLambda) return '';2123 switch (this.lambdaTab) {2124 case 'overview':2125 return 'GET /api/v0/lambdas/{lambda_id}';2126 case 'code':2127 return 'PATCH /api/v0/lambdas/{lambda_id} to update source';2128 case 'test':2129 return 'POST /api/v0/run/{lambda_id}';2130 case 'versions':2131 return 'GET /api/v0/lambdas/{lambda_id} for version metadata';2132 default:2133 return '';2134 }2135 },2136 databaseTabSummaryComputed() {2137 if (!this.activeDatabase) return '';2138 switch (this.databaseTab) {2139 case 'overview':2140 return 'GET /v1/databases/{databaseId}';2141 case 'collections':2142 return 'POST /v1/databases/{databaseId}/collections';2143 case 'apiKeys':2144 return 'POST /v1/databases/{databaseId}:createApiKey';2145 case 'owners':2146 return 'POST /v1/databases/{databaseId}:addOwner';2147 default:2148 return '';2149 }2150 },2151 queueTabSummaryComputed() {2152 if (!this.activeQueue) return '';2153 switch (this.queueTab) {2154 case 'overview':2155 return 'GET /v1/queues/{queue_id}';2156 case 'consumers':2157 return 'GET /v1/clients';2158 case 'messages':2159 return 'POST /v1/queues/{queue_id}:read';2160 default:2161 return '';2162 }2163 },2164 },2165 watch: {2166 lambdaTabSummaryComputed(val) {2167 this.lambdaTabSummary = val;2168 },2169 databaseTabSummaryComputed(val) {2170 this.databaseTabSummary = val;2171 },2172 queueTabSummaryComputed(val) {2173 this.queueTabSummary = val;2174 },2175 selectedProjectId() {2176 this.selectedConfigId = null;2177 this.selectedBucketId = null;2178 this.selectedDatabaseId = null;2179 this.selectedLoggerId = null;2180 this.selectedLambdaId = null;2181 this.selectedQueueId = null;2182 },2183 },2184 mounted() {2185 this.lambdaTabSummary = this.lambdaTabSummaryComputed;2186 this.databaseTabSummary = this.databaseTabSummaryComputed;2187 this.queueTabSummary = this.queueTabSummaryComputed;2188 },2189 methods: {2190 formatUnix(timestamp) {2191 if (!timestamp) return '—';2192 const date = new Date(timestamp * 1000);2193 return date.toLocaleString('en-US', { hour12: false });2194 },2195 formatNumber(value) {2196 if (value === undefined || value === null) return '0';2197 return new Intl.NumberFormat('en-US').format(value);2198 },2199 formatBytes(bytes) {2200 if (!bytes || bytes <= 0) return '0 B';2201 const units = ['B', 'KB', 'MB', 'GB', 'TB'];2202 const idx = Math.floor(Math.log(bytes) / Math.log(1024));2203 const value = bytes / Math.pow(1024, idx);2204 return `${value.toFixed(1)} ${units[idx]}`;2205 },2206 summarizeDefaults(defaults) {2207 if (!defaults || Object.keys(defaults).length === 0) return '—';2208 return Object.entries(defaults)2209 .map(([key, value]) => `${key}: ${value}`)2210 .join(', ');2211 },2212 objectKeys(object) {2213 return Object.keys(object || {});2214 },2215 methodColor(method) {2216 switch (method) {2217 case 'GET':2218 return 'text-emerald-300 border-emerald-400/40';2219 case 'POST':2220 return 'text-brand-300 border-brand-400/40';2221 case 'PATCH':2222 return 'text-amber-300 border-amber-400/40';2223 case 'DELETE':2224 return 'text-rose-300 border-rose-400/40';2225 default:2226 return 'text-slate-300 border-slate-700';2227 }2228 },2229 },2230 }).mount('#app');2231 </script>2232 </body>2233 </html>2234 Enlace
El enlace para compartir es:

