Wikipedia para siempre
ShareCode
Permalink: http://www.treeweb.es/u/974/ 01/02/2011

ShareCode

1 <!DOCTYPE html>2 <html lang="en">3  <head>4  <meta charset="UTF-8" />5  <meta name="viewport" content="width=device-width, initial-scale=1.0" />6  <title>Hola Cloud Console &mdash; 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 }} &middot; {{ 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">&mdash; {{ 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 &amp; 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 &rarr; 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 &amp; 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 }} &lt; 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 &amp; 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: