webspresso 0.0.104 → 0.0.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/AGENTS.md +21 -2
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +220 -8
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/FLAGS.md +118 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/LOGGING.md +93 -0
- package/.agents/METRICS.md +127 -0
- package/.agents/MIGRATION_JOBS.md +95 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +38 -3
- package/.agents/QUEUE.md +42 -0
- package/.agents/RATE_LIMIT.md +147 -0
- package/.agents/RESILIENCE.md +235 -0
- package/.agents/SCALE_OUT.md +109 -0
- package/.agents/SECRETS.md +117 -0
- package/AGENTS.md +16 -0
- package/README.md +2 -2
- package/bin/commands/add-aws.js +396 -0
- package/bin/commands/add-ci.js +523 -0
- package/bin/commands/add-cloudflare.js +240 -0
- package/bin/commands/add-cloudrun.js +381 -0
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/add-migration-job.js +396 -0
- package/bin/commands/assets-sync.js +209 -0
- package/bin/commands/client-generate.js +52 -0
- package/bin/commands/console.js +53 -0
- package/bin/commands/db-migrate.js +47 -2
- package/bin/commands/db-studio.js +84 -0
- package/bin/commands/deploy-check.js +242 -13
- package/bin/commands/dev.js +1 -1
- package/bin/commands/export.js +87 -0
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/secrets-pull.js +208 -0
- package/bin/commands/start.js +1 -1
- package/bin/utils/db.js +7 -1
- package/bin/utils/project.js +2 -2
- package/bin/webspresso.js +22 -0
- package/core/app.js +421 -0
- package/core/auth/authenticate.js +93 -0
- package/core/auth/hash.js +5 -4
- package/core/auth/index.js +34 -4
- package/core/auth/jwt.js +5 -54
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +53 -10
- package/core/auth/middleware.js +16 -68
- package/core/auth/oauth.js +8 -17
- package/core/auth/redis-session.js +1008 -0
- package/core/auth/sessions.js +156 -0
- package/core/auth/totp.js +2 -1
- package/core/cache/byte-size.js +123 -0
- package/core/cache/cdn.js +296 -0
- package/core/cache/index.js +33 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +354 -0
- package/core/commands/registry.js +6 -20
- package/core/console/commands.js +148 -0
- package/core/console/context.js +260 -0
- package/core/console/eval.js +70 -0
- package/core/console/index.js +22 -0
- package/core/console/repl.js +102 -0
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +49 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +9 -7
- package/core/events/signal.js +7 -0
- package/core/export/discovery.js +191 -0
- package/core/export/exporter.js +277 -0
- package/core/export/index.js +24 -0
- package/core/flags/evaluator.js +252 -0
- package/core/flags/hasher.js +37 -0
- package/core/flags/index.js +21 -0
- package/core/flags/manager.js +263 -0
- package/core/flags/store.js +126 -0
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/http/adapter.js +74 -0
- package/core/http/express-adapter.js +46 -0
- package/core/http/hono-adapter.js +531 -0
- package/core/http/index.js +41 -0
- package/core/http/pipeline.js +485 -0
- package/core/index.js +336 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +325 -0
- package/core/limits/normalize.js +222 -0
- package/core/logger/formatters.js +264 -0
- package/core/logger/index.js +260 -0
- package/core/logger/trace.js +137 -0
- package/core/metrics/collectors.js +125 -0
- package/core/metrics/index.js +45 -0
- package/core/metrics/middleware.js +230 -0
- package/core/metrics/registry.js +366 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/d1.js +265 -0
- package/core/orm/index.js +176 -13
- package/core/orm/json-fields.js +22 -8
- package/core/orm/migrate-cloud.js +194 -0
- package/core/orm/model.js +10 -0
- package/core/orm/pool.js +97 -0
- package/core/orm/query-builder.js +72 -9
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/cloudflare.js +131 -0
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/adapters/pubsub.js +400 -0
- package/core/queue/adapters/sqs.js +419 -0
- package/core/queue/adapters/wait-until.js +77 -0
- package/core/queue/index.js +8 -0
- package/core/queue/manager.js +66 -41
- package/core/rate-limit/index.js +22 -0
- package/core/rate-limit/limiter.js +219 -0
- package/core/rate-limit/stores/memory.js +129 -0
- package/core/rate-limit/stores/redis.js +151 -0
- package/core/rate-limit/utils.js +112 -0
- package/core/resilience/circuit-breaker.js +422 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/secrets/index.js +113 -0
- package/core/secrets/manager.js +308 -0
- package/core/secrets/providers/aws.js +288 -0
- package/core/secrets/providers/env.js +44 -0
- package/core/secrets/providers/gcp.js +307 -0
- package/core/secrets/providers/memory.js +76 -0
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +129 -19
- package/core/stream/index.js +19 -0
- package/core/stream/text-stream.js +326 -0
- package/core/studio/index.js +43 -0
- package/core/studio/inspector.js +258 -0
- package/core/studio/server.js +325 -0
- package/core/studio/ui.js +681 -0
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +118 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +6 -5
- package/core/utils/fs.js +144 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +1549 -2
- package/index.js +272 -2
- package/package.json +3 -2
- package/plugins/admin-panel/core/api-extensions.js +2 -5
- package/plugins/admin-panel/index.js +13 -6
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -3
- package/plugins/api-key/index.js +3 -2
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +6 -15
- package/plugins/audit-log/index.js +3 -1
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +3 -2
- package/plugins/cors.js +3 -2
- package/plugins/csrf/index.js +9 -12
- package/plugins/csv/index.js +8 -5
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/email/index.js +11 -9
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +5 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/polar/src/api.js +2 -1
- package/plugins/polar/src/webhooks.js +5 -6
- package/plugins/queue/index.js +18 -2
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/redirect/index.js +16 -9
- package/plugins/rls/index.js +170 -0
- package/plugins/seo-checker/index.js +2 -2
- package/plugins/site-analytics/index.js +3 -1
- package/plugins/site-analytics/tracking.js +2 -2
- package/plugins/sitemap.js +3 -2
- package/plugins/swagger.js +12 -5
- package/plugins/upload/gcs-provider.js +148 -0
- package/plugins/upload/index.js +136 -77
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/upload/s3-provider.js +257 -0
- package/plugins/webhooks/crypto.js +6 -11
- package/plugins/webhooks/dispatcher.js +8 -8
- package/plugins/webhooks/index.js +33 -23
- package/plugins/webhooks/matcher.js +7 -11
- package/plugins/webhooks/sender.js +8 -11
- package/plugins/xlsx/index.js +7 -5
- package/src/api/api-loader.js +12 -95
- package/src/api/define-api.js +1 -0
- package/src/app-context.js +41 -5
- package/src/client-runtime/constants.js +10 -0
- package/src/client-runtime/mount.js +2 -3
- package/src/dev/banner.js +6 -2
- package/src/dev/client-gen.js +458 -0
- package/src/discovery/scan-routes.js +5 -2
- package/src/file-router.js +3 -3
- package/src/helpers.js +212 -24
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +13 -154
- package/src/plugin-manager.js +99 -18
- package/src/server.js +335 -133
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +95 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +15 -1
- package/src/services/validator.js +38 -20
- package/src/template/precompile.js +8 -19
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Modern Web UI for Webspresso DB Studio
|
|
5
|
+
* Built with Tailwind CSS v4 and Native Declarative EUIX Engine (<api_config>, <api_endpoint>, <watch>)
|
|
6
|
+
* @module core/studio/ui
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function renderStudioHtml() {
|
|
10
|
+
return `<!DOCTYPE html>
|
|
11
|
+
<html lang="en" class="dark">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="UTF-8">
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
15
|
+
<title>Webspresso DB Studio</title>
|
|
16
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
17
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
18
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
19
|
+
|
|
20
|
+
<!-- Tailwind CSS v4 Browser Runtime -->
|
|
21
|
+
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
|
|
22
|
+
|
|
23
|
+
<!-- EUIX Engine (Litepacks Declarative Reactive UI Engine with SWR API Client) -->
|
|
24
|
+
<script src="https://unpkg.com/euixjs@latest/dist/EUIXEngine.umd.js"></script>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
body {
|
|
28
|
+
font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;
|
|
29
|
+
}
|
|
30
|
+
.font-mono {
|
|
31
|
+
font-family: 'JetBrains Mono', monospace;
|
|
32
|
+
}
|
|
33
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
34
|
+
::-webkit-scrollbar-track { background: #090d16; }
|
|
35
|
+
::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 3px; }
|
|
36
|
+
::-webkit-scrollbar-thumb:hover { background: #334155; }
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body class="bg-slate-950 text-slate-100 flex h-screen overflow-hidden selection:bg-indigo-500/30 selection:text-indigo-200">
|
|
40
|
+
|
|
41
|
+
<!-- EUIX Mount Target -->
|
|
42
|
+
<div id="app" class="flex h-screen w-screen overflow-hidden"></div>
|
|
43
|
+
|
|
44
|
+
<!-- 100% Declarative EUIX Application Specification -->
|
|
45
|
+
<script type="application/euix" target="#app" id="euix-studio-spec">
|
|
46
|
+
<uid_spec>
|
|
47
|
+
<!-- Declarative REST SWR API Configuration -->
|
|
48
|
+
<api_config base_url="/_studio/api">
|
|
49
|
+
<!-- 1. Auto-fetch all database tables on mount -->
|
|
50
|
+
<api_endpoint
|
|
51
|
+
id="get_tables"
|
|
52
|
+
tag="get_tables"
|
|
53
|
+
url="/tables"
|
|
54
|
+
method="GET"
|
|
55
|
+
bind_target="tables"
|
|
56
|
+
auto_fetch="true"
|
|
57
|
+
loading="isLoadingTables"
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
<!-- 2. On-demand Table Schema inspection -->
|
|
61
|
+
<api_endpoint
|
|
62
|
+
id="get_schema"
|
|
63
|
+
tag="get_schema"
|
|
64
|
+
url="/tables/{data.activeTable}/schema"
|
|
65
|
+
method="GET"
|
|
66
|
+
bind_target="columns"
|
|
67
|
+
auto_fetch="false"
|
|
68
|
+
loading="isLoadingSchema"
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
<!-- 3. On-demand Paginated Rows fetching -->
|
|
72
|
+
<api_endpoint
|
|
73
|
+
id="get_rows"
|
|
74
|
+
tag="get_rows"
|
|
75
|
+
url="/tables/{data.activeTable}/rows?page={data.page}&limit={data.limit}"
|
|
76
|
+
method="GET"
|
|
77
|
+
bind_target="tableResult"
|
|
78
|
+
auto_fetch="false"
|
|
79
|
+
loading="isLoadingRows"
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<!-- 4. Raw SQL Query Execution -->
|
|
83
|
+
<api_endpoint
|
|
84
|
+
id="run_query"
|
|
85
|
+
tag="run_query"
|
|
86
|
+
url="/query"
|
|
87
|
+
method="POST"
|
|
88
|
+
bind_target="sqlResult"
|
|
89
|
+
auto_fetch="false"
|
|
90
|
+
loading="isLoadingSql"
|
|
91
|
+
>
|
|
92
|
+
<body>{"sql": "{data.sqlQuery}"}</body>
|
|
93
|
+
</api_endpoint>
|
|
94
|
+
|
|
95
|
+
<!-- 5. Live Telemetry & Metrics -->
|
|
96
|
+
<api_endpoint
|
|
97
|
+
id="get_telemetry"
|
|
98
|
+
tag="get_telemetry"
|
|
99
|
+
url="/telemetry"
|
|
100
|
+
method="GET"
|
|
101
|
+
bind_target="telemetry"
|
|
102
|
+
auto_fetch="true"
|
|
103
|
+
loading="isLoadingTelemetry"
|
|
104
|
+
/>
|
|
105
|
+
</api_config>
|
|
106
|
+
|
|
107
|
+
<!-- Central EUIX Reactive Data Model -->
|
|
108
|
+
<data_model>
|
|
109
|
+
<state id="tables" type="array">[]</state>
|
|
110
|
+
<state id="filterQuery" type="string"></state>
|
|
111
|
+
<state id="activeTable" type="string"></state>
|
|
112
|
+
<state id="activeTab" type="string">data</state>
|
|
113
|
+
<state id="columns" type="array">[]</state>
|
|
114
|
+
<state id="tableResult" type="object">{"rows": [], "total": 0, "totalPages": 1}</state>
|
|
115
|
+
<state id="page" type="number">1</state>
|
|
116
|
+
<state id="limit" type="number">25</state>
|
|
117
|
+
<state id="pkField" type="string">id</state>
|
|
118
|
+
<state id="isLoadingTables" type="boolean">false</state>
|
|
119
|
+
<state id="isLoadingSchema" type="boolean">false</state>
|
|
120
|
+
<state id="isLoadingRows" type="boolean">false</state>
|
|
121
|
+
<state id="isLoadingSql" type="boolean">false</state>
|
|
122
|
+
<state id="sqlQuery" type="string">SELECT * FROM sqlite_master LIMIT 10;</state>
|
|
123
|
+
<state id="sqlResult" type="object">{"rows": [], "durationMs": 0}</state>
|
|
124
|
+
<state id="isModalOpen" type="boolean">false</state>
|
|
125
|
+
<state id="modalMode" type="string">insert</state>
|
|
126
|
+
<state id="modalRecordId" type="string"></state>
|
|
127
|
+
<state id="newRowJson" type="string">{}</state>
|
|
128
|
+
<state id="telemetry" type="object">{"memory":{"heapUsedMb":0,"heapTotalMb":0,"rssMb":0,"externalMb":0},"pool":{"used":0,"free":0,"pending":0,"client":""},"breakers":[],"queue":{"enabled":false},"uptimeSeconds":0,"nodeVersion":""}</state>
|
|
129
|
+
<state id="isLoadingTelemetry" type="boolean">false</state>
|
|
130
|
+
|
|
131
|
+
<!-- Declarative Watchers for Automatic Reactive HTTP Revalidation -->
|
|
132
|
+
<watch path="activeTable">
|
|
133
|
+
<step action="SET_STATE">
|
|
134
|
+
<path>data.page</path>
|
|
135
|
+
<value>1</value>
|
|
136
|
+
</step>
|
|
137
|
+
<step action="REVALIDATE_API" tag="get_schema" />
|
|
138
|
+
<step action="REVALIDATE_API" tag="get_rows" />
|
|
139
|
+
</watch>
|
|
140
|
+
|
|
141
|
+
<watch path="page">
|
|
142
|
+
<step action="REVALIDATE_API" tag="get_rows" />
|
|
143
|
+
</watch>
|
|
144
|
+
</data_model>
|
|
145
|
+
|
|
146
|
+
<div class="flex h-full w-full overflow-hidden">
|
|
147
|
+
<!-- Reactive Sidebar -->
|
|
148
|
+
<aside class="w-72 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0">
|
|
149
|
+
<!-- Brand Header -->
|
|
150
|
+
<div class="p-4 border-b border-slate-800 flex items-center justify-between bg-slate-900/50 backdrop-blur">
|
|
151
|
+
<div class="flex items-center gap-2.5">
|
|
152
|
+
<span class="w-8 h-8 rounded-lg bg-indigo-600 flex items-center justify-center font-bold text-white shadow-lg shadow-indigo-500/20 text-sm">⚡</span>
|
|
153
|
+
<div>
|
|
154
|
+
<h1 class="text-sm font-bold tracking-tight text-white flex items-center gap-1.5">
|
|
155
|
+
Webspresso <span class="text-[10px] font-semibold bg-indigo-500/20 text-indigo-400 px-1.5 py-0.5 rounded border border-indigo-500/30">Studio</span>
|
|
156
|
+
</h1>
|
|
157
|
+
<p class="text-[11px] text-slate-400">Database Inspector & Runner</p>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- EUIX Engine SWR Badge -->
|
|
163
|
+
<div class="px-4 py-2 bg-slate-950/40 border-b border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
|
|
164
|
+
<div class="flex items-center gap-1.5">
|
|
165
|
+
<span class="w-2 h-2 rounded-full {data.isLoadingTables || data.isLoadingRows ? 'bg-amber-400 animate-ping' : 'bg-emerald-400 animate-pulse'}"></span>
|
|
166
|
+
<span class="font-medium text-slate-300">EUIX Engine</span>
|
|
167
|
+
</div>
|
|
168
|
+
<span class="font-mono text-indigo-400 font-semibold">{data.tables.length} Tables</span>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<!-- Two-Way Bound Search Input -->
|
|
172
|
+
<div class="p-3 border-b border-slate-800">
|
|
173
|
+
<input bind="filterQuery" placeholder="Filter tables..."
|
|
174
|
+
class="w-full bg-slate-950 border border-slate-800 rounded-lg px-3 py-1.5 text-xs text-slate-200 placeholder-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition" />
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<!-- Declarative Tables List with Keyed Reconciliation -->
|
|
178
|
+
<div class="flex-1 overflow-y-auto p-2 space-y-1">
|
|
179
|
+
<if condition="data.tables.length === 0">
|
|
180
|
+
<div class="p-4 text-center text-xs text-slate-500">
|
|
181
|
+
{data.isLoadingTables ? 'Loading tables...' : 'No tables found'}
|
|
182
|
+
</div>
|
|
183
|
+
</if>
|
|
184
|
+
<for_each items="{data.tables}" var="t" key="name">
|
|
185
|
+
<if condition="!data.filterQuery || t.name.toLowerCase().includes(data.filterQuery.toLowerCase())">
|
|
186
|
+
<div class="flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium cursor-pointer transition {data.activeTable == t.name ? 'bg-indigo-600 text-white font-semibold shadow-sm' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60'}">
|
|
187
|
+
<!-- Setting activeTable automatically triggers <watch path="activeTable"> to fetch schema & rows -->
|
|
188
|
+
<on_click action="SET_STATE">
|
|
189
|
+
<path>data.activeTable</path>
|
|
190
|
+
<value>{t.name}</value>
|
|
191
|
+
</on_click>
|
|
192
|
+
<span class="truncate">{t.name}</span>
|
|
193
|
+
<span class="font-mono text-[10px] px-1.5 py-0.5 rounded-full {data.activeTable == t.name ? 'bg-indigo-700/60 text-indigo-100' : 'bg-slate-800 text-slate-400'}">{t.count}</span>
|
|
194
|
+
</div>
|
|
195
|
+
</if>
|
|
196
|
+
</for_each>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<!-- Sidebar Footer -->
|
|
200
|
+
<div class="p-3 border-t border-slate-800 text-[11px] text-slate-500 flex items-center justify-between bg-slate-950/20">
|
|
201
|
+
<div class="flex items-center gap-1.5">
|
|
202
|
+
<span class="text-indigo-400 font-semibold">Tailwind 4</span>
|
|
203
|
+
<span>•</span>
|
|
204
|
+
<span class="text-emerald-400 font-semibold">Declarative EUIX</span>
|
|
205
|
+
</div>
|
|
206
|
+
<span class="font-mono text-slate-400">v0.0.105</span>
|
|
207
|
+
</div>
|
|
208
|
+
</aside>
|
|
209
|
+
|
|
210
|
+
<!-- Main Content Area -->
|
|
211
|
+
<main class="flex-1 flex flex-col overflow-hidden bg-slate-950">
|
|
212
|
+
<!-- Header -->
|
|
213
|
+
<header class="h-14 px-6 bg-slate-900 border-b border-slate-800 flex items-center justify-between gap-4 shrink-0">
|
|
214
|
+
<div class="flex items-center gap-4">
|
|
215
|
+
<div class="flex items-center gap-2.5">
|
|
216
|
+
<span class="text-base font-bold font-mono text-white">{data.activeTable ? data.activeTable : 'Select a table'}</span>
|
|
217
|
+
<if condition="data.activeTable">
|
|
218
|
+
<span class="text-xs font-mono px-2 py-0.5 rounded-full bg-slate-800 text-slate-300 border border-slate-700">{data.tableResult.total || 0} rows</span>
|
|
219
|
+
</if>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<!-- Tab Switching using Declarative on_click:set Shorthands -->
|
|
223
|
+
<nav class="flex gap-1 bg-slate-950 p-1 rounded-lg border border-slate-800">
|
|
224
|
+
<button class="px-3 py-1 text-xs font-semibold rounded-md transition {data.activeTab == 'data' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}">
|
|
225
|
+
<on_click action="SET_STATE">
|
|
226
|
+
<path>data.activeTab</path>
|
|
227
|
+
<value>data</value>
|
|
228
|
+
</on_click>
|
|
229
|
+
Table Data
|
|
230
|
+
</button>
|
|
231
|
+
<button class="px-3 py-1 text-xs font-semibold rounded-md transition {data.activeTab == 'sql' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}">
|
|
232
|
+
<on_click action="SET_STATE">
|
|
233
|
+
<path>data.activeTab</path>
|
|
234
|
+
<value>sql</value>
|
|
235
|
+
</on_click>
|
|
236
|
+
SQL Query
|
|
237
|
+
</button>
|
|
238
|
+
<button class="px-3 py-1 text-xs font-semibold rounded-md transition {data.activeTab == 'telemetry' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}">
|
|
239
|
+
<on_click action="SET_STATE">
|
|
240
|
+
<path>data.activeTab</path>
|
|
241
|
+
<value>telemetry</value>
|
|
242
|
+
</on_click>
|
|
243
|
+
<on_click action="REVALIDATE_API" tag="get_telemetry" />
|
|
244
|
+
Live Telemetry
|
|
245
|
+
</button>
|
|
246
|
+
</nav>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div class="flex items-center gap-2">
|
|
250
|
+
<!-- Declarative SWR Revalidate Action -->
|
|
251
|
+
<button class="px-3 py-1.5 text-xs font-semibold rounded-lg border border-slate-700 bg-slate-800 hover:bg-slate-700 text-slate-200 transition flex items-center gap-1.5 shadow-sm">
|
|
252
|
+
<on_click action="REVALIDATE_API" tag="get_tables" />
|
|
253
|
+
<on_click action="REVALIDATE_API" tag="get_rows" />
|
|
254
|
+
<on_click action="REVALIDATE_API" tag="get_telemetry" />
|
|
255
|
+
<span>↻</span> Refresh
|
|
256
|
+
</button>
|
|
257
|
+
<if condition="data.activeTable">
|
|
258
|
+
<button class="px-3 py-1.5 text-xs font-semibold rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white transition flex items-center gap-1.5 shadow-sm shadow-indigo-600/30">
|
|
259
|
+
<on_click action="SET_STATE">
|
|
260
|
+
<path>data.modalMode</path>
|
|
261
|
+
<value>insert</value>
|
|
262
|
+
</on_click>
|
|
263
|
+
<on_click action="SET_STATE">
|
|
264
|
+
<path>data.newRowJson</path>
|
|
265
|
+
<value>{}</value>
|
|
266
|
+
</on_click>
|
|
267
|
+
<on_click action="SET_STATE">
|
|
268
|
+
<path>data.isModalOpen</path>
|
|
269
|
+
<value>true</value>
|
|
270
|
+
</on_click>
|
|
271
|
+
<span>+</span> New Record
|
|
272
|
+
</button>
|
|
273
|
+
</if>
|
|
274
|
+
</div>
|
|
275
|
+
</header>
|
|
276
|
+
|
|
277
|
+
<!-- View 1: Table Data View -->
|
|
278
|
+
<if condition="data.activeTab == 'data'">
|
|
279
|
+
<section class="flex-1 flex flex-col overflow-hidden p-6 gap-4">
|
|
280
|
+
<div class="flex-1 bg-slate-900 border border-slate-800 rounded-xl flex flex-col overflow-hidden shadow-xl shadow-black/40">
|
|
281
|
+
|
|
282
|
+
<!-- When No Table is Selected -->
|
|
283
|
+
<if condition="!data.activeTable">
|
|
284
|
+
<div class="flex-1 flex items-center justify-center p-12 text-center text-slate-500 text-sm">
|
|
285
|
+
<div>
|
|
286
|
+
<div class="text-3xl mb-2">⚡</div>
|
|
287
|
+
<p class="font-medium text-slate-400">Select a table from the sidebar</p>
|
|
288
|
+
<p class="text-xs text-slate-600 mt-1">Explore schema, browse paginated records, insert or edit rows</p>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</if>
|
|
292
|
+
|
|
293
|
+
<!-- When a Table is Selected -->
|
|
294
|
+
<if condition="data.activeTable">
|
|
295
|
+
<div class="flex-1 overflow-auto">
|
|
296
|
+
<table class="w-full text-left text-xs border-collapse">
|
|
297
|
+
<thead class="sticky top-0 bg-slate-900 border-b border-slate-800 z-10 font-mono text-slate-400 text-[11px]">
|
|
298
|
+
<tr class="border-b border-slate-800">
|
|
299
|
+
<for_each items="{data.columns}" var="c" key="name">
|
|
300
|
+
<th class="px-4 py-3 whitespace-nowrap">
|
|
301
|
+
{c.name} {c.isPrimaryKey ? '🔑' : ''}
|
|
302
|
+
<span class="text-[10px] text-slate-500 font-normal">({c.type})</span>
|
|
303
|
+
</th>
|
|
304
|
+
</for_each>
|
|
305
|
+
<th class="px-4 py-3 text-right w-24 whitespace-nowrap">Actions</th>
|
|
306
|
+
</tr>
|
|
307
|
+
</thead>
|
|
308
|
+
<tbody class="divide-y divide-slate-800/60 font-mono text-slate-300">
|
|
309
|
+
<if condition="!data.tableResult.rows || data.tableResult.rows.length === 0">
|
|
310
|
+
<tr>
|
|
311
|
+
<td class="text-center py-16 text-slate-500 font-sans" colspan="100">
|
|
312
|
+
{data.isLoadingRows ? 'Loading records...' : 'No records found in this table'}
|
|
313
|
+
</td>
|
|
314
|
+
</tr>
|
|
315
|
+
</if>
|
|
316
|
+
<for_each items="{data.tableResult.rows}" var="row" key="id">
|
|
317
|
+
<tr class="hover:bg-slate-850 transition">
|
|
318
|
+
<for_each items="{data.columns}" var="col" key="name">
|
|
319
|
+
<td class="px-4 py-2.5 whitespace-nowrap text-slate-300">
|
|
320
|
+
{row[col.name] === null ? 'null' : (typeof row[col.name] === 'object' ? JSON.stringify(row[col.name]) : String(row[col.name]))}
|
|
321
|
+
</td>
|
|
322
|
+
</for_each>
|
|
323
|
+
<td class="px-4 py-2.5 text-right whitespace-nowrap">
|
|
324
|
+
<div class="flex items-center justify-end gap-1">
|
|
325
|
+
<!-- Declarative Delete with Native XHR and automatic Revalidation -->
|
|
326
|
+
<button class="p-1 rounded text-slate-400 hover:text-rose-400 hover:bg-slate-800 transition text-xs">
|
|
327
|
+
<on_click action="XHR" confirm="Are you sure you want to delete this record?">
|
|
328
|
+
<url>/_studio/api/tables/{data.activeTable}/rows/{row[data.pkField || 'id']}?pk={data.pkField || 'id'}</url>
|
|
329
|
+
<method>DELETE</method>
|
|
330
|
+
<step action="REVALIDATE_API" tag="get_tables" />
|
|
331
|
+
<step action="REVALIDATE_API" tag="get_rows" />
|
|
332
|
+
</on_click>
|
|
333
|
+
✕
|
|
334
|
+
</button>
|
|
335
|
+
</div>
|
|
336
|
+
</td>
|
|
337
|
+
</tr>
|
|
338
|
+
</for_each>
|
|
339
|
+
</tbody>
|
|
340
|
+
</table>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<!-- Declarative Pagination Controls -->
|
|
344
|
+
<div class="h-12 px-4 bg-slate-900/90 border-t border-slate-800 flex items-center justify-between text-xs text-slate-400 font-mono shrink-0">
|
|
345
|
+
<div>Showing {data.tableResult.rows ? data.tableResult.rows.length : 0} of {data.tableResult.total || 0} records</div>
|
|
346
|
+
<div class="flex items-center gap-2">
|
|
347
|
+
<button class="px-2.5 py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-200 transition" disabled="{data.page <= 1}">
|
|
348
|
+
<on_click action="SET_STATE">
|
|
349
|
+
<path>data.page</path>
|
|
350
|
+
<value>{data.page - 1}</value>
|
|
351
|
+
</on_click>
|
|
352
|
+
Prev
|
|
353
|
+
</button>
|
|
354
|
+
<span class="px-2 font-bold text-white">Page {data.page} of {data.tableResult.totalPages || 1}</span>
|
|
355
|
+
<button class="px-2.5 py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-200 transition" disabled="{data.page >= (data.tableResult.totalPages || 1)}">
|
|
356
|
+
<on_click action="SET_STATE">
|
|
357
|
+
<path>data.page</path>
|
|
358
|
+
<value>{data.page + 1}</value>
|
|
359
|
+
</on_click>
|
|
360
|
+
Next
|
|
361
|
+
</button>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</if>
|
|
365
|
+
</div>
|
|
366
|
+
</section>
|
|
367
|
+
</if>
|
|
368
|
+
|
|
369
|
+
<!-- View 2: SQL Console View -->
|
|
370
|
+
<if condition="data.activeTab == 'sql'">
|
|
371
|
+
<section class="flex-1 flex flex-col overflow-hidden p-6 gap-4">
|
|
372
|
+
<div class="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-3 shadow-lg shrink-0">
|
|
373
|
+
<div class="flex items-center justify-between">
|
|
374
|
+
<span class="text-xs font-mono font-semibold text-slate-300">Raw SQL Editor</span>
|
|
375
|
+
<div class="flex items-center gap-1.5">
|
|
376
|
+
<span class="text-[11px] text-slate-500">Quick:</span>
|
|
377
|
+
<button class="text-[10px] text-slate-400 hover:text-indigo-300 font-mono px-2 py-0.5 rounded bg-slate-800/80 border border-slate-700/60 transition">
|
|
378
|
+
<on_click action="SET_STATE">
|
|
379
|
+
<path>data.sqlQuery</path>
|
|
380
|
+
<value>SELECT * FROM {data.activeTable || 'sqlite_master'} LIMIT 25;</value>
|
|
381
|
+
</on_click>
|
|
382
|
+
SELECT 25
|
|
383
|
+
</button>
|
|
384
|
+
<button class="text-[10px] text-slate-400 hover:text-indigo-300 font-mono px-2 py-0.5 rounded bg-slate-800/80 border border-slate-700/60 transition">
|
|
385
|
+
<on_click action="SET_STATE">
|
|
386
|
+
<path>data.sqlQuery</path>
|
|
387
|
+
<value>SELECT COUNT(*) AS total FROM {data.activeTable || 'sqlite_master'};</value>
|
|
388
|
+
</on_click>
|
|
389
|
+
COUNT(*)
|
|
390
|
+
</button>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
<textarea bind="sqlQuery" placeholder="SELECT * FROM sqlite_master LIMIT 10;"
|
|
394
|
+
class="w-full h-28 bg-slate-950 border border-slate-800 rounded-lg p-3 text-xs font-mono text-slate-200 placeholder-slate-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition resize-y"></textarea>
|
|
395
|
+
<div class="flex items-center justify-between">
|
|
396
|
+
<span class="text-xs font-mono text-slate-400">
|
|
397
|
+
{data.isLoadingSql ? 'Executing query...' : (data.sqlResult.durationMs ? '✓ Executed in ' + data.sqlResult.durationMs + 'ms (' + (data.sqlResult.rows ? data.sqlResult.rows.length : 0) + ' rows)' : '')}
|
|
398
|
+
</span>
|
|
399
|
+
<!-- Declarative SQL Revalidate Action -->
|
|
400
|
+
<button class="px-4 py-1.5 text-xs font-semibold rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white transition flex items-center gap-1.5 shadow-sm shadow-indigo-600/30">
|
|
401
|
+
<on_click action="REVALIDATE_API" tag="run_query" />
|
|
402
|
+
<span>▶</span> Run Query
|
|
403
|
+
</button>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<div class="flex-1 bg-slate-900 border border-slate-800 rounded-xl overflow-hidden flex flex-col shadow-xl shadow-black/40">
|
|
408
|
+
<div class="flex-1 overflow-auto">
|
|
409
|
+
<table class="w-full text-left text-xs border-collapse">
|
|
410
|
+
<tbody class="divide-y divide-slate-800/60 font-mono text-slate-300">
|
|
411
|
+
<if condition="!data.sqlResult.rows || data.sqlResult.rows.length === 0">
|
|
412
|
+
<tr>
|
|
413
|
+
<td class="text-center py-16 text-slate-500 font-sans" colspan="100">
|
|
414
|
+
{data.isLoadingSql ? 'Executing query...' : 'Enter a SQL query above and click "Run Query"'}
|
|
415
|
+
</td>
|
|
416
|
+
</tr>
|
|
417
|
+
</if>
|
|
418
|
+
<for_each items="{data.sqlResult.rows}" var="r" key="_idx">
|
|
419
|
+
<tr class="hover:bg-slate-850 transition">
|
|
420
|
+
<td class="px-4 py-2.5 whitespace-nowrap text-slate-300">{JSON.stringify(r)}</td>
|
|
421
|
+
</tr>
|
|
422
|
+
</for_each>
|
|
423
|
+
</tbody>
|
|
424
|
+
</table>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
</section>
|
|
428
|
+
</if>
|
|
429
|
+
|
|
430
|
+
<!-- View 3: Live Telemetry & Queue Dashboard -->
|
|
431
|
+
<if condition="data.activeTab == 'telemetry'">
|
|
432
|
+
<section class="flex-1 flex flex-col overflow-y-auto p-6 gap-6">
|
|
433
|
+
<!-- Top Metric Cards Grid -->
|
|
434
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
435
|
+
<!-- Memory (Heap Used / Total) -->
|
|
436
|
+
<div class="bg-slate-900 border border-slate-800 rounded-xl p-4 shadow-lg shadow-black/30">
|
|
437
|
+
<div class="flex items-center justify-between text-xs text-slate-400 font-mono mb-2">
|
|
438
|
+
<span>RAM (Heap Used)</span>
|
|
439
|
+
<span class="text-indigo-400 font-bold">{data.telemetry.memory.heapUsedMb || 0} MB</span>
|
|
440
|
+
</div>
|
|
441
|
+
<div class="text-2xl font-bold font-mono text-white mb-2">
|
|
442
|
+
{data.telemetry.memory.heapUsedMb || 0} <span class="text-xs font-normal text-slate-400">/ {data.telemetry.memory.heapTotalMb || 0} MB</span>
|
|
443
|
+
</div>
|
|
444
|
+
<div class="w-full bg-slate-950 rounded-full h-1.5 overflow-hidden border border-slate-800">
|
|
445
|
+
<div class="bg-indigo-500 h-1.5 rounded-full" style="width: 45%;"></div>
|
|
446
|
+
</div>
|
|
447
|
+
<div class="mt-2 text-[10px] text-slate-500 font-mono flex justify-between">
|
|
448
|
+
<span>RSS: {data.telemetry.memory.rssMb || 0} MB</span>
|
|
449
|
+
<span>Ext: {data.telemetry.memory.externalMb || 0} MB</span>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
<!-- Database Connection Pool -->
|
|
454
|
+
<div class="bg-slate-900 border border-slate-800 rounded-xl p-4 shadow-lg shadow-black/30">
|
|
455
|
+
<div class="flex items-center justify-between text-xs text-slate-400 font-mono mb-2">
|
|
456
|
+
<span>DB Pool ({data.telemetry.pool.client || 'Knex'})</span>
|
|
457
|
+
<span class="text-emerald-400 font-bold">{data.telemetry.pool.used || 0} Active</span>
|
|
458
|
+
</div>
|
|
459
|
+
<div class="text-2xl font-bold font-mono text-white mb-2">
|
|
460
|
+
{data.telemetry.pool.used || 0} <span class="text-xs font-normal text-slate-400">active / {data.telemetry.pool.free || 0} free</span>
|
|
461
|
+
</div>
|
|
462
|
+
<div class="w-full bg-slate-950 rounded-full h-1.5 overflow-hidden border border-slate-800">
|
|
463
|
+
<div class="bg-emerald-500 h-1.5 rounded-full" style="width: 30%;"></div>
|
|
464
|
+
</div>
|
|
465
|
+
<div class="mt-2 text-[10px] text-slate-500 font-mono flex justify-between">
|
|
466
|
+
<span>Pending Acquires: {data.telemetry.pool.pending || 0}</span>
|
|
467
|
+
<span class="text-emerald-400">Connected</span>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
<!-- Background Queue Status -->
|
|
472
|
+
<div class="bg-slate-900 border border-slate-800 rounded-xl p-4 shadow-lg shadow-black/30">
|
|
473
|
+
<div class="flex items-center justify-between text-xs text-slate-400 font-mono mb-2">
|
|
474
|
+
<span>Queue Workers</span>
|
|
475
|
+
<span class="{data.telemetry.queue.enabled ? (data.telemetry.queue.isPaused ? 'text-amber-400' : 'text-emerald-400') : 'text-slate-500'} font-bold">
|
|
476
|
+
{data.telemetry.queue.enabled ? (data.telemetry.queue.isPaused ? 'Paused' : 'Active') : 'Disabled'}
|
|
477
|
+
</span>
|
|
478
|
+
</div>
|
|
479
|
+
<div class="text-2xl font-bold font-mono text-white mb-2">
|
|
480
|
+
{data.telemetry.queue.activeWorkers || 0} <span class="text-xs font-normal text-slate-400">workers / {data.telemetry.queue.concurrency || 2} max</span>
|
|
481
|
+
</div>
|
|
482
|
+
<div class="w-full bg-slate-950 rounded-full h-1.5 overflow-hidden border border-slate-800">
|
|
483
|
+
<div class="bg-amber-500 h-1.5 rounded-full" style="width: 20%;"></div>
|
|
484
|
+
</div>
|
|
485
|
+
<div class="mt-2 text-[10px] text-slate-500 font-mono flex justify-between">
|
|
486
|
+
<span>Jobs Registered: {data.telemetry.queue.registeredJobs ? data.telemetry.queue.registeredJobs.length : 0}</span>
|
|
487
|
+
<span>Waiting: {data.telemetry.queue.waiting || 0}</span>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
<!-- Runtime & System -->
|
|
492
|
+
<div class="bg-slate-900 border border-slate-800 rounded-xl p-4 shadow-lg shadow-black/30">
|
|
493
|
+
<div class="flex items-center justify-between text-xs text-slate-400 font-mono mb-2">
|
|
494
|
+
<span>Node.js Process</span>
|
|
495
|
+
<span class="text-indigo-400 font-bold">PID {data.telemetry.pid || 0}</span>
|
|
496
|
+
</div>
|
|
497
|
+
<div class="text-2xl font-bold font-mono text-white mb-2">
|
|
498
|
+
{data.telemetry.uptimeSeconds || 0} <span class="text-xs font-normal text-slate-400">sec uptime</span>
|
|
499
|
+
</div>
|
|
500
|
+
<div class="w-full bg-slate-950 rounded-full h-1.5 overflow-hidden border border-slate-800">
|
|
501
|
+
<div class="bg-indigo-400 h-1.5 rounded-full" style="width: 100%;"></div>
|
|
502
|
+
</div>
|
|
503
|
+
<div class="mt-2 text-[10px] text-slate-500 font-mono flex justify-between">
|
|
504
|
+
<span>Node: {data.telemetry.nodeVersion || 'v20'}</span>
|
|
505
|
+
<span class="text-emerald-400">SSE Active</span>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<!-- Circuit Breakers Management Table -->
|
|
511
|
+
<div class="bg-slate-900 border border-slate-800 rounded-xl flex flex-col overflow-hidden shadow-xl shadow-black/40">
|
|
512
|
+
<div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between">
|
|
513
|
+
<div>
|
|
514
|
+
<h3 class="text-sm font-bold text-white font-mono flex items-center gap-2">
|
|
515
|
+
<span>🛡️</span> Circuit Breakers & Fault Tolerance
|
|
516
|
+
</h3>
|
|
517
|
+
<p class="text-xs text-slate-400 mt-0.5">Real-time circuit states, failure ratios, and manual trip recovery</p>
|
|
518
|
+
</div>
|
|
519
|
+
<button class="px-3 py-1 text-xs font-semibold rounded-lg border border-slate-700 bg-slate-800 hover:bg-slate-700 text-slate-200 transition flex items-center gap-1.5 shadow-sm">
|
|
520
|
+
<on_click action="REVALIDATE_API" tag="get_telemetry" />
|
|
521
|
+
<span>↻</span> Refresh Telemetry
|
|
522
|
+
</button>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
<div class="overflow-x-auto">
|
|
526
|
+
<table class="w-full text-left text-xs border-collapse font-mono">
|
|
527
|
+
<thead class="bg-slate-950/60 border-b border-slate-800 text-slate-400 text-[11px]">
|
|
528
|
+
<tr>
|
|
529
|
+
<th class="px-6 py-3">Breaker Name</th>
|
|
530
|
+
<th class="px-6 py-3">State</th>
|
|
531
|
+
<th class="px-6 py-3">Success / Failure</th>
|
|
532
|
+
<th class="px-6 py-3">Failure Rate</th>
|
|
533
|
+
<th class="px-6 py-3">Total Calls</th>
|
|
534
|
+
<th class="px-6 py-3 text-right">Action</th>
|
|
535
|
+
</tr>
|
|
536
|
+
</thead>
|
|
537
|
+
<tbody class="divide-y divide-slate-800/60 text-slate-300">
|
|
538
|
+
<if condition="!data.telemetry.breakers || data.telemetry.breakers.length === 0">
|
|
539
|
+
<tr>
|
|
540
|
+
<td class="text-center py-12 text-slate-500 font-sans" colspan="6">
|
|
541
|
+
No Circuit Breakers registered yet. They will appear here when services or handlers use defineService({ circuitBreaker: true }).
|
|
542
|
+
</td>
|
|
543
|
+
</tr>
|
|
544
|
+
</if>
|
|
545
|
+
<for_each items="{data.telemetry.breakers}" var="b" key="name">
|
|
546
|
+
<tr class="hover:bg-slate-850 transition">
|
|
547
|
+
<td class="px-6 py-3.5 font-bold text-white flex items-center gap-2">
|
|
548
|
+
<span class="w-2 h-2 rounded-full {b.state == 'OPEN' ? 'bg-rose-500 animate-ping' : (b.state == 'HALF_OPEN' ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500')}"></span>
|
|
549
|
+
{b.name}
|
|
550
|
+
</td>
|
|
551
|
+
<td class="px-6 py-3.5">
|
|
552
|
+
<span class="px-2 py-0.5 rounded text-[10px] font-semibold uppercase tracking-wider {b.state == 'OPEN' ? 'bg-rose-500/20 text-rose-300 border border-rose-500/30' : (b.state == 'HALF_OPEN' ? 'bg-amber-500/20 text-amber-300 border border-amber-500/30' : 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30')}">
|
|
553
|
+
{b.state}
|
|
554
|
+
</span>
|
|
555
|
+
</td>
|
|
556
|
+
<td class="px-6 py-3.5">
|
|
557
|
+
<span class="text-emerald-400 font-semibold">{b.successes || 0}</span> / <span class="text-rose-400 font-semibold">{b.failures || 0}</span>
|
|
558
|
+
</td>
|
|
559
|
+
<td class="px-6 py-3.5">
|
|
560
|
+
<span class="{b.failureRate > 0.5 ? 'text-rose-400 font-bold' : 'text-slate-400'}">
|
|
561
|
+
{(b.failureRate * 100).toFixed(1)}%
|
|
562
|
+
</span>
|
|
563
|
+
</td>
|
|
564
|
+
<td class="px-6 py-3.5 text-slate-300">
|
|
565
|
+
{b.totalCalls || 0}
|
|
566
|
+
</td>
|
|
567
|
+
<td class="px-6 py-3.5 text-right">
|
|
568
|
+
<button class="px-2.5 py-1 text-[11px] font-semibold rounded bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 hover:text-white transition">
|
|
569
|
+
<on_click action="XHR" revalidate="get_telemetry">
|
|
570
|
+
<url>/_studio/api/breakers/{b.name}/reset</url>
|
|
571
|
+
<method>POST</method>
|
|
572
|
+
</on_click>
|
|
573
|
+
Reset
|
|
574
|
+
</button>
|
|
575
|
+
</td>
|
|
576
|
+
</tr>
|
|
577
|
+
</for_each>
|
|
578
|
+
</tbody>
|
|
579
|
+
</table>
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
</section>
|
|
583
|
+
</if>
|
|
584
|
+
</main>
|
|
585
|
+
</div>
|
|
586
|
+
|
|
587
|
+
<!-- Declarative Modal Dialog -->
|
|
588
|
+
<if condition="data.isModalOpen">
|
|
589
|
+
<div class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
|
590
|
+
<div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg max-h-[85vh] flex flex-col overflow-hidden shadow-2xl shadow-black/80">
|
|
591
|
+
<div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between">
|
|
592
|
+
<h3 class="text-sm font-bold font-mono text-white">
|
|
593
|
+
Add Record to {data.activeTable}
|
|
594
|
+
</h3>
|
|
595
|
+
<button class="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-800 transition">
|
|
596
|
+
<on_click action="SET_STATE">
|
|
597
|
+
<path>data.isModalOpen</path>
|
|
598
|
+
<value>false</value>
|
|
599
|
+
</on_click>
|
|
600
|
+
✕
|
|
601
|
+
</button>
|
|
602
|
+
</div>
|
|
603
|
+
|
|
604
|
+
<div class="p-6 overflow-y-auto space-y-4">
|
|
605
|
+
<div>
|
|
606
|
+
<span class="block text-xs font-mono font-medium text-slate-300 mb-2">Available Columns:</span>
|
|
607
|
+
<div class="flex flex-wrap gap-1.5">
|
|
608
|
+
<for_each items="{data.columns}" var="field" key="name">
|
|
609
|
+
<span class="px-2 py-0.5 rounded text-[11px] font-mono bg-slate-950 border border-slate-800 text-slate-300">
|
|
610
|
+
{field.name} <span class="text-slate-500">({field.type})</span>
|
|
611
|
+
<if condition="field.isPrimaryKey">
|
|
612
|
+
<span class="text-amber-400 ml-0.5">🔑</span>
|
|
613
|
+
</if>
|
|
614
|
+
</span>
|
|
615
|
+
</for_each>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
<div>
|
|
620
|
+
<label class="block text-xs font-mono font-medium text-slate-300 mb-1.5">
|
|
621
|
+
Record JSON Payload:
|
|
622
|
+
</label>
|
|
623
|
+
<textarea bind="newRowJson" rows="8" placeholder="{\n "column_name": "value"\n}"
|
|
624
|
+
class="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-xs font-mono text-slate-200 placeholder-slate-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition resize-y"></textarea>
|
|
625
|
+
<p class="text-[11px] text-slate-500 mt-1.5 font-mono">
|
|
626
|
+
Provide field values in JSON. Primary keys can be omitted for auto-increment columns.
|
|
627
|
+
</p>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<div class="px-6 py-4 border-t border-slate-800 flex justify-end gap-2.5 bg-slate-900/50">
|
|
632
|
+
<button class="px-3.5 py-1.5 text-xs font-semibold rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 transition">
|
|
633
|
+
<on_click action="SET_STATE">
|
|
634
|
+
<path>data.isModalOpen</path>
|
|
635
|
+
<value>false</value>
|
|
636
|
+
</on_click>
|
|
637
|
+
Cancel
|
|
638
|
+
</button>
|
|
639
|
+
<button class="px-4 py-1.5 text-xs font-semibold rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white transition shadow-sm shadow-indigo-600/30">
|
|
640
|
+
<on_click action="XHR" revalidate="get_rows">
|
|
641
|
+
<url>/_studio/api/tables/{data.activeTable}/rows</url>
|
|
642
|
+
<method>POST</method>
|
|
643
|
+
<body>{data.newRowJson}</body>
|
|
644
|
+
</on_click>
|
|
645
|
+
<on_click action="REVALIDATE_API" tag="get_tables" />
|
|
646
|
+
<on_click action="SET_STATE">
|
|
647
|
+
<path>data.isModalOpen</path>
|
|
648
|
+
<value>false</value>
|
|
649
|
+
</on_click>
|
|
650
|
+
Save Record
|
|
651
|
+
</button>
|
|
652
|
+
</div>
|
|
653
|
+
</div>
|
|
654
|
+
</div>
|
|
655
|
+
</if>
|
|
656
|
+
</uid_spec>
|
|
657
|
+
</script>
|
|
658
|
+
|
|
659
|
+
<!-- Live SSE Telemetry Stream Bridge -->
|
|
660
|
+
<script>
|
|
661
|
+
if (typeof EventSource !== 'undefined') {
|
|
662
|
+
try {
|
|
663
|
+
var es = new EventSource('/_studio/api/live');
|
|
664
|
+
es.onmessage = function (ev) {
|
|
665
|
+
try {
|
|
666
|
+
var data = JSON.parse(ev.data);
|
|
667
|
+
if (window.EUIX && window.EUIX.setState) {
|
|
668
|
+
window.EUIX.setState('telemetry', data);
|
|
669
|
+
}
|
|
670
|
+
} catch (_) {}
|
|
671
|
+
};
|
|
672
|
+
} catch (_) {}
|
|
673
|
+
}
|
|
674
|
+
</script>
|
|
675
|
+
</body>
|
|
676
|
+
</html>`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
module.exports = {
|
|
680
|
+
renderStudioHtml,
|
|
681
|
+
};
|