lopata 0.0.1

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.
Files changed (147) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/runtime/bindings/ai.ts +132 -0
  4. package/runtime/bindings/analytics-engine.ts +96 -0
  5. package/runtime/bindings/browser.ts +64 -0
  6. package/runtime/bindings/cache.ts +179 -0
  7. package/runtime/bindings/cf-streams.ts +56 -0
  8. package/runtime/bindings/container-docker.ts +225 -0
  9. package/runtime/bindings/container.ts +662 -0
  10. package/runtime/bindings/crypto-extras.ts +89 -0
  11. package/runtime/bindings/d1.ts +315 -0
  12. package/runtime/bindings/do-executor-inprocess.ts +140 -0
  13. package/runtime/bindings/do-executor-worker.ts +368 -0
  14. package/runtime/bindings/do-executor.ts +45 -0
  15. package/runtime/bindings/do-websocket-bridge.ts +70 -0
  16. package/runtime/bindings/do-worker-entry.ts +220 -0
  17. package/runtime/bindings/do-worker-env.ts +74 -0
  18. package/runtime/bindings/durable-object.ts +992 -0
  19. package/runtime/bindings/email.ts +180 -0
  20. package/runtime/bindings/html-rewriter.ts +84 -0
  21. package/runtime/bindings/hyperdrive.ts +130 -0
  22. package/runtime/bindings/images.ts +381 -0
  23. package/runtime/bindings/kv.ts +359 -0
  24. package/runtime/bindings/queue.ts +507 -0
  25. package/runtime/bindings/r2.ts +759 -0
  26. package/runtime/bindings/rpc-stub.ts +267 -0
  27. package/runtime/bindings/scheduled.ts +172 -0
  28. package/runtime/bindings/service-binding.ts +217 -0
  29. package/runtime/bindings/static-assets.ts +481 -0
  30. package/runtime/bindings/websocket-pair.ts +182 -0
  31. package/runtime/bindings/workflow.ts +858 -0
  32. package/runtime/bunflare-config.ts +56 -0
  33. package/runtime/cli/cache.ts +39 -0
  34. package/runtime/cli/context.ts +105 -0
  35. package/runtime/cli/d1.ts +163 -0
  36. package/runtime/cli/dev.ts +392 -0
  37. package/runtime/cli/kv.ts +84 -0
  38. package/runtime/cli/queues.ts +109 -0
  39. package/runtime/cli/r2.ts +140 -0
  40. package/runtime/cli/traces.ts +251 -0
  41. package/runtime/cli.ts +102 -0
  42. package/runtime/config.ts +148 -0
  43. package/runtime/d1-migrate.ts +37 -0
  44. package/runtime/dashboard/api.ts +174 -0
  45. package/runtime/dashboard/app.tsx +220 -0
  46. package/runtime/dashboard/components/breadcrumb.tsx +16 -0
  47. package/runtime/dashboard/components/buttons.tsx +13 -0
  48. package/runtime/dashboard/components/code-block.tsx +5 -0
  49. package/runtime/dashboard/components/detail-field.tsx +8 -0
  50. package/runtime/dashboard/components/empty-state.tsx +8 -0
  51. package/runtime/dashboard/components/filter-input.tsx +11 -0
  52. package/runtime/dashboard/components/index.ts +16 -0
  53. package/runtime/dashboard/components/key-value-table.tsx +23 -0
  54. package/runtime/dashboard/components/modal.tsx +23 -0
  55. package/runtime/dashboard/components/page-header.tsx +11 -0
  56. package/runtime/dashboard/components/pill-button.tsx +14 -0
  57. package/runtime/dashboard/components/refresh-button.tsx +7 -0
  58. package/runtime/dashboard/components/service-info.tsx +45 -0
  59. package/runtime/dashboard/components/status-badge.tsx +7 -0
  60. package/runtime/dashboard/components/table-link.tsx +5 -0
  61. package/runtime/dashboard/components/table.tsx +26 -0
  62. package/runtime/dashboard/components.tsx +19 -0
  63. package/runtime/dashboard/index.html +23 -0
  64. package/runtime/dashboard/lib.ts +45 -0
  65. package/runtime/dashboard/rpc/client.ts +20 -0
  66. package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
  67. package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
  68. package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
  69. package/runtime/dashboard/rpc/handlers/config.ts +137 -0
  70. package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
  71. package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
  72. package/runtime/dashboard/rpc/handlers/do.ts +117 -0
  73. package/runtime/dashboard/rpc/handlers/email.ts +82 -0
  74. package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
  75. package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
  76. package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
  77. package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
  78. package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
  79. package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
  80. package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
  81. package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
  82. package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
  83. package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
  84. package/runtime/dashboard/rpc/hooks.ts +132 -0
  85. package/runtime/dashboard/rpc/server.ts +70 -0
  86. package/runtime/dashboard/rpc/types.ts +396 -0
  87. package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
  88. package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
  89. package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
  90. package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
  91. package/runtime/dashboard/sql-browser/hooks.ts +137 -0
  92. package/runtime/dashboard/sql-browser/index.ts +4 -0
  93. package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
  94. package/runtime/dashboard/sql-browser/modals.tsx +116 -0
  95. package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
  96. package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
  97. package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
  98. package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
  99. package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
  100. package/runtime/dashboard/sql-browser/types.ts +61 -0
  101. package/runtime/dashboard/sql-browser/utils.ts +167 -0
  102. package/runtime/dashboard/style.css +177 -0
  103. package/runtime/dashboard/views/ai.tsx +152 -0
  104. package/runtime/dashboard/views/analytics-engine.tsx +169 -0
  105. package/runtime/dashboard/views/cache.tsx +93 -0
  106. package/runtime/dashboard/views/containers.tsx +197 -0
  107. package/runtime/dashboard/views/d1.tsx +81 -0
  108. package/runtime/dashboard/views/do.tsx +168 -0
  109. package/runtime/dashboard/views/email.tsx +235 -0
  110. package/runtime/dashboard/views/errors.tsx +558 -0
  111. package/runtime/dashboard/views/home.tsx +287 -0
  112. package/runtime/dashboard/views/kv.tsx +273 -0
  113. package/runtime/dashboard/views/queue.tsx +193 -0
  114. package/runtime/dashboard/views/r2.tsx +202 -0
  115. package/runtime/dashboard/views/scheduled.tsx +89 -0
  116. package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
  117. package/runtime/dashboard/views/traces.tsx +768 -0
  118. package/runtime/dashboard/views/workers.tsx +55 -0
  119. package/runtime/dashboard/views/workflows.tsx +473 -0
  120. package/runtime/db.ts +258 -0
  121. package/runtime/env.ts +362 -0
  122. package/runtime/error-page/app.tsx +394 -0
  123. package/runtime/error-page/build.ts +269 -0
  124. package/runtime/error-page/index.html +16 -0
  125. package/runtime/error-page/style.css +31 -0
  126. package/runtime/execution-context.ts +18 -0
  127. package/runtime/file-watcher.ts +57 -0
  128. package/runtime/generation-manager.ts +230 -0
  129. package/runtime/generation.ts +411 -0
  130. package/runtime/plugin.ts +292 -0
  131. package/runtime/request-cf.ts +28 -0
  132. package/runtime/rpc-validate.ts +154 -0
  133. package/runtime/tracing/context.ts +40 -0
  134. package/runtime/tracing/db.ts +73 -0
  135. package/runtime/tracing/frames.ts +75 -0
  136. package/runtime/tracing/instrument.ts +186 -0
  137. package/runtime/tracing/span.ts +138 -0
  138. package/runtime/tracing/store.ts +499 -0
  139. package/runtime/tracing/types.ts +47 -0
  140. package/runtime/vite-plugin/config-plugin.ts +68 -0
  141. package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
  142. package/runtime/vite-plugin/dist/index.mjs +52333 -0
  143. package/runtime/vite-plugin/globals-plugin.ts +94 -0
  144. package/runtime/vite-plugin/index.ts +43 -0
  145. package/runtime/vite-plugin/modules-plugin.ts +88 -0
  146. package/runtime/vite-plugin/react-router-plugin.ts +95 -0
  147. package/runtime/worker-registry.ts +52 -0
@@ -0,0 +1,169 @@
1
+ import { useState } from "preact/hooks";
2
+ import { formatTime } from "../lib";
3
+ import { useQuery, useMutation } from "../rpc/hooks";
4
+ import { EmptyState, Table, PageHeader, DeleteButton, ServiceInfo, RefreshButton } from "../components";
5
+ import type { AnalyticsEngineDataPoint } from "../rpc/types";
6
+
7
+ export function AnalyticsEngineView({ route }: { route: string }) {
8
+ const parts = route.split("/").filter(Boolean);
9
+ if (parts.length >= 2) return <DataPointDetail id={parts[1]!} />;
10
+ return <DataPointList />;
11
+ }
12
+
13
+ function DataPointList() {
14
+ const [datasetFilter, setDatasetFilter] = useState("");
15
+ const { data: points, refetch } = useQuery("analyticsEngine.list", {
16
+ dataset: datasetFilter || undefined,
17
+ });
18
+ const { data: stats } = useQuery("analyticsEngine.stats");
19
+ const { data: datasets } = useQuery("analyticsEngine.datasets");
20
+ const { data: configGroups } = useQuery("config.forService", { type: "analytics_engine" });
21
+ const deletePoint = useMutation("analyticsEngine.delete");
22
+
23
+ const handleDelete = async (id: string) => {
24
+ if (!confirm("Delete this data point?")) return;
25
+ await deletePoint.mutate({ id });
26
+ refetch();
27
+ };
28
+
29
+ return (
30
+ <div class="p-8 max-w-6xl">
31
+ <PageHeader title="Analytics Engine" subtitle={`${stats?.total ?? 0} data point(s)`} actions={<RefreshButton onClick={refetch} />} />
32
+ <div class="flex gap-6 items-start">
33
+ <div class="flex-1 min-w-0">
34
+ <div class="mb-6 flex gap-2 items-center flex-wrap">
35
+ {(datasets?.length ?? 0) > 0 && (
36
+ <select
37
+ value={datasetFilter}
38
+ onChange={e => setDatasetFilter((e.target as HTMLSelectElement).value)}
39
+ class="text-xs bg-panel-secondary border border-border rounded-md px-2 py-1 outline-none"
40
+ >
41
+ <option value="">All datasets</option>
42
+ {datasets!.map(d => (
43
+ <option key={d} value={d}>{d}</option>
44
+ ))}
45
+ </select>
46
+ )}
47
+ </div>
48
+ {!points?.length ? (
49
+ <EmptyState message="No data points found" />
50
+ ) : (
51
+ <Table
52
+ headers={["Dataset", "Index", "Doubles", "Blobs", "Time", ""]}
53
+ rows={points.map(p => [
54
+ <a href={`#/analytics/${p.id}`} class="font-mono text-xs text-blue-600 hover:underline">{p.dataset}</a>,
55
+ <span class="font-mono text-xs text-text-muted">{p.index1 ?? "-"}</span>,
56
+ <span class="text-xs text-text-muted tabular-nums">{formatDoubles(p)}</span>,
57
+ <span class="text-xs text-text-muted truncate max-w-[150px] block">{formatBlobs(p)}</span>,
58
+ <span class="text-xs text-text-muted">{formatTime(p.timestamp)}</span>,
59
+ <DeleteButton onClick={() => handleDelete(p.id)} />,
60
+ ])}
61
+ />
62
+ )}
63
+ </div>
64
+ <ServiceInfo
65
+ description="Analytics Engine — write-only data point storage for custom metrics, events, and clickstream data."
66
+ stats={[
67
+ { label: "Total", value: stats?.total ?? 0 },
68
+ ...(stats?.byDataset ? Object.entries(stats.byDataset).map(([k, v]) => ({ label: k, value: v })) : []),
69
+ ]}
70
+ configGroups={configGroups}
71
+ links={[
72
+ { label: "Analytics Engine docs", href: "https://developers.cloudflare.com/analytics/analytics-engine/" },
73
+ { label: "SQL API", href: "https://developers.cloudflare.com/analytics/analytics-engine/sql-api/" },
74
+ ]}
75
+ />
76
+ </div>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ function DataPointDetail({ id }: { id: string }) {
82
+ const { data } = useQuery("analyticsEngine.get", { id });
83
+
84
+ if (!data) {
85
+ return (
86
+ <div class="p-8">
87
+ <a href="#/analytics" class="text-sm text-blue-600 hover:underline mb-4 inline-block">Back to data points</a>
88
+ <EmptyState message="Data point not found" />
89
+ </div>
90
+ );
91
+ }
92
+
93
+ const doubles: { name: string; value: number }[] = [];
94
+ const blobs: { name: string; value: string | null }[] = [];
95
+ const row = data as unknown as Record<string, unknown>;
96
+ for (let i = 1; i <= 20; i++) {
97
+ const dv = row[`double${i}`] as number | null;
98
+ if (dv != null) doubles.push({ name: `double${i}`, value: dv });
99
+ const bv = row[`blob${i}`] as string | null;
100
+ if (bv != null) blobs.push({ name: `blob${i}`, value: bv });
101
+ }
102
+
103
+ return (
104
+ <div class="p-8 max-w-4xl">
105
+ <a href="#/analytics" class="text-sm text-blue-600 hover:underline mb-4 inline-block">Back to data points</a>
106
+ <div class="bg-panel border border-border rounded-lg p-6">
107
+ <h2 class="text-lg font-semibold text-ink mb-4">Data Point Detail</h2>
108
+ <div class="grid grid-cols-2 gap-4 mb-4 text-sm">
109
+ <div>
110
+ <span class="text-text-muted">Dataset:</span>{" "}
111
+ <span class="font-mono">{data.dataset}</span>
112
+ </div>
113
+ <div>
114
+ <span class="text-text-muted">Time:</span>{" "}
115
+ {formatTime(data.timestamp)}
116
+ </div>
117
+ <div>
118
+ <span class="text-text-muted">Index:</span>{" "}
119
+ <span class="font-mono">{data.index1 ?? "-"}</span>
120
+ </div>
121
+ <div>
122
+ <span class="text-text-muted">Sample interval:</span>{" "}
123
+ <span class="tabular-nums">{data._sample_interval}</span>
124
+ </div>
125
+ </div>
126
+ {doubles.length > 0 && (
127
+ <div class="mb-4">
128
+ <div class="text-xs text-text-muted mb-2">Doubles</div>
129
+ <div class="bg-panel-secondary border border-border rounded-lg p-4">
130
+ <div class="grid grid-cols-4 gap-2 text-sm">
131
+ {doubles.map(d => (
132
+ <div key={d.name}>
133
+ <span class="text-text-muted text-xs">{d.name}:</span>{" "}
134
+ <span class="font-mono tabular-nums">{d.value}</span>
135
+ </div>
136
+ ))}
137
+ </div>
138
+ </div>
139
+ </div>
140
+ )}
141
+ {blobs.length > 0 && (
142
+ <div>
143
+ <div class="text-xs text-text-muted mb-2">Blobs</div>
144
+ <div class="bg-panel-secondary border border-border rounded-lg p-4">
145
+ <div class="space-y-1 text-sm">
146
+ {blobs.map(b => (
147
+ <div key={b.name}>
148
+ <span class="text-text-muted text-xs">{b.name}:</span>{" "}
149
+ <span class="font-mono">{b.value}</span>
150
+ </div>
151
+ ))}
152
+ </div>
153
+ </div>
154
+ </div>
155
+ )}
156
+ </div>
157
+ </div>
158
+ );
159
+ }
160
+
161
+ function formatDoubles(p: AnalyticsEngineDataPoint): string {
162
+ const vals = [p.double1, p.double2, p.double3, p.double4, p.double5].filter((v): v is number => v != null);
163
+ return vals.length ? vals.join(", ") : "-";
164
+ }
165
+
166
+ function formatBlobs(p: AnalyticsEngineDataPoint): string {
167
+ const vals = [p.blob1, p.blob2, p.blob3, p.blob4, p.blob5].filter((v): v is string => v != null);
168
+ return vals.length ? vals.join(", ") : "-";
169
+ }
@@ -0,0 +1,93 @@
1
+ import { useQuery, useMutation } from "../rpc/hooks";
2
+ import { EmptyState, Breadcrumb, Table, PageHeader, DeleteButton, TableLink, ServiceInfo, RefreshButton } from "../components";
3
+
4
+ const HTTP_STATUS_COLORS: Record<string, string> = {
5
+ "2xx": "bg-emerald-100 text-emerald-700",
6
+ "3xx": "bg-amber-100 text-amber-700",
7
+ "4xx": "bg-red-100 text-red-700",
8
+ "5xx": "bg-red-100 text-red-700",
9
+ };
10
+
11
+ function httpStatusColor(status: number): string {
12
+ if (status < 300) return HTTP_STATUS_COLORS["2xx"]!;
13
+ if (status < 400) return HTTP_STATUS_COLORS["3xx"]!;
14
+ return HTTP_STATUS_COLORS["4xx"]!;
15
+ }
16
+
17
+ export function CacheView({ route }: { route: string }) {
18
+ const parts = route.split("/").filter(Boolean);
19
+ if (parts.length === 1) return <CacheNameList />;
20
+ if (parts.length >= 2) return <CacheEntryList name={decodeURIComponent(parts[1]!)} />;
21
+ return null;
22
+ }
23
+
24
+ function CacheNameList() {
25
+ const { data: caches, refetch } = useQuery("cache.listCaches");
26
+
27
+ const totalEntries = caches?.reduce((s, c) => s + c.count, 0) ?? 0;
28
+
29
+ return (
30
+ <div class="p-8 max-w-6xl">
31
+ <PageHeader title="Cache" subtitle={`${caches?.length ?? 0} cache(s)`} actions={<RefreshButton onClick={refetch} />} />
32
+ <div class="flex gap-6 items-start">
33
+ <div class="flex-1 min-w-0">
34
+ {!caches?.length ? (
35
+ <EmptyState message="No cache entries found" />
36
+ ) : (
37
+ <Table
38
+ headers={["Cache Name", "Entries"]}
39
+ rows={caches.map(c => [
40
+ <TableLink href={`#/cache/${encodeURIComponent(c.cache_name)}`}>{c.cache_name}</TableLink>,
41
+ <span class="tabular-nums">{c.count}</span>,
42
+ ])}
43
+ />
44
+ )}
45
+ </div>
46
+ <ServiceInfo
47
+ description="Cache API for programmatic HTTP response caching."
48
+ stats={[
49
+ { label: "Caches", value: caches?.length ?? 0 },
50
+ { label: "Entries", value: totalEntries.toLocaleString() },
51
+ ]}
52
+ links={[
53
+ { label: "Documentation", href: "https://developers.cloudflare.com/cache/" },
54
+ { label: "API Reference", href: "https://developers.cloudflare.com/api/resources/cache/" },
55
+ ]}
56
+ />
57
+ </div>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ function CacheEntryList({ name }: { name: string }) {
63
+ const { data: entries, refetch } = useQuery("cache.listEntries", { name });
64
+ const deleteEntry = useMutation("cache.deleteEntry");
65
+
66
+ const handleDelete = async (url: string) => {
67
+ if (!confirm(`Delete cache entry for "${url}"?`)) return;
68
+ await deleteEntry.mutate({ name, url });
69
+ refetch();
70
+ };
71
+
72
+ return (
73
+ <div class="p-8">
74
+ <Breadcrumb items={[{ label: "Cache", href: "#/cache" }, { label: name }]} />
75
+ <div class="mb-6 flex justify-end">
76
+ <RefreshButton onClick={refetch} />
77
+ </div>
78
+ {!entries?.length ? (
79
+ <EmptyState message="No cache entries" />
80
+ ) : (
81
+ <Table
82
+ headers={["URL", "Status", "Expires", ""]}
83
+ rows={entries.map(e => [
84
+ <span class="font-mono text-xs max-w-md truncate block">{e.url}</span>,
85
+ <span class={`inline-flex px-2 py-0.5 rounded-md text-xs font-medium ${httpStatusColor(e.status)}`}>{e.status}</span>,
86
+ e.expires_at ? new Date(e.expires_at * 1000).toLocaleString() : "—",
87
+ <DeleteButton onClick={() => handleDelete(e.url)} />,
88
+ ])}
89
+ />
90
+ )}
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,197 @@
1
+ import { useState } from "preact/hooks";
2
+ import { useQuery, useMutation } from "../rpc/hooks";
3
+ import { EmptyState, Breadcrumb, Table, PageHeader, TableLink, StatusBadge, ServiceInfo, RefreshButton, DetailField, CodeBlock } from "../components";
4
+
5
+ const CONTAINER_STATE_COLORS: Record<string, string> = {
6
+ running: "bg-emerald-100 text-emerald-700",
7
+ healthy: "bg-emerald-100 text-emerald-700",
8
+ exited: "bg-panel-active text-text-data",
9
+ stopped: "bg-panel-active text-text-data",
10
+ created: "bg-accent-blue text-ink",
11
+ paused: "bg-yellow-100 text-yellow-700",
12
+ dead: "bg-red-100 text-red-700",
13
+ };
14
+
15
+ export function ContainersView({ route }: { route: string }) {
16
+ const parts = route.split("/").filter(Boolean);
17
+ if (parts.length === 1) return <ContainerList />;
18
+ if (parts.length === 2) return <ContainerInstanceList className={decodeURIComponent(parts[1]!)} />;
19
+ if (parts.length >= 3) return <ContainerDetailView className={decodeURIComponent(parts[1]!)} id={decodeURIComponent(parts[2]!)} />;
20
+ return null;
21
+ }
22
+
23
+ function ContainerList() {
24
+ const { data: containers, refetch } = useQuery("containers.list");
25
+ const { data: configGroups } = useQuery("config.forService", { type: "containers" });
26
+
27
+ const totalInstances = containers?.reduce((s, c) => s + c.instanceCount, 0) ?? 0;
28
+ const totalRunning = containers?.reduce((s, c) => s + c.runningCount, 0) ?? 0;
29
+
30
+ return (
31
+ <div class="p-8 max-w-6xl">
32
+ <PageHeader title="Containers" subtitle={`${containers?.length ?? 0} container class(es)`} actions={<RefreshButton onClick={refetch} />} />
33
+ <div class="flex gap-6 items-start">
34
+ <div class="flex-1 min-w-0">
35
+ {!containers?.length ? (
36
+ <EmptyState message="No containers configured" />
37
+ ) : (
38
+ <Table
39
+ headers={["Class Name", "Image", "Max Instances", "Instances", "Running"]}
40
+ rows={containers.map(c => [
41
+ <TableLink href={`#/containers/${encodeURIComponent(c.className)}`}>{c.className}</TableLink>,
42
+ <span class="font-mono text-xs">{c.image}</span>,
43
+ c.maxInstances ?? "unlimited",
44
+ <span class="tabular-nums">{c.instanceCount}</span>,
45
+ <span class={`tabular-nums font-medium ${c.runningCount > 0 ? "text-emerald-600" : ""}`}>{c.runningCount}</span>,
46
+ ])}
47
+ />
48
+ )}
49
+ </div>
50
+ <ServiceInfo
51
+ description="Docker-backed container instances managed as Durable Objects."
52
+ stats={[
53
+ { label: "Classes", value: String(containers?.length ?? 0) },
54
+ { label: "Instances", value: String(totalInstances) },
55
+ { label: "Running", value: String(totalRunning) },
56
+ ]}
57
+ configGroups={configGroups}
58
+ links={[
59
+ { label: "Documentation", href: "https://developers.cloudflare.com/containers/" },
60
+ ]}
61
+ />
62
+ </div>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ function ContainerInstanceList({ className }: { className: string }) {
68
+ const { data: instances, refetch } = useQuery("containers.listInstances", { className });
69
+
70
+ return (
71
+ <div class="p-8">
72
+ <Breadcrumb items={[{ label: "Containers", href: "#/containers" }, { label: className }]} />
73
+ <div class="mb-6 flex justify-end">
74
+ <RefreshButton onClick={refetch} />
75
+ </div>
76
+ {!instances?.length ? (
77
+ <EmptyState message="No container instances found" />
78
+ ) : (
79
+ <Table
80
+ headers={["Instance", "Docker State", "Ports"]}
81
+ rows={instances.map(inst => [
82
+ <div>
83
+ <TableLink href={`#/containers/${encodeURIComponent(className)}/${encodeURIComponent(inst.id)}`} mono>
84
+ {inst.containerName}
85
+ </TableLink>
86
+ {inst.doName && <span class="text-text-muted text-xs ml-2">({inst.doName})</span>}
87
+ </div>,
88
+ <StatusBadge status={inst.state} colorMap={CONTAINER_STATE_COLORS} />,
89
+ Object.keys(inst.ports).length > 0
90
+ ? <span class="font-mono text-xs">{Object.entries(inst.ports).map(([k, v]) => `${v}->${k}`).join(", ")}</span>
91
+ : <span class="text-text-muted">—</span>,
92
+ ])}
93
+ />
94
+ )}
95
+ </div>
96
+ );
97
+ }
98
+
99
+ function ContainerDetailView({ className, id }: { className: string; id: string }) {
100
+ const [tail, setTail] = useState(100);
101
+ const { data: detail, refetch: refetchDetail } = useQuery("containers.getDetail", { className, id });
102
+ const { data: logsData, refetch: refetchLogs } = useQuery("containers.getLogs", { className, id, tail });
103
+ const stopMutation = useMutation("containers.stop");
104
+ const destroyMutation = useMutation("containers.destroy");
105
+
106
+ const refetch = () => {
107
+ refetchDetail();
108
+ refetchLogs();
109
+ };
110
+
111
+ if (!detail) return <div class="p-8 text-text-muted font-medium">Loading...</div>;
112
+
113
+ const isRunning = detail.state === "running" || detail.state === "healthy";
114
+
115
+ const handleStop = async () => {
116
+ if (!confirm("Stop this container?")) return;
117
+ await stopMutation.mutate({ className, id });
118
+ refetch();
119
+ };
120
+
121
+ const handleDestroy = async () => {
122
+ if (!confirm("Force remove this container? This cannot be undone.")) return;
123
+ await destroyMutation.mutate({ className, id });
124
+ refetch();
125
+ };
126
+
127
+ return (
128
+ <div class="p-8 max-w-5xl">
129
+ <Breadcrumb items={[
130
+ { label: "Containers", href: "#/containers" },
131
+ { label: className, href: `#/containers/${encodeURIComponent(className)}` },
132
+ { label: id.slice(0, 16) + "..." },
133
+ ]} />
134
+
135
+ {/* Status + Actions */}
136
+ <div class="mb-6 flex items-center gap-4">
137
+ <StatusBadge status={detail.state} colorMap={CONTAINER_STATE_COLORS} />
138
+ {detail.exitCode !== null && (
139
+ <span class="text-sm text-text-muted">Exit code: <span class="font-mono">{detail.exitCode}</span></span>
140
+ )}
141
+ <div class="flex-1" />
142
+ {isRunning && (
143
+ <button
144
+ onClick={handleStop}
145
+ disabled={stopMutation.isLoading}
146
+ class="px-3 py-1.5 text-sm rounded-md bg-yellow-100 text-yellow-800 hover:bg-yellow-200 border border-yellow-300 disabled:opacity-50"
147
+ >
148
+ {stopMutation.isLoading ? "Stopping..." : "Stop"}
149
+ </button>
150
+ )}
151
+ <button
152
+ onClick={handleDestroy}
153
+ disabled={destroyMutation.isLoading}
154
+ class="px-3 py-1.5 text-sm rounded-md bg-red-100 text-red-800 hover:bg-red-200 border border-red-300 disabled:opacity-50"
155
+ >
156
+ {destroyMutation.isLoading ? "Removing..." : "Force Remove"}
157
+ </button>
158
+ <RefreshButton onClick={refetch} />
159
+ </div>
160
+
161
+ {/* Info Grid */}
162
+ <div class="grid grid-cols-2 gap-4 mb-8">
163
+ <DetailField label="Container Name" value={detail.containerName} />
164
+ <DetailField label="Instance ID"><span class="font-mono text-sm font-medium break-all">{detail.id}</span></DetailField>
165
+ <DetailField label="Image" value={detail.image || "—"} />
166
+ <DetailField label="DO Name" value={detail.doName || "—"} />
167
+ <DetailField label="Ports">
168
+ {Object.keys(detail.ports).length > 0
169
+ ? <span class="font-mono text-sm">{Object.entries(detail.ports).map(([k, v]) => `${v} -> ${k}`).join(", ")}</span>
170
+ : <span class="text-text-muted">No ports mapped</span>
171
+ }
172
+ </DetailField>
173
+ <DetailField label="Default Port" value={String(detail.config.defaultPort)} />
174
+ <DetailField label="Sleep After" value={detail.config.sleepAfter != null ? String(detail.config.sleepAfter) : "disabled"} />
175
+ <DetailField label="Internet" value={detail.config.enableInternet ? "enabled" : "disabled"} />
176
+ <DetailField label="Ping Endpoint" value={detail.config.pingEndpoint} />
177
+ </div>
178
+
179
+ {/* Logs */}
180
+ <div class="mb-4 flex items-center gap-3">
181
+ <h3 class="text-lg font-semibold text-text-data">Logs</h3>
182
+ <select
183
+ value={tail}
184
+ onChange={(e) => setTail(Number((e.target as HTMLSelectElement).value))}
185
+ class="text-sm border border-border rounded px-2 py-1 bg-panel text-text-data"
186
+ >
187
+ <option value={50}>Last 50 lines</option>
188
+ <option value={100}>Last 100 lines</option>
189
+ <option value={500}>Last 500 lines</option>
190
+ <option value={1000}>Last 1000 lines</option>
191
+ </select>
192
+ <RefreshButton onClick={refetchLogs} />
193
+ </div>
194
+ <CodeBlock>{logsData?.logs || "(no logs)"}</CodeBlock>
195
+ </div>
196
+ );
197
+ }
@@ -0,0 +1,81 @@
1
+ import { useQuery } from "../rpc/hooks";
2
+ import { rpc } from "../rpc/client";
3
+ import { Breadcrumb, PageHeader, Table, TableLink, ServiceInfo, EmptyState, SqlBrowser, RefreshButton } from "../components";
4
+ import { parseHashRoute } from "../lib";
5
+ import type { Tab } from "../sql-browser/index";
6
+
7
+ export function D1View({ route }: { route: string }) {
8
+ const { segments, query } = parseHashRoute(route);
9
+ if (segments.length <= 1) return <D1DatabaseList />;
10
+ const dbName = decodeURIComponent(segments[1]!);
11
+ const rawTab = segments[2] as Tab | undefined;
12
+ const tab: Tab = rawTab === "schema" || rawTab === "sql" ? rawTab : "data";
13
+ const tableName = segments[3] ? decodeURIComponent(segments[3]) : null;
14
+ const basePath = `/d1/${encodeURIComponent(dbName)}`;
15
+ return <D1DatabaseDetail dbName={dbName} basePath={basePath} routeTab={tab} routeTable={tableName} routeQuery={query} />;
16
+ }
17
+
18
+ function D1DatabaseList() {
19
+ const { data: databases, refetch } = useQuery("d1.listDatabases");
20
+ const { data: configGroups } = useQuery("config.forService", { type: "d1" });
21
+
22
+ const totalTables = databases?.reduce((s, db) => s + db.tables, 0) ?? 0;
23
+
24
+ return (
25
+ <div class="p-8 max-w-6xl">
26
+ <PageHeader title="D1 Databases" subtitle={`${databases?.length ?? 0} database(s)`} actions={<RefreshButton onClick={refetch} />} />
27
+ <div class="flex gap-6 items-start">
28
+ <div class="flex-1 min-w-0">
29
+ {!databases?.length ? (
30
+ <EmptyState message="No D1 databases found" />
31
+ ) : (
32
+ <Table
33
+ headers={["Database", "Tables"]}
34
+ rows={databases.map(db => [
35
+ <TableLink href={`#/d1/${encodeURIComponent(db.name)}`}>{db.name}</TableLink>,
36
+ <span class="tabular-nums">{db.tables}</span>,
37
+ ])}
38
+ />
39
+ )}
40
+ </div>
41
+ <ServiceInfo
42
+ description="Serverless SQLite databases at the edge."
43
+ stats={[
44
+ { label: "Databases", value: databases?.length ?? 0 },
45
+ { label: "Tables", value: totalTables.toLocaleString() },
46
+ ]}
47
+ configGroups={configGroups}
48
+ links={[
49
+ { label: "Documentation", href: "https://developers.cloudflare.com/d1/" },
50
+ { label: "API Reference", href: "https://developers.cloudflare.com/api/resources/d1/" },
51
+ ]}
52
+ />
53
+ </div>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ function D1DatabaseDetail({ dbName, basePath, routeTab, routeTable, routeQuery }: {
59
+ dbName: string;
60
+ basePath: string;
61
+ routeTab: Tab;
62
+ routeTable: string | null;
63
+ routeQuery: URLSearchParams;
64
+ }) {
65
+ const { data: tables } = useQuery("d1.listTables", { dbName });
66
+
67
+ return (
68
+ <div class="p-8">
69
+ <Breadcrumb items={[{ label: "D1", href: "#/d1" }, { label: dbName }]} />
70
+ <SqlBrowser
71
+ tables={tables}
72
+ execQuery={(sql) => rpc("d1.query", { dbName, sql })}
73
+ historyScope={`d1:${dbName}`}
74
+ basePath={basePath}
75
+ routeTab={routeTab}
76
+ routeTable={routeTable}
77
+ routeQuery={routeQuery}
78
+ />
79
+ </div>
80
+ );
81
+ }