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,193 @@
1
+ import { useState } from "preact/hooks";
2
+ import { formatTime } from "../lib";
3
+ import { useQuery, useMutation } from "../rpc/hooks";
4
+ import { EmptyState, Breadcrumb, Table, PageHeader, PillButton, DeleteButton, TableLink, StatusBadge, ServiceInfo, RefreshButton } from "../components";
5
+
6
+ const QUEUE_STATUS_COLORS: Record<string, string> = {
7
+ pending: "bg-amber-100 text-amber-700",
8
+ acked: "bg-emerald-100 text-emerald-700",
9
+ failed: "bg-red-100 text-red-700",
10
+ };
11
+
12
+ export function QueueView({ route }: { route: string }) {
13
+ const parts = route.split("/").filter(Boolean);
14
+ if (parts.length === 1) return <QueueList />;
15
+ if (parts.length >= 2) return <QueueMessages name={decodeURIComponent(parts[1]!)} />;
16
+ return null;
17
+ }
18
+
19
+ function QueueList() {
20
+ const { data: queues, refetch } = useQuery("queue.listQueues");
21
+ const { data: configGroups } = useQuery("config.forService", { type: "queue" });
22
+
23
+ const totalPending = queues?.reduce((s, q) => s + q.pending, 0) ?? 0;
24
+ const totalAcked = queues?.reduce((s, q) => s + q.acked, 0) ?? 0;
25
+ const totalFailed = queues?.reduce((s, q) => s + q.failed, 0) ?? 0;
26
+
27
+ return (
28
+ <div class="p-8 max-w-6xl">
29
+ <PageHeader title="Queues" subtitle={`${queues?.length ?? 0} queue(s)`} actions={<RefreshButton onClick={refetch} />} />
30
+ <div class="flex gap-6 items-start">
31
+ <div class="flex-1 min-w-0">
32
+ {!queues?.length ? (
33
+ <EmptyState message="No queues found" />
34
+ ) : (
35
+ <Table
36
+ headers={["Queue", "Pending", "Acked", "Failed"]}
37
+ rows={queues.map(q => [
38
+ <TableLink href={`#/queue/${encodeURIComponent(q.queue)}`}>{q.queue}</TableLink>,
39
+ <span class="tabular-nums">{q.pending}</span>,
40
+ <span class="tabular-nums">{q.acked}</span>,
41
+ <span class="tabular-nums">{q.failed}</span>,
42
+ ])}
43
+ />
44
+ )}
45
+ </div>
46
+ <ServiceInfo
47
+ description="Message queues for asynchronous task processing."
48
+ stats={[
49
+ { label: "Pending", value: totalPending.toLocaleString() },
50
+ { label: "Processed", value: totalAcked.toLocaleString() },
51
+ { label: "Failed", value: totalFailed.toLocaleString() },
52
+ { label: "Queues", value: queues?.length ?? 0 },
53
+ ]}
54
+ configGroups={configGroups}
55
+ links={[
56
+ { label: "Documentation", href: "https://developers.cloudflare.com/queues/" },
57
+ { label: "API Reference", href: "https://developers.cloudflare.com/api/resources/queues/" },
58
+ ]}
59
+ />
60
+ </div>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ function PublishForm({ queue, onPublished }: { queue: string; onPublished: () => void }) {
66
+ const [open, setOpen] = useState(false);
67
+ const [body, setBody] = useState("");
68
+ const [contentType, setContentType] = useState("json");
69
+ const [error, setError] = useState("");
70
+ const publish = useMutation("queue.publishMessage");
71
+
72
+ const handleSubmit = async () => {
73
+ setError("");
74
+ const result = await publish.mutate({ queue, body, contentType });
75
+ if (result) {
76
+ setBody("");
77
+ setOpen(false);
78
+ onPublished();
79
+ } else if (publish.error) {
80
+ setError(publish.error.message);
81
+ }
82
+ };
83
+
84
+ if (!open) {
85
+ return (
86
+ <button
87
+ onClick={() => setOpen(true)}
88
+ class="rounded-md px-3 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all"
89
+ >
90
+ Publish message
91
+ </button>
92
+ );
93
+ }
94
+
95
+ return (
96
+ <div class="bg-panel border border-border rounded-lg p-4 mb-6">
97
+ <div class="flex items-center justify-between mb-3">
98
+ <div class="text-sm font-semibold text-ink">Publish message</div>
99
+ <button onClick={() => { setOpen(false); setError(""); }} class="text-text-muted hover:text-text-data text-xs font-medium">
100
+ Cancel
101
+ </button>
102
+ </div>
103
+ <div class="flex gap-2 mb-3">
104
+ {["json", "text"].map(ct => (
105
+ <PillButton key={ct} onClick={() => setContentType(ct)} active={contentType === ct}>
106
+ {ct.toUpperCase()}
107
+ </PillButton>
108
+ ))}
109
+ </div>
110
+ <textarea
111
+ value={body}
112
+ onInput={e => setBody((e.target as HTMLTextAreaElement).value)}
113
+ placeholder={contentType === "json" ? '{"key": "value"}' : "Message body..."}
114
+ class="w-full bg-panel-secondary border border-border rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-border focus:ring-1 focus:ring-border transition-all resize-y min-h-[80px]"
115
+ rows={3}
116
+ />
117
+ {error && <div class="text-red-500 text-xs mt-1">{error}</div>}
118
+ <div class="flex justify-end mt-3">
119
+ <button
120
+ onClick={handleSubmit}
121
+ disabled={publish.isLoading || !body.trim()}
122
+ class="rounded-md px-4 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
123
+ >
124
+ {publish.isLoading ? "Publishing..." : "Publish"}
125
+ </button>
126
+ </div>
127
+ </div>
128
+ );
129
+ }
130
+
131
+ function RequeueButton({ onClick }: { onClick: () => void }) {
132
+ return (
133
+ <button onClick={onClick} class="text-amber-500 hover:text-amber-700 text-xs font-medium rounded-md px-2 py-1 hover:bg-amber-50 transition-all">
134
+ Requeue
135
+ </button>
136
+ );
137
+ }
138
+
139
+ function QueueMessages({ name }: { name: string }) {
140
+ const [filter, setFilter] = useState("");
141
+ const { data: messages, refetch } = useQuery("queue.listMessages", { queue: name, status: filter || undefined });
142
+ const deleteMsg = useMutation("queue.deleteMessage");
143
+ const requeueMsg = useMutation("queue.requeueMessage");
144
+
145
+ const handleDelete = async (id: string) => {
146
+ if (!confirm("Delete this message?")) return;
147
+ await deleteMsg.mutate({ queue: name, id });
148
+ refetch();
149
+ };
150
+
151
+ const handleRequeue = async (id: string) => {
152
+ await requeueMsg.mutate({ queue: name, id });
153
+ refetch();
154
+ };
155
+
156
+ return (
157
+ <div class="p-8">
158
+ <Breadcrumb items={[{ label: "Queues", href: "#/queue" }, { label: name }]} />
159
+ <div class="mb-6 flex gap-2 items-center justify-between">
160
+ <div class="flex gap-2">
161
+ {["", "pending", "acked", "failed"].map(s => (
162
+ <PillButton key={s} onClick={() => setFilter(s)} active={filter === s}>
163
+ {s || "All"}
164
+ </PillButton>
165
+ ))}
166
+ </div>
167
+ <div class="flex gap-2 items-center">
168
+ <RefreshButton onClick={refetch} />
169
+ <PublishForm queue={name} onPublished={refetch} />
170
+ </div>
171
+ </div>
172
+ {!messages?.length ? (
173
+ <EmptyState message="No messages found" />
174
+ ) : (
175
+ <Table
176
+ headers={["ID", "Body", "Status", "Attempts", "Created", "Completed", ""]}
177
+ rows={messages.map(m => [
178
+ <span class="font-mono text-xs">{m.id.slice(0, 12)}...</span>,
179
+ <pre class="text-xs max-w-md truncate font-mono">{m.body}</pre>,
180
+ <StatusBadge status={m.status} colorMap={QUEUE_STATUS_COLORS} />,
181
+ m.attempts,
182
+ formatTime(m.created_at),
183
+ m.completed_at ? formatTime(m.completed_at) : "—",
184
+ <div class="flex gap-1">
185
+ {m.status !== "pending" && <RequeueButton onClick={() => handleRequeue(m.id)} />}
186
+ <DeleteButton onClick={() => handleDelete(m.id)} />
187
+ </div>,
188
+ ])}
189
+ />
190
+ )}
191
+ </div>
192
+ );
193
+ }
@@ -0,0 +1,202 @@
1
+ import { useState, useRef } from "preact/hooks";
2
+ import { formatBytes } from "../lib";
3
+ import { useQuery, usePaginatedQuery, useMutation } from "../rpc/hooks";
4
+ import { EmptyState, Breadcrumb, Table, PageHeader, FilterInput, LoadMoreButton, DeleteButton, TableLink, ServiceInfo, RefreshButton } from "../components";
5
+
6
+ export function R2View({ route }: { route: string }) {
7
+ const parts = route.split("/").filter(Boolean);
8
+ if (parts.length === 1) return <R2BucketList />;
9
+ if (parts.length >= 2) return <R2ObjectList bucket={decodeURIComponent(parts[1]!)} />;
10
+ return null;
11
+ }
12
+
13
+ function R2BucketList() {
14
+ const { data: buckets, refetch } = useQuery("r2.listBuckets");
15
+ const { data: configGroups } = useQuery("config.forService", { type: "r2" });
16
+
17
+ const totalObjects = buckets?.reduce((s, b) => s + b.count, 0) ?? 0;
18
+ const totalSize = buckets?.reduce((s, b) => s + b.total_size, 0) ?? 0;
19
+
20
+ return (
21
+ <div class="p-8 max-w-6xl">
22
+ <PageHeader title="R2 Buckets" subtitle={`${buckets?.length ?? 0} bucket(s)`} actions={<RefreshButton onClick={refetch} />} />
23
+ <div class="flex gap-6 items-start">
24
+ <div class="flex-1 min-w-0">
25
+ {!buckets?.length ? (
26
+ <EmptyState message="No R2 buckets found" />
27
+ ) : (
28
+ <Table
29
+ headers={["Bucket", "Objects", "Total Size"]}
30
+ rows={buckets.map(b => [
31
+ <TableLink href={`#/r2/${encodeURIComponent(b.bucket)}`}>{b.bucket}</TableLink>,
32
+ <span class="tabular-nums">{b.count}</span>,
33
+ formatBytes(b.total_size),
34
+ ])}
35
+ />
36
+ )}
37
+ </div>
38
+ <ServiceInfo
39
+ description="Object storage with S3-compatible API. Zero egress fees."
40
+ stats={[
41
+ { label: "Buckets", value: buckets?.length ?? 0 },
42
+ { label: "Objects", value: totalObjects.toLocaleString() },
43
+ { label: "Storage", value: formatBytes(totalSize) },
44
+ ]}
45
+ configGroups={configGroups}
46
+ links={[
47
+ { label: "Documentation", href: "https://developers.cloudflare.com/r2/" },
48
+ { label: "API Reference", href: "https://developers.cloudflare.com/api/resources/r2/" },
49
+ ]}
50
+ />
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ function UploadForm({ bucket, onUploaded }: { bucket: string; onUploaded: () => void }) {
57
+ const [open, setOpen] = useState(false);
58
+ const [key, setKey] = useState("");
59
+ const [file, setFile] = useState<File | null>(null);
60
+ const [error, setError] = useState("");
61
+ const [uploading, setUploading] = useState(false);
62
+ const fileInputRef = useRef<HTMLInputElement>(null);
63
+
64
+ const handleFileChange = (e: Event) => {
65
+ const input = e.target as HTMLInputElement;
66
+ const selected = input.files?.[0] ?? null;
67
+ setFile(selected);
68
+ if (selected && !key) setKey(selected.name);
69
+ };
70
+
71
+ const handleSubmit = async () => {
72
+ if (!file || !key.trim()) return;
73
+ setError("");
74
+ setUploading(true);
75
+ try {
76
+ const formData = new FormData();
77
+ formData.append("bucket", bucket);
78
+ formData.append("key", key.trim());
79
+ formData.append("file", file);
80
+ const res = await fetch("/__dashboard/api/r2/upload", { method: "POST", body: formData });
81
+ const data = await res.json() as { error?: string };
82
+ if (!res.ok) throw new Error(data.error ?? "Upload failed");
83
+ setKey("");
84
+ setFile(null);
85
+ if (fileInputRef.current) fileInputRef.current.value = "";
86
+ setOpen(false);
87
+ onUploaded();
88
+ } catch (err) {
89
+ setError(err instanceof Error ? err.message : String(err));
90
+ } finally {
91
+ setUploading(false);
92
+ }
93
+ };
94
+
95
+ if (!open) {
96
+ return (
97
+ <button
98
+ onClick={() => setOpen(true)}
99
+ class="rounded-md px-3 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all"
100
+ >
101
+ Upload object
102
+ </button>
103
+ );
104
+ }
105
+
106
+ return (
107
+ <div class="bg-panel border border-border rounded-lg p-4 mb-6">
108
+ <div class="flex items-center justify-between mb-3">
109
+ <div class="text-sm font-semibold text-ink">Upload object</div>
110
+ <button onClick={() => { setOpen(false); setError(""); }} class="text-text-muted hover:text-text-data text-xs font-medium">
111
+ Cancel
112
+ </button>
113
+ </div>
114
+ <div class="space-y-3">
115
+ <input
116
+ ref={fileInputRef}
117
+ type="file"
118
+ onChange={handleFileChange}
119
+ class="w-full text-sm text-text-secondary file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-panel-hover file:text-ink hover:file:bg-panel-active file:cursor-pointer file:transition-all"
120
+ />
121
+ <input
122
+ type="text"
123
+ value={key}
124
+ onInput={e => setKey((e.target as HTMLInputElement).value)}
125
+ placeholder="Object key (defaults to filename)"
126
+ class="w-full bg-panel-secondary border border-border rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-border focus:ring-1 focus:ring-border transition-all"
127
+ />
128
+ </div>
129
+ {error && <div class="text-red-500 text-xs mt-2">{error}</div>}
130
+ <div class="flex justify-end mt-3">
131
+ <button
132
+ onClick={handleSubmit}
133
+ disabled={uploading || !file || !key.trim()}
134
+ class="rounded-md px-4 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
135
+ >
136
+ {uploading ? "Uploading..." : "Upload"}
137
+ </button>
138
+ </div>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ function R2ObjectList({ bucket }: { bucket: string }) {
144
+ const [prefix, setPrefix] = useState("");
145
+ const { items: objects, hasMore, loadMore, refetch } = usePaginatedQuery("r2.listObjects", { bucket, prefix });
146
+ const deleteObject = useMutation("r2.deleteObject");
147
+ const renameObject = useMutation("r2.renameObject");
148
+
149
+ const handleDelete = async (key: string) => {
150
+ if (!confirm(`Delete object "${key}"?`)) return;
151
+ await deleteObject.mutate({ bucket, key });
152
+ refetch();
153
+ };
154
+
155
+ const handleRename = async (oldKey: string) => {
156
+ const newKey = prompt("New key:", oldKey);
157
+ if (!newKey || newKey === oldKey) return;
158
+ await renameObject.mutate({ bucket, oldKey, newKey });
159
+ refetch();
160
+ };
161
+
162
+ return (
163
+ <div class="p-8">
164
+ <Breadcrumb items={[{ label: "R2", href: "#/r2" }, { label: bucket }]} />
165
+ <div class="mb-6 flex gap-2 items-center justify-between">
166
+ <FilterInput value={prefix} onInput={setPrefix} placeholder="Filter by prefix..." />
167
+ <div class="flex gap-2 items-center">
168
+ <RefreshButton onClick={refetch} />
169
+ <UploadForm bucket={bucket} onUploaded={refetch} />
170
+ </div>
171
+ </div>
172
+ {objects.length === 0 ? (
173
+ <EmptyState message="No objects found" />
174
+ ) : (
175
+ <>
176
+ <Table
177
+ headers={["Key", "Size", "ETag", "Uploaded", ""]}
178
+ rows={objects.map(o => [
179
+ <span class="font-mono text-xs font-medium">{o.key}</span>,
180
+ formatBytes(o.size),
181
+ <span class="font-mono text-xs text-text-muted">{o.etag.slice(0, 12)}</span>,
182
+ o.uploaded,
183
+ <div class="flex gap-1">
184
+ <a
185
+ href={`/__dashboard/api/r2/download?bucket=${encodeURIComponent(bucket)}&key=${encodeURIComponent(o.key)}`}
186
+ class="text-blue-500 hover:text-blue-700 text-xs font-medium rounded-md px-2 py-1 hover:bg-blue-50 transition-all"
187
+ >
188
+ Download
189
+ </a>
190
+ <button onClick={() => handleRename(o.key)} class="text-text-secondary hover:text-ink text-xs font-medium rounded-md px-2 py-1 hover:bg-panel-hover transition-all">
191
+ Rename
192
+ </button>
193
+ <DeleteButton onClick={() => handleDelete(o.key)} />
194
+ </div>,
195
+ ])}
196
+ />
197
+ {hasMore && <LoadMoreButton onClick={loadMore} />}
198
+ </>
199
+ )}
200
+ </div>
201
+ );
202
+ }
@@ -0,0 +1,89 @@
1
+ import { useState } from "preact/hooks";
2
+ import { useQuery, useMutation } from "../rpc/hooks";
3
+ import { PageHeader, Table, EmptyState, ServiceInfo, RefreshButton } from "../components";
4
+
5
+ export function ScheduledView({ route }: { route: string }) {
6
+ return <ScheduledList />;
7
+ }
8
+
9
+ function ScheduledList() {
10
+ const { data: triggers, refetch } = useQuery("scheduled.listTriggers");
11
+ const { data: configGroups } = useQuery("config.forService", { type: "scheduled" });
12
+ const runNow = useMutation("scheduled.trigger");
13
+ const [runningCron, setRunningCron] = useState<string | null>(null);
14
+ const [lastResult, setLastResult] = useState<{ cron: string; ok: boolean; error?: string } | null>(null);
15
+
16
+ const handleRun = async (cron: string, workerName: string | null) => {
17
+ setRunningCron(cron);
18
+ setLastResult(null);
19
+ const result = await runNow.mutate({ cron, workerName });
20
+ setRunningCron(null);
21
+ if (result) {
22
+ setLastResult({ cron, ok: true });
23
+ } else if (runNow.error) {
24
+ setLastResult({ cron, ok: false, error: runNow.error.message });
25
+ }
26
+ };
27
+
28
+ return (
29
+ <div class="p-8 max-w-6xl">
30
+ <PageHeader title="Scheduled" subtitle={`${triggers?.length ?? 0} cron trigger(s)`} actions={<RefreshButton onClick={refetch} />} />
31
+
32
+ {lastResult && (
33
+ <div class={`mb-6 px-4 py-3 rounded-lg text-sm font-medium ${
34
+ lastResult.ok
35
+ ? "bg-emerald-50 text-emerald-700"
36
+ : "bg-red-50 text-red-600"
37
+ }`}>
38
+ {lastResult.ok
39
+ ? `Triggered "${lastResult.cron}" successfully`
40
+ : `Failed to trigger "${lastResult.cron}": ${lastResult.error}`}
41
+ </div>
42
+ )}
43
+
44
+ <div class="flex gap-6 items-start">
45
+ <div class="flex-1 min-w-0">
46
+ {!triggers?.length ? (
47
+ <EmptyState message="No scheduled triggers configured" />
48
+ ) : (
49
+ <Table
50
+ headers={["Cron Expression", "Schedule", ...(triggers.some(t => t.workerName) ? ["Worker"] : []), ""]}
51
+ rows={triggers.map(t => {
52
+ const row = [
53
+ <span class="font-mono text-xs font-medium">{t.expression}</span>,
54
+ <span class="text-sm text-text-secondary">{t.description}</span>,
55
+ ];
56
+ if (triggers.some(t => t.workerName)) {
57
+ row.push(
58
+ <span class="text-xs text-text-muted font-mono">{t.workerName ?? "main"}</span>,
59
+ );
60
+ }
61
+ row.push(
62
+ <button
63
+ onClick={() => handleRun(t.expression, t.workerName)}
64
+ disabled={runningCron === t.expression}
65
+ class="rounded-md px-3 py-1.5 text-xs font-medium bg-ink text-surface hover:opacity-80 disabled:opacity-40 disabled:cursor-not-allowed transition-all"
66
+ >
67
+ {runningCron === t.expression ? "Running..." : "Trigger"}
68
+ </button>,
69
+ );
70
+ return row;
71
+ })}
72
+ />
73
+ )}
74
+ </div>
75
+ <ServiceInfo
76
+ description="Cron-based scheduled triggers. Each expression defines when the worker's scheduled handler is invoked."
77
+ stats={[
78
+ { label: "Triggers", value: triggers?.length ?? 0 },
79
+ ]}
80
+ configGroups={configGroups}
81
+ links={[
82
+ { label: "Cron Triggers", href: "https://developers.cloudflare.com/workers/configuration/cron-triggers/" },
83
+ { label: "Crontab Guru", href: "https://crontab.guru/" },
84
+ ]}
85
+ />
86
+ </div>
87
+ </div>
88
+ );
89
+ }