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.
- package/README.md +15 -0
- package/package.json +51 -0
- package/runtime/bindings/ai.ts +132 -0
- package/runtime/bindings/analytics-engine.ts +96 -0
- package/runtime/bindings/browser.ts +64 -0
- package/runtime/bindings/cache.ts +179 -0
- package/runtime/bindings/cf-streams.ts +56 -0
- package/runtime/bindings/container-docker.ts +225 -0
- package/runtime/bindings/container.ts +662 -0
- package/runtime/bindings/crypto-extras.ts +89 -0
- package/runtime/bindings/d1.ts +315 -0
- package/runtime/bindings/do-executor-inprocess.ts +140 -0
- package/runtime/bindings/do-executor-worker.ts +368 -0
- package/runtime/bindings/do-executor.ts +45 -0
- package/runtime/bindings/do-websocket-bridge.ts +70 -0
- package/runtime/bindings/do-worker-entry.ts +220 -0
- package/runtime/bindings/do-worker-env.ts +74 -0
- package/runtime/bindings/durable-object.ts +992 -0
- package/runtime/bindings/email.ts +180 -0
- package/runtime/bindings/html-rewriter.ts +84 -0
- package/runtime/bindings/hyperdrive.ts +130 -0
- package/runtime/bindings/images.ts +381 -0
- package/runtime/bindings/kv.ts +359 -0
- package/runtime/bindings/queue.ts +507 -0
- package/runtime/bindings/r2.ts +759 -0
- package/runtime/bindings/rpc-stub.ts +267 -0
- package/runtime/bindings/scheduled.ts +172 -0
- package/runtime/bindings/service-binding.ts +217 -0
- package/runtime/bindings/static-assets.ts +481 -0
- package/runtime/bindings/websocket-pair.ts +182 -0
- package/runtime/bindings/workflow.ts +858 -0
- package/runtime/bunflare-config.ts +56 -0
- package/runtime/cli/cache.ts +39 -0
- package/runtime/cli/context.ts +105 -0
- package/runtime/cli/d1.ts +163 -0
- package/runtime/cli/dev.ts +392 -0
- package/runtime/cli/kv.ts +84 -0
- package/runtime/cli/queues.ts +109 -0
- package/runtime/cli/r2.ts +140 -0
- package/runtime/cli/traces.ts +251 -0
- package/runtime/cli.ts +102 -0
- package/runtime/config.ts +148 -0
- package/runtime/d1-migrate.ts +37 -0
- package/runtime/dashboard/api.ts +174 -0
- package/runtime/dashboard/app.tsx +220 -0
- package/runtime/dashboard/components/breadcrumb.tsx +16 -0
- package/runtime/dashboard/components/buttons.tsx +13 -0
- package/runtime/dashboard/components/code-block.tsx +5 -0
- package/runtime/dashboard/components/detail-field.tsx +8 -0
- package/runtime/dashboard/components/empty-state.tsx +8 -0
- package/runtime/dashboard/components/filter-input.tsx +11 -0
- package/runtime/dashboard/components/index.ts +16 -0
- package/runtime/dashboard/components/key-value-table.tsx +23 -0
- package/runtime/dashboard/components/modal.tsx +23 -0
- package/runtime/dashboard/components/page-header.tsx +11 -0
- package/runtime/dashboard/components/pill-button.tsx +14 -0
- package/runtime/dashboard/components/refresh-button.tsx +7 -0
- package/runtime/dashboard/components/service-info.tsx +45 -0
- package/runtime/dashboard/components/status-badge.tsx +7 -0
- package/runtime/dashboard/components/table-link.tsx +5 -0
- package/runtime/dashboard/components/table.tsx +26 -0
- package/runtime/dashboard/components.tsx +19 -0
- package/runtime/dashboard/index.html +23 -0
- package/runtime/dashboard/lib.ts +45 -0
- package/runtime/dashboard/rpc/client.ts +20 -0
- package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
- package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
- package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
- package/runtime/dashboard/rpc/handlers/config.ts +137 -0
- package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
- package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
- package/runtime/dashboard/rpc/handlers/do.ts +117 -0
- package/runtime/dashboard/rpc/handlers/email.ts +82 -0
- package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
- package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
- package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
- package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
- package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
- package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
- package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
- package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
- package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
- package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
- package/runtime/dashboard/rpc/hooks.ts +132 -0
- package/runtime/dashboard/rpc/server.ts +70 -0
- package/runtime/dashboard/rpc/types.ts +396 -0
- package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
- package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
- package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
- package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
- package/runtime/dashboard/sql-browser/hooks.ts +137 -0
- package/runtime/dashboard/sql-browser/index.ts +4 -0
- package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
- package/runtime/dashboard/sql-browser/modals.tsx +116 -0
- package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
- package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
- package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
- package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
- package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
- package/runtime/dashboard/sql-browser/types.ts +61 -0
- package/runtime/dashboard/sql-browser/utils.ts +167 -0
- package/runtime/dashboard/style.css +177 -0
- package/runtime/dashboard/views/ai.tsx +152 -0
- package/runtime/dashboard/views/analytics-engine.tsx +169 -0
- package/runtime/dashboard/views/cache.tsx +93 -0
- package/runtime/dashboard/views/containers.tsx +197 -0
- package/runtime/dashboard/views/d1.tsx +81 -0
- package/runtime/dashboard/views/do.tsx +168 -0
- package/runtime/dashboard/views/email.tsx +235 -0
- package/runtime/dashboard/views/errors.tsx +558 -0
- package/runtime/dashboard/views/home.tsx +287 -0
- package/runtime/dashboard/views/kv.tsx +273 -0
- package/runtime/dashboard/views/queue.tsx +193 -0
- package/runtime/dashboard/views/r2.tsx +202 -0
- package/runtime/dashboard/views/scheduled.tsx +89 -0
- package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
- package/runtime/dashboard/views/traces.tsx +768 -0
- package/runtime/dashboard/views/workers.tsx +55 -0
- package/runtime/dashboard/views/workflows.tsx +473 -0
- package/runtime/db.ts +258 -0
- package/runtime/env.ts +362 -0
- package/runtime/error-page/app.tsx +394 -0
- package/runtime/error-page/build.ts +269 -0
- package/runtime/error-page/index.html +16 -0
- package/runtime/error-page/style.css +31 -0
- package/runtime/execution-context.ts +18 -0
- package/runtime/file-watcher.ts +57 -0
- package/runtime/generation-manager.ts +230 -0
- package/runtime/generation.ts +411 -0
- package/runtime/plugin.ts +292 -0
- package/runtime/request-cf.ts +28 -0
- package/runtime/rpc-validate.ts +154 -0
- package/runtime/tracing/context.ts +40 -0
- package/runtime/tracing/db.ts +73 -0
- package/runtime/tracing/frames.ts +75 -0
- package/runtime/tracing/instrument.ts +186 -0
- package/runtime/tracing/span.ts +138 -0
- package/runtime/tracing/store.ts +499 -0
- package/runtime/tracing/types.ts +47 -0
- package/runtime/vite-plugin/config-plugin.ts +68 -0
- package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
- package/runtime/vite-plugin/dist/index.mjs +52333 -0
- package/runtime/vite-plugin/globals-plugin.ts +94 -0
- package/runtime/vite-plugin/index.ts +43 -0
- package/runtime/vite-plugin/modules-plugin.ts +88 -0
- package/runtime/vite-plugin/react-router-plugin.ts +95 -0
- package/runtime/worker-registry.ts +52 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Re-export all shared UI components from the components/ directory
|
|
2
|
+
export {
|
|
3
|
+
Modal,
|
|
4
|
+
EmptyState,
|
|
5
|
+
PageHeader,
|
|
6
|
+
Breadcrumb,
|
|
7
|
+
Table,
|
|
8
|
+
DetailField,
|
|
9
|
+
CodeBlock,
|
|
10
|
+
FilterInput,
|
|
11
|
+
PillButton,
|
|
12
|
+
RefreshButton,
|
|
13
|
+
LoadMoreButton,
|
|
14
|
+
DeleteButton,
|
|
15
|
+
TableLink,
|
|
16
|
+
StatusBadge,
|
|
17
|
+
ServiceInfo,
|
|
18
|
+
SqlBrowser,
|
|
19
|
+
} from "./components/index";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Bunflare Dashboard</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
10
|
+
<link rel="stylesheet" href="./style.css" />
|
|
11
|
+
</head>
|
|
12
|
+
<body class="h-full bg-surface text-ink" style="font-family: 'Poppins', sans-serif;">
|
|
13
|
+
<script>
|
|
14
|
+
// Apply saved theme before first paint to prevent flash
|
|
15
|
+
(function() {
|
|
16
|
+
var t = localStorage.getItem("bunflare-theme");
|
|
17
|
+
if (t === "light" || t === "dark") document.documentElement.setAttribute("data-theme", t);
|
|
18
|
+
})();
|
|
19
|
+
</script>
|
|
20
|
+
<div id="app" class="h-full"></div>
|
|
21
|
+
<script type="module" src="./app.tsx"></script>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useState, useEffect } from "preact/hooks";
|
|
2
|
+
|
|
3
|
+
export function useRoute(): string {
|
|
4
|
+
const [route, setRoute] = useState(location.hash.slice(1) || "/");
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const handler = () => setRoute(location.hash.slice(1) || "/");
|
|
8
|
+
window.addEventListener("hashchange", handler);
|
|
9
|
+
return () => window.removeEventListener("hashchange", handler);
|
|
10
|
+
}, []);
|
|
11
|
+
|
|
12
|
+
return route;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function navigate(path: string) {
|
|
16
|
+
location.hash = path;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function replaceRoute(path: string) {
|
|
20
|
+
history.replaceState(null, "", "#" + path);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseHashRoute(hash: string): { segments: string[]; query: URLSearchParams } {
|
|
24
|
+
const raw = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
25
|
+
const qIdx = raw.indexOf("?");
|
|
26
|
+
const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
|
|
27
|
+
const queryStr = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
|
|
28
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
29
|
+
return { segments, query: new URLSearchParams(queryStr) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatBytes(bytes: number): string {
|
|
33
|
+
if (bytes === 0) return "0 B";
|
|
34
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
35
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
36
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatTime(ts: number): string {
|
|
40
|
+
return new Date(ts).toLocaleString();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function classNames(...classes: (string | false | null | undefined)[]): string {
|
|
44
|
+
return classes.filter(Boolean).join(" ");
|
|
45
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Procedures } from "./server";
|
|
2
|
+
|
|
3
|
+
type EmptyObject = Record<string, never>;
|
|
4
|
+
|
|
5
|
+
export async function rpc<K extends keyof Procedures>(
|
|
6
|
+
procedure: K,
|
|
7
|
+
...args: Procedures[K]["input"] extends EmptyObject ? [] : [Procedures[K]["input"]]
|
|
8
|
+
): Promise<Procedures[K]["output"]> {
|
|
9
|
+
const input = args[0] ?? {};
|
|
10
|
+
const res = await fetch("/__dashboard/api/rpc", {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "Content-Type": "application/json" },
|
|
13
|
+
body: JSON.stringify({ procedure, input }),
|
|
14
|
+
});
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
const text = await res.text();
|
|
17
|
+
throw new Error(text);
|
|
18
|
+
}
|
|
19
|
+
return res.json();
|
|
20
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { HandlerContext, AiRequest, OkResponse } from "../types";
|
|
2
|
+
import { getDatabase } from "../../../db";
|
|
3
|
+
import type { SQLQueryBindings } from "bun:sqlite";
|
|
4
|
+
|
|
5
|
+
export const handlers = {
|
|
6
|
+
"ai.list"({ model, status, limit = 50 }: { model?: string; status?: string; limit?: number }): AiRequest[] {
|
|
7
|
+
const db = getDatabase();
|
|
8
|
+
let query = "SELECT id, model, input_summary, output_summary, duration_ms, status, error, is_streaming, created_at FROM ai_requests";
|
|
9
|
+
const conditions: string[] = [];
|
|
10
|
+
const params: SQLQueryBindings[] = [];
|
|
11
|
+
|
|
12
|
+
if (model) {
|
|
13
|
+
conditions.push("model = ?");
|
|
14
|
+
params.push(model);
|
|
15
|
+
}
|
|
16
|
+
if (status) {
|
|
17
|
+
conditions.push("status = ?");
|
|
18
|
+
params.push(status);
|
|
19
|
+
}
|
|
20
|
+
if (conditions.length) {
|
|
21
|
+
query += " WHERE " + conditions.join(" AND ");
|
|
22
|
+
}
|
|
23
|
+
query += " ORDER BY created_at DESC LIMIT ?";
|
|
24
|
+
params.push(limit);
|
|
25
|
+
|
|
26
|
+
return db.prepare(query).all(...params) as AiRequest[];
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
"ai.get"({ id }: { id: string }): AiRequest | null {
|
|
30
|
+
const db = getDatabase();
|
|
31
|
+
return db.query<AiRequest, [string]>(
|
|
32
|
+
"SELECT id, model, input_summary, output_summary, duration_ms, status, error, is_streaming, created_at FROM ai_requests WHERE id = ?",
|
|
33
|
+
).get(id);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
"ai.delete"({ id }: { id: string }): OkResponse {
|
|
37
|
+
const db = getDatabase();
|
|
38
|
+
db.prepare("DELETE FROM ai_requests WHERE id = ?").run(id);
|
|
39
|
+
return { ok: true };
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
"ai.stats"(_input: {}): { total: number; byModel: Record<string, number>; byStatus: Record<string, number>; avgDuration: number } {
|
|
43
|
+
const db = getDatabase();
|
|
44
|
+
const total = db.query<{ count: number }, []>("SELECT COUNT(*) as count FROM ai_requests").get()?.count ?? 0;
|
|
45
|
+
|
|
46
|
+
const modelRows = db.query<{ model: string; count: number }, []>(
|
|
47
|
+
"SELECT model, COUNT(*) as count FROM ai_requests GROUP BY model ORDER BY count DESC",
|
|
48
|
+
).all();
|
|
49
|
+
const byModel: Record<string, number> = {};
|
|
50
|
+
for (const r of modelRows) byModel[r.model] = r.count;
|
|
51
|
+
|
|
52
|
+
const statusRows = db.query<{ status: string; count: number }, []>(
|
|
53
|
+
"SELECT status, COUNT(*) as count FROM ai_requests GROUP BY status",
|
|
54
|
+
).all();
|
|
55
|
+
const byStatus: Record<string, number> = {};
|
|
56
|
+
for (const r of statusRows) byStatus[r.status] = r.count;
|
|
57
|
+
|
|
58
|
+
const avgDuration = db.query<{ avg: number | null }, []>(
|
|
59
|
+
"SELECT AVG(duration_ms) as avg FROM ai_requests",
|
|
60
|
+
).get()?.avg ?? 0;
|
|
61
|
+
|
|
62
|
+
return { total, byModel, byStatus, avgDuration: Math.round(avgDuration) };
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
"ai.models"(_input: {}): string[] {
|
|
66
|
+
const db = getDatabase();
|
|
67
|
+
return db.query<{ model: string }, []>(
|
|
68
|
+
"SELECT DISTINCT model FROM ai_requests ORDER BY model",
|
|
69
|
+
).all().map(r => r.model);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { HandlerContext, OkResponse, AnalyticsEngineDataPoint as AEDataPoint } from "../types";
|
|
2
|
+
import { getDatabase } from "../../../db";
|
|
3
|
+
import type { SQLQueryBindings } from "bun:sqlite";
|
|
4
|
+
|
|
5
|
+
export const handlers = {
|
|
6
|
+
"analyticsEngine.list"({ dataset, limit = 50 }: { dataset?: string; limit?: number }): AEDataPoint[] {
|
|
7
|
+
const db = getDatabase();
|
|
8
|
+
let query = "SELECT id, dataset, timestamp, index1, blob1, blob2, blob3, blob4, blob5, double1, double2, double3, double4, double5, _sample_interval FROM analytics_engine";
|
|
9
|
+
const params: SQLQueryBindings[] = [];
|
|
10
|
+
|
|
11
|
+
if (dataset) {
|
|
12
|
+
query += " WHERE dataset = ?";
|
|
13
|
+
params.push(dataset);
|
|
14
|
+
}
|
|
15
|
+
query += " ORDER BY timestamp DESC LIMIT ?";
|
|
16
|
+
params.push(limit);
|
|
17
|
+
|
|
18
|
+
return db.prepare(query).all(...params) as AEDataPoint[];
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
"analyticsEngine.get"({ id }: { id: string }): AEDataPoint | null {
|
|
22
|
+
const db = getDatabase();
|
|
23
|
+
return db.query<AEDataPoint, [string]>(
|
|
24
|
+
`SELECT * FROM analytics_engine WHERE id = ?`,
|
|
25
|
+
).get(id);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
"analyticsEngine.delete"({ id }: { id: string }): OkResponse {
|
|
29
|
+
const db = getDatabase();
|
|
30
|
+
db.prepare("DELETE FROM analytics_engine WHERE id = ?").run(id);
|
|
31
|
+
return { ok: true };
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
"analyticsEngine.datasets"(_input: {}): string[] {
|
|
35
|
+
const db = getDatabase();
|
|
36
|
+
return db.query<{ dataset: string }, []>(
|
|
37
|
+
"SELECT DISTINCT dataset FROM analytics_engine ORDER BY dataset",
|
|
38
|
+
).all().map(r => r.dataset);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
"analyticsEngine.stats"(_input: {}): { total: number; byDataset: Record<string, number> } {
|
|
42
|
+
const db = getDatabase();
|
|
43
|
+
const total = db.query<{ count: number }, []>("SELECT COUNT(*) as count FROM analytics_engine").get()?.count ?? 0;
|
|
44
|
+
|
|
45
|
+
const rows = db.query<{ dataset: string; count: number }, []>(
|
|
46
|
+
"SELECT dataset, COUNT(*) as count FROM analytics_engine GROUP BY dataset ORDER BY count DESC",
|
|
47
|
+
).all();
|
|
48
|
+
const byDataset: Record<string, number> = {};
|
|
49
|
+
for (const r of rows) byDataset[r.dataset] = r.count;
|
|
50
|
+
|
|
51
|
+
return { total, byDataset };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CacheName, CacheEntry, OkResponse } from "../types";
|
|
2
|
+
import { getDatabase } from "../../../db";
|
|
3
|
+
|
|
4
|
+
export const handlers = {
|
|
5
|
+
"cache.listCaches"(_input: {}): CacheName[] {
|
|
6
|
+
const db = getDatabase();
|
|
7
|
+
return db.query<{ cache_name: string; count: number }, []>(
|
|
8
|
+
"SELECT cache_name, COUNT(*) as count FROM cache_entries GROUP BY cache_name ORDER BY cache_name"
|
|
9
|
+
).all();
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
"cache.listEntries"({ name }: { name: string }): CacheEntry[] {
|
|
13
|
+
const db = getDatabase();
|
|
14
|
+
return db.query<{ url: string; status: number; headers: string; expires_at: number | null }, [string]>(
|
|
15
|
+
"SELECT url, status, headers, expires_at FROM cache_entries WHERE cache_name = ? ORDER BY url"
|
|
16
|
+
).all(name);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
"cache.deleteEntry"({ name, url }: { name: string; url: string }): OkResponse {
|
|
20
|
+
const db = getDatabase();
|
|
21
|
+
db.prepare("DELETE FROM cache_entries WHERE cache_name = ? AND url = ?").run(name, url);
|
|
22
|
+
return { ok: true };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { HandlerContext } from "../types";
|
|
2
|
+
import { getAllConfigs } from "../types";
|
|
3
|
+
|
|
4
|
+
export interface ConfigItem {
|
|
5
|
+
name: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ConfigGroup {
|
|
10
|
+
title: string;
|
|
11
|
+
items: ConfigItem[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const handlers = {
|
|
15
|
+
"config.forService"({ type }: { type: string }, ctx: HandlerContext): ConfigGroup[] {
|
|
16
|
+
const groups: ConfigGroup[] = [];
|
|
17
|
+
|
|
18
|
+
for (const config of getAllConfigs(ctx)) {
|
|
19
|
+
switch (type) {
|
|
20
|
+
case "kv": {
|
|
21
|
+
const items = (config.kv_namespaces ?? []).map(ns => ({ name: ns.binding, value: ns.id }));
|
|
22
|
+
if (items.length) groups.push({ title: "Bindings", items });
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
case "r2": {
|
|
27
|
+
const items = (config.r2_buckets ?? []).map(b => ({ name: b.binding, value: b.bucket_name }));
|
|
28
|
+
if (items.length) groups.push({ title: "Bindings", items });
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
case "queue": {
|
|
33
|
+
const producers = (config.queues?.producers ?? []).map(p => {
|
|
34
|
+
let value = p.queue;
|
|
35
|
+
if (p.delivery_delay != null) value += ` · delay ${p.delivery_delay}s`;
|
|
36
|
+
return { name: p.binding, value };
|
|
37
|
+
});
|
|
38
|
+
if (producers.length) groups.push({ title: "Producers", items: producers });
|
|
39
|
+
|
|
40
|
+
const consumers = (config.queues?.consumers ?? []).map(c => {
|
|
41
|
+
const parts: string[] = [];
|
|
42
|
+
if (c.max_batch_size != null) parts.push(`batch ${c.max_batch_size}`);
|
|
43
|
+
if (c.max_retries != null) parts.push(`retries ${c.max_retries}`);
|
|
44
|
+
if (c.max_batch_timeout != null) parts.push(`timeout ${c.max_batch_timeout}s`);
|
|
45
|
+
if (c.dead_letter_queue) parts.push(`dlq ${c.dead_letter_queue}`);
|
|
46
|
+
return { name: c.queue, value: parts.join(" · ") || "defaults" };
|
|
47
|
+
});
|
|
48
|
+
if (consumers.length) groups.push({ title: "Consumers", items: consumers });
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case "do": {
|
|
53
|
+
const items = (config.durable_objects?.bindings ?? []).map(b => ({ name: b.name, value: b.class_name }));
|
|
54
|
+
if (items.length) groups.push({ title: "Bindings", items });
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case "workflows": {
|
|
59
|
+
const items = (config.workflows ?? []).map(w => ({ name: w.binding, value: w.class_name }));
|
|
60
|
+
if (items.length) groups.push({ title: "Bindings", items });
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case "d1": {
|
|
65
|
+
const items = (config.d1_databases ?? []).map(db => ({ name: db.binding, value: db.database_name }));
|
|
66
|
+
if (items.length) groups.push({ title: "Bindings", items });
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case "containers": {
|
|
71
|
+
const items = (config.containers ?? []).map(c => ({
|
|
72
|
+
name: c.name ?? c.class_name,
|
|
73
|
+
value: `${c.image}${c.max_instances ? ` · max ${c.max_instances}` : ""}`,
|
|
74
|
+
}));
|
|
75
|
+
if (items.length) groups.push({ title: "Containers", items });
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case "scheduled": {
|
|
80
|
+
const crons = config.triggers?.crons ?? [];
|
|
81
|
+
if (crons.length) {
|
|
82
|
+
groups.push({
|
|
83
|
+
title: "Cron Triggers",
|
|
84
|
+
items: crons.map((c, i) => ({ name: `Trigger ${i + 1}`, value: c })),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case "ai": {
|
|
91
|
+
if (config.ai) {
|
|
92
|
+
groups.push({ title: "Bindings", items: [{ name: config.ai.binding, value: "Workers AI" }] });
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case "hyperdrive": {
|
|
98
|
+
const items = (config.hyperdrive ?? []).map(hd => {
|
|
99
|
+
let value = hd.id;
|
|
100
|
+
if (hd.localConnectionString) {
|
|
101
|
+
try {
|
|
102
|
+
const url = new URL(hd.localConnectionString);
|
|
103
|
+
value += ` · ${url.hostname}`;
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
return { name: hd.binding, value };
|
|
107
|
+
});
|
|
108
|
+
if (items.length) groups.push({ title: "Bindings", items });
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case "analytics_engine": {
|
|
113
|
+
const items = (config.analytics_engine_datasets ?? []).map(ae => ({
|
|
114
|
+
name: ae.binding,
|
|
115
|
+
value: ae.dataset ?? ae.binding,
|
|
116
|
+
}));
|
|
117
|
+
if (items.length) groups.push({ title: "Bindings", items });
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "email": {
|
|
122
|
+
const items = (config.send_email ?? []).map(e => {
|
|
123
|
+
let value = e.destination_address ?? "any destination";
|
|
124
|
+
if (e.allowed_destination_addresses?.length) {
|
|
125
|
+
value = e.allowed_destination_addresses.join(", ");
|
|
126
|
+
}
|
|
127
|
+
return { name: e.name, value };
|
|
128
|
+
});
|
|
129
|
+
if (items.length) groups.push({ title: "Bindings", items });
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return groups;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
import type { HandlerContext, ContainerSummary, ContainerInstance, ContainerDetail, OkResponse } from "../types";
|
|
3
|
+
import { getAllConfigs, getDoNamespace } from "../types";
|
|
4
|
+
import { getDatabase } from "../../../db";
|
|
5
|
+
import { DockerManager } from "../../../bindings/container-docker";
|
|
6
|
+
import type { ContainerBase } from "../../../bindings/container";
|
|
7
|
+
|
|
8
|
+
interface DockerPsEntry {
|
|
9
|
+
Names: string;
|
|
10
|
+
State: string;
|
|
11
|
+
Status: string;
|
|
12
|
+
Ports: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DOCKER_JSON_FORMAT = "{{json .}}";
|
|
16
|
+
|
|
17
|
+
async function listDockerContainers(filterPrefix: string): Promise<DockerPsEntry[]> {
|
|
18
|
+
const result = await $`docker ps -a --filter name=${filterPrefix} --format=${DOCKER_JSON_FORMAT}`.quiet().nothrow();
|
|
19
|
+
if (result.exitCode !== 0) return [];
|
|
20
|
+
const lines = result.stdout.toString().trim().split("\n").filter(Boolean);
|
|
21
|
+
return lines.map(line => JSON.parse(line) as DockerPsEntry);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parsePorts(portsStr: string): Record<string, string> {
|
|
25
|
+
const ports: Record<string, string> = {};
|
|
26
|
+
if (!portsStr) return ports;
|
|
27
|
+
for (const part of portsStr.split(", ")) {
|
|
28
|
+
const match = part.match(/(.+)->(\d+\/\w+)/);
|
|
29
|
+
if (match) {
|
|
30
|
+
ports[match[2]!] = match[1]!;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return ports;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const handlers = {
|
|
37
|
+
async "containers.list"(_input: {}, ctx: HandlerContext): Promise<ContainerSummary[]> {
|
|
38
|
+
const seen = new Map<string, ContainerSummary>();
|
|
39
|
+
const db = getDatabase();
|
|
40
|
+
|
|
41
|
+
for (const config of getAllConfigs(ctx)) {
|
|
42
|
+
for (const c of config.containers ?? []) {
|
|
43
|
+
if (seen.has(c.class_name)) continue;
|
|
44
|
+
|
|
45
|
+
const row = db.query<{ count: number }, [string]>(
|
|
46
|
+
"SELECT COUNT(*) as count FROM do_instances WHERE namespace = ?"
|
|
47
|
+
).get(c.class_name);
|
|
48
|
+
|
|
49
|
+
seen.set(c.class_name, {
|
|
50
|
+
className: c.class_name,
|
|
51
|
+
image: c.image,
|
|
52
|
+
maxInstances: c.max_instances ?? null,
|
|
53
|
+
bindingName: c.name ?? c.class_name,
|
|
54
|
+
instanceCount: row?.count ?? 0,
|
|
55
|
+
runningCount: 0,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Count running Docker containers per class
|
|
61
|
+
for (const [className, summary] of seen) {
|
|
62
|
+
const entries = await listDockerContainers(`bunflare-${className}-`);
|
|
63
|
+
summary.runningCount = entries.filter(e => e.State === "running").length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return Array.from(seen.values());
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async "containers.listInstances"({ className }: { className: string }, _ctx: HandlerContext): Promise<ContainerInstance[]> {
|
|
70
|
+
const db = getDatabase();
|
|
71
|
+
|
|
72
|
+
// Primary source: DO instances from SQLite
|
|
73
|
+
const doInstances = db.query<{ id: string; name: string | null }, [string]>(
|
|
74
|
+
"SELECT id, name FROM do_instances WHERE namespace = ? ORDER BY id"
|
|
75
|
+
).all(className);
|
|
76
|
+
|
|
77
|
+
// Secondary source: Docker containers for state info
|
|
78
|
+
const dockerEntries = await listDockerContainers(`bunflare-${className}-`);
|
|
79
|
+
const dockerByPrefix = new Map<string, DockerPsEntry>();
|
|
80
|
+
for (const e of dockerEntries) {
|
|
81
|
+
// Container name format: bunflare-{className}-{idHex.slice(0,12)}
|
|
82
|
+
const prefix = e.Names.replace(`bunflare-${className}-`, "");
|
|
83
|
+
if (prefix) dockerByPrefix.set(prefix, e);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Map DO instances with Docker state
|
|
87
|
+
const seenPrefixes = new Set<string>();
|
|
88
|
+
const results: ContainerInstance[] = doInstances.map(inst => {
|
|
89
|
+
const idPrefix = inst.id.slice(0, 12);
|
|
90
|
+
seenPrefixes.add(idPrefix);
|
|
91
|
+
const docker = dockerByPrefix.get(idPrefix);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
id: inst.id,
|
|
95
|
+
doName: inst.name,
|
|
96
|
+
containerName: docker?.Names ?? `bunflare-${className}-${idPrefix}`,
|
|
97
|
+
state: docker?.State ?? "stopped",
|
|
98
|
+
ports: docker ? parsePorts(docker.Ports) : {},
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Include any Docker containers without a matching DO instance
|
|
103
|
+
for (const [prefix, docker] of dockerByPrefix) {
|
|
104
|
+
if (seenPrefixes.has(prefix)) continue;
|
|
105
|
+
results.push({
|
|
106
|
+
id: prefix,
|
|
107
|
+
doName: null,
|
|
108
|
+
containerName: docker.Names,
|
|
109
|
+
state: docker.State,
|
|
110
|
+
ports: parsePorts(docker.Ports),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return results;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async "containers.getDetail"({ className, id }: { className: string; id: string }, ctx: HandlerContext): Promise<ContainerDetail> {
|
|
118
|
+
const db = getDatabase();
|
|
119
|
+
const docker = new DockerManager();
|
|
120
|
+
|
|
121
|
+
// Get DO instance info
|
|
122
|
+
const inst = db.query<{ id: string; name: string | null }, [string, string]>(
|
|
123
|
+
"SELECT id, name FROM do_instances WHERE namespace = ? AND id = ?"
|
|
124
|
+
).get(className, id);
|
|
125
|
+
|
|
126
|
+
const containerName = `bunflare-${className}-${id.slice(0, 12)}`;
|
|
127
|
+
|
|
128
|
+
// Get Docker state
|
|
129
|
+
const dockerInfo = await docker.inspect(containerName);
|
|
130
|
+
|
|
131
|
+
// Get container config from live instance if available
|
|
132
|
+
let config = {
|
|
133
|
+
defaultPort: 8080,
|
|
134
|
+
sleepAfter: null as string | number | null,
|
|
135
|
+
enableInternet: true,
|
|
136
|
+
pingEndpoint: "/",
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Try to find config from wrangler config
|
|
140
|
+
let image = "";
|
|
141
|
+
for (const cfg of getAllConfigs(ctx)) {
|
|
142
|
+
const containerCfg = cfg.containers?.find(c => c.class_name === className);
|
|
143
|
+
if (containerCfg) {
|
|
144
|
+
image = containerCfg.image;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Try to get config from live DO instance
|
|
150
|
+
const namespace = getDoNamespace(ctx, className);
|
|
151
|
+
if (namespace) {
|
|
152
|
+
const instance = (namespace as any)._getInstance(id) as ContainerBase | null;
|
|
153
|
+
if (instance) {
|
|
154
|
+
config.defaultPort = instance.defaultPort ?? 8080;
|
|
155
|
+
config.sleepAfter = instance.sleepAfter ?? null;
|
|
156
|
+
config.enableInternet = instance.enableInternet;
|
|
157
|
+
config.pingEndpoint = instance.pingEndpoint;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
id,
|
|
163
|
+
doName: inst?.name ?? null,
|
|
164
|
+
containerName,
|
|
165
|
+
image,
|
|
166
|
+
state: dockerInfo?.state ?? "stopped",
|
|
167
|
+
exitCode: dockerInfo?.exitCode ?? null,
|
|
168
|
+
ports: dockerInfo?.ports ?? {},
|
|
169
|
+
created: null, // docker inspect State.StartedAt could be used but not in DockerContainerInfo
|
|
170
|
+
config,
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async "containers.getLogs"({ className, id, tail }: { className: string; id: string; tail?: number }): Promise<{ logs: string }> {
|
|
175
|
+
const docker = new DockerManager();
|
|
176
|
+
const containerName = `bunflare-${className}-${id.slice(0, 12)}`;
|
|
177
|
+
const logs = await docker.logs(containerName, tail);
|
|
178
|
+
return { logs };
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async "containers.stop"({ className, id }: { className: string; id: string }, ctx: HandlerContext): Promise<OkResponse> {
|
|
182
|
+
const docker = new DockerManager();
|
|
183
|
+
const containerName = `bunflare-${className}-${id.slice(0, 12)}`;
|
|
184
|
+
await docker.stop(containerName, 10);
|
|
185
|
+
return { ok: true };
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
async "containers.destroy"({ className, id }: { className: string; id: string }, ctx: HandlerContext): Promise<OkResponse> {
|
|
189
|
+
const docker = new DockerManager();
|
|
190
|
+
const containerName = `bunflare-${className}-${id.slice(0, 12)}`;
|
|
191
|
+
await docker.remove(containerName);
|
|
192
|
+
return { ok: true };
|
|
193
|
+
},
|
|
194
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { HandlerContext, D1Database as D1DatabaseInfo, D1Table, QueryResult } from "../types";
|
|
2
|
+
import { getAllConfigs } from "../types";
|
|
3
|
+
import { getDataDir } from "../../../db";
|
|
4
|
+
import { Database } from "bun:sqlite";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
export const handlers = {
|
|
9
|
+
"d1.listDatabases"(_input: {}, ctx: HandlerContext): D1DatabaseInfo[] {
|
|
10
|
+
const d1Dir = join(getDataDir(), "d1");
|
|
11
|
+
const databases: D1DatabaseInfo[] = [];
|
|
12
|
+
const seen = new Set<string>();
|
|
13
|
+
|
|
14
|
+
if (existsSync(d1Dir)) {
|
|
15
|
+
const files = readdirSync(d1Dir).filter(f => f.endsWith(".sqlite"));
|
|
16
|
+
for (const f of files) {
|
|
17
|
+
const name = f.replace(".sqlite", "");
|
|
18
|
+
seen.add(name);
|
|
19
|
+
const d1db = new Database(join(d1Dir, f));
|
|
20
|
+
try {
|
|
21
|
+
const tables = d1db.query<{ name: string }, []>(
|
|
22
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
23
|
+
).all();
|
|
24
|
+
databases.push({ name, tables: tables.length });
|
|
25
|
+
} finally {
|
|
26
|
+
d1db.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const config of getAllConfigs(ctx)) {
|
|
32
|
+
for (const d of config.d1_databases ?? []) {
|
|
33
|
+
if (!seen.has(d.database_name)) {
|
|
34
|
+
databases.push({ name: d.database_name, tables: 0 });
|
|
35
|
+
seen.add(d.database_name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
databases.sort((a, b) => a.name.localeCompare(b.name));
|
|
41
|
+
return databases;
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
"d1.listTables"({ dbName }: { dbName: string }): D1Table[] {
|
|
45
|
+
const dbPath = join(getDataDir(), "d1", `${dbName}.sqlite`);
|
|
46
|
+
if (!existsSync(dbPath)) throw new Error("Database not found");
|
|
47
|
+
|
|
48
|
+
const d1db = new Database(dbPath);
|
|
49
|
+
try {
|
|
50
|
+
const tables = d1db.query<{ name: string; sql: string }, []>(
|
|
51
|
+
"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
52
|
+
).all();
|
|
53
|
+
|
|
54
|
+
return tables.map(t => {
|
|
55
|
+
const row = d1db.query<{ count: number }, []>(`SELECT COUNT(*) as count FROM "${t.name}"`).get();
|
|
56
|
+
return { name: t.name, sql: t.sql, rows: row?.count ?? 0 };
|
|
57
|
+
});
|
|
58
|
+
} finally {
|
|
59
|
+
d1db.close();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
"d1.query"({ dbName, sql }: { dbName: string; sql: string }): QueryResult {
|
|
64
|
+
if (!sql) throw new Error("Missing sql field");
|
|
65
|
+
|
|
66
|
+
const dbPath = join(getDataDir(), "d1", `${dbName}.sqlite`);
|
|
67
|
+
if (!existsSync(dbPath)) throw new Error("Database not found");
|
|
68
|
+
|
|
69
|
+
const d1db = new Database(dbPath);
|
|
70
|
+
try {
|
|
71
|
+
const stmt = d1db.prepare(sql);
|
|
72
|
+
if (stmt.columnNames.length > 0) {
|
|
73
|
+
const rows = stmt.all() as Record<string, unknown>[];
|
|
74
|
+
return { columns: stmt.columnNames, rows, count: rows.length };
|
|
75
|
+
} else {
|
|
76
|
+
stmt.run();
|
|
77
|
+
const changes = d1db.query<{ c: number }, []>("SELECT changes() as c").get()?.c ?? 0;
|
|
78
|
+
return { columns: [], rows: [], count: changes, message: `${changes} row(s) affected` };
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
d1db.close();
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|