@emdash-cms/cloudflare 0.1.0 → 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License
2
+
3
+ Copyright 2026 Cloudflare Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/dist/db/do.mjs CHANGED
@@ -326,6 +326,7 @@ function loadingPage() {
326
326
  <meta name="viewport" content="width=device-width, initial-scale=1">
327
327
  <meta http-equiv="refresh" content="2">
328
328
  <title>Loading preview...</title>
329
+ <link rel="icon" href="data:image/svg+xml,<svg width='75' height='75' viewBox='0 0 75 75' fill='none' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='3' width='69' height='69' rx='10.518' stroke='url(%23pb)' stroke-width='6'/><rect x='18' y='34' width='39.366' height='6.561' fill='url(%23pd)'/><defs><linearGradient id='pb' x1='-43' y1='124' x2='92.42' y2='-41.75' gradientUnits='userSpaceOnUse'><stop stop-color='%230F006B'/><stop offset='.08' stop-color='%23281A81'/><stop offset='.17' stop-color='%235D0C83'/><stop offset='.25' stop-color='%23911475'/><stop offset='.33' stop-color='%23CE2F55'/><stop offset='.42' stop-color='%23FF6633'/><stop offset='.5' stop-color='%23F6821F'/><stop offset='.58' stop-color='%23FBAD41'/><stop offset='.67' stop-color='%23FFCD89'/><stop offset='.75' stop-color='%23FFE9CB'/><stop offset='.83' stop-color='%23FFF7EC'/><stop offset='.92' stop-color='%23FFF8EE'/><stop offset='1' stop-color='white'/></linearGradient><linearGradient id='pd' x1='91.5' y1='27.5' x2='28.12' y2='54.18' gradientUnits='userSpaceOnUse'><stop stop-color='white'/><stop offset='.13' stop-color='%23FFF8EE'/><stop offset='.62' stop-color='%23FBAD41'/><stop offset='.85' stop-color='%23F6821F'/><stop offset='1' stop-color='%23FF6633'/></linearGradient></defs></svg>" />
329
330
  <style>
330
331
  body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #fafafa; color: #333; }
331
332
  .spinner { width: 40px; height: 40px; border: 3px solid #e0e0e0; border-top-color: #333; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 16px; }
@@ -7,6 +7,275 @@ import { ulid } from "ulidx";
7
7
  import { defineMiddleware } from "astro:middleware";
8
8
  import virtualConfig from "virtual:emdash/config";
9
9
 
10
+ //#region src/db/playground-loading.ts
11
+ /**
12
+ * Playground Loading Page
13
+ *
14
+ * Rendered when a user first hits /playground. Shows an animated loading state
15
+ * while the client-side JS calls /_playground/init to create the DO, run
16
+ * migrations, and apply the seed. Once init completes, redirects to the admin.
17
+ *
18
+ * No dependencies -- plain HTML with inline styles and a <script> tag.
19
+ */
20
+ function renderPlaygroundLoadingPage() {
21
+ return `<!DOCTYPE html>
22
+ <html lang="en">
23
+ <head>
24
+ <meta charset="utf-8" />
25
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
26
+ <title>EmDash Playground</title>
27
+ <link rel="icon" href="data:image/svg+xml,<svg width='75' height='75' viewBox='0 0 75 75' fill='none' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='3' width='69' height='69' rx='10.518' stroke='url(%23pb)' stroke-width='6'/><rect x='18' y='34' width='39.366' height='6.561' fill='url(%23pd)'/><defs><linearGradient id='pb' x1='-43' y1='124' x2='92.42' y2='-41.75' gradientUnits='userSpaceOnUse'><stop stop-color='%230F006B'/><stop offset='.08' stop-color='%23281A81'/><stop offset='.17' stop-color='%235D0C83'/><stop offset='.25' stop-color='%23911475'/><stop offset='.33' stop-color='%23CE2F55'/><stop offset='.42' stop-color='%23FF6633'/><stop offset='.5' stop-color='%23F6821F'/><stop offset='.58' stop-color='%23FBAD41'/><stop offset='.67' stop-color='%23FFCD89'/><stop offset='.75' stop-color='%23FFE9CB'/><stop offset='.83' stop-color='%23FFF7EC'/><stop offset='.92' stop-color='%23FFF8EE'/><stop offset='1' stop-color='white'/></linearGradient><linearGradient id='pd' x1='91.5' y1='27.5' x2='28.12' y2='54.18' gradientUnits='userSpaceOnUse'><stop stop-color='white'/><stop offset='.13' stop-color='%23FFF8EE'/><stop offset='.62' stop-color='%23FBAD41'/><stop offset='.85' stop-color='%23F6821F'/><stop offset='1' stop-color='%23FF6633'/></linearGradient></defs></svg>" />
28
+ <style>
29
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
30
+
31
+ body {
32
+ min-height: 100dvh;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ background: #0a0a0a;
37
+ color: #e0e0e0;
38
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
39
+ -webkit-font-smoothing: antialiased;
40
+ }
41
+
42
+ .pg-loading {
43
+ text-align: center;
44
+ display: flex;
45
+ flex-direction: column;
46
+ align-items: center;
47
+ gap: 32px;
48
+ }
49
+
50
+ .pg-logo {
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ gap: 12px;
55
+ font-size: 28px;
56
+ font-weight: 700;
57
+ letter-spacing: -0.02em;
58
+ color: #fff;
59
+ }
60
+
61
+ .pg-logo svg {
62
+ width: 36px;
63
+ height: 36px;
64
+ flex-shrink: 0;
65
+ }
66
+
67
+ .pg-spinner-wrap {
68
+ position: relative;
69
+ width: 48px;
70
+ height: 48px;
71
+ }
72
+
73
+ .pg-spinner {
74
+ width: 48px;
75
+ height: 48px;
76
+ border: 3px solid rgba(255, 255, 255, 0.08);
77
+ border-top-color: #facc15;
78
+ border-radius: 50%;
79
+ animation: pg-spin 0.8s linear infinite;
80
+ }
81
+
82
+ @keyframes pg-spin {
83
+ to { transform: rotate(360deg); }
84
+ }
85
+
86
+ .pg-message {
87
+ font-size: 15px;
88
+ color: #888;
89
+ line-height: 1.5;
90
+ }
91
+
92
+ .pg-steps {
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: 8px;
96
+ margin-top: 4px;
97
+ }
98
+
99
+ .pg-step {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 8px;
103
+ font-size: 13px;
104
+ color: #555;
105
+ transition: color 0.3s;
106
+ }
107
+
108
+ .pg-step.active {
109
+ color: #ccc;
110
+ }
111
+
112
+ .pg-step.done {
113
+ color: #4ade80;
114
+ }
115
+
116
+ .pg-step-dot {
117
+ width: 6px;
118
+ height: 6px;
119
+ border-radius: 50%;
120
+ background: #333;
121
+ flex-shrink: 0;
122
+ transition: background 0.3s;
123
+ }
124
+
125
+ .pg-step.active .pg-step-dot {
126
+ background: #facc15;
127
+ box-shadow: 0 0 6px rgba(250, 204, 21, 0.4);
128
+ }
129
+
130
+ .pg-step.done .pg-step-dot {
131
+ background: #4ade80;
132
+ }
133
+
134
+ .pg-error {
135
+ display: none;
136
+ flex-direction: column;
137
+ align-items: center;
138
+ gap: 16px;
139
+ }
140
+
141
+ .pg-error.visible {
142
+ display: flex;
143
+ }
144
+
145
+ .pg-error-message {
146
+ font-size: 14px;
147
+ color: #f87171;
148
+ max-width: 360px;
149
+ line-height: 1.5;
150
+ }
151
+
152
+ .pg-retry-btn {
153
+ display: inline-flex;
154
+ align-items: center;
155
+ gap: 6px;
156
+ padding: 8px 16px;
157
+ background: rgba(250, 204, 21, 0.12);
158
+ color: #facc15;
159
+ border: none;
160
+ border-radius: 999px;
161
+ font-size: 13px;
162
+ font-weight: 500;
163
+ cursor: pointer;
164
+ font-family: inherit;
165
+ transition: background 0.15s;
166
+ }
167
+
168
+ .pg-retry-btn:hover {
169
+ background: rgba(250, 204, 21, 0.22);
170
+ }
171
+ </style>
172
+ </head>
173
+ <body>
174
+ <div class="pg-loading">
175
+ <div class="pg-logo"><svg viewBox="0 0 75 75" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="3" width="69" height="69" rx="10.518" stroke="url(#pl-b)" stroke-width="6"/><rect x="18" y="34" width="39.366" height="6.561" fill="url(#pl-d)"/><defs><linearGradient id="pl-b" x1="-43" y1="124" x2="92.42" y2="-41.75" gradientUnits="userSpaceOnUse"><stop stop-color="#0F006B"/><stop offset=".08" stop-color="#281A81"/><stop offset=".17" stop-color="#5D0C83"/><stop offset=".25" stop-color="#911475"/><stop offset=".33" stop-color="#CE2F55"/><stop offset=".42" stop-color="#FF6633"/><stop offset=".5" stop-color="#F6821F"/><stop offset=".58" stop-color="#FBAD41"/><stop offset=".67" stop-color="#FFCD89"/><stop offset=".75" stop-color="#FFE9CB"/><stop offset=".83" stop-color="#FFF7EC"/><stop offset=".92" stop-color="#FFF8EE"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="pl-d" x1="91.5" y1="27.5" x2="28.12" y2="54.18" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset=".13" stop-color="#FFF8EE"/><stop offset=".62" stop-color="#FBAD41"/><stop offset=".85" stop-color="#F6821F"/><stop offset="1" stop-color="#FF6633"/></linearGradient></defs></svg>EmDash</div>
176
+
177
+ <div class="pg-spinner-wrap">
178
+ <div class="pg-spinner" id="pg-spinner"></div>
179
+ </div>
180
+
181
+ <div>
182
+ <div class="pg-message" id="pg-message">Creating your playground&hellip;</div>
183
+ <div class="pg-steps" id="pg-steps">
184
+ <div class="pg-step active" id="step-db">
185
+ <span class="pg-step-dot"></span>
186
+ Setting up database
187
+ </div>
188
+ <div class="pg-step" id="step-content">
189
+ <span class="pg-step-dot"></span>
190
+ Loading demo content
191
+ </div>
192
+ <div class="pg-step" id="step-ready">
193
+ <span class="pg-step-dot"></span>
194
+ Almost ready
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ <div class="pg-error" id="pg-error">
200
+ <div class="pg-error-message" id="pg-error-message"></div>
201
+ <button class="pg-retry-btn" id="pg-retry">Try again</button>
202
+ </div>
203
+ </div>
204
+
205
+ <script>
206
+ (function() {
207
+ var steps = ["step-db", "step-content", "step-ready"];
208
+ var currentStep = 0;
209
+
210
+ function setStep(index) {
211
+ for (var i = 0; i < steps.length; i++) {
212
+ var el = document.getElementById(steps[i]);
213
+ if (!el) continue;
214
+ el.className = "pg-step" + (i < index ? " done" : i === index ? " active" : "");
215
+ }
216
+ currentStep = index;
217
+ }
218
+
219
+ function showError(message) {
220
+ document.getElementById("pg-spinner").style.display = "none";
221
+ document.getElementById("pg-message").textContent = "Something went wrong";
222
+ document.getElementById("pg-steps").style.display = "none";
223
+ var errorEl = document.getElementById("pg-error");
224
+ var errorMsg = document.getElementById("pg-error-message");
225
+ if (errorEl) errorEl.className = "pg-error visible";
226
+ if (errorMsg) errorMsg.textContent = message;
227
+ }
228
+
229
+ function init() {
230
+ setStep(0);
231
+ document.getElementById("pg-spinner").style.display = "";
232
+ document.getElementById("pg-message").textContent = "Creating your playground\\u2026";
233
+ document.getElementById("pg-steps").style.display = "";
234
+ var errorEl = document.getElementById("pg-error");
235
+ if (errorEl) errorEl.className = "pg-error";
236
+
237
+ // Advance steps on a timer for visual feedback while init runs.
238
+ // The actual init is a single server call -- these steps are cosmetic.
239
+ var stepTimer = setTimeout(function() { setStep(1); }, 800);
240
+ var stepTimer2 = setTimeout(function() { setStep(2); }, 2000);
241
+
242
+ fetch("/_playground/init", { method: "POST", credentials: "same-origin" })
243
+ .then(function(res) {
244
+ clearTimeout(stepTimer);
245
+ clearTimeout(stepTimer2);
246
+ if (!res.ok) {
247
+ return res.json().then(function(body) {
248
+ throw new Error(body.error?.message || "Initialization failed");
249
+ });
250
+ }
251
+ return res.json();
252
+ })
253
+ .then(function() {
254
+ // Mark all steps done
255
+ setStep(steps.length);
256
+ document.getElementById("pg-message").textContent = "Ready!";
257
+ // Brief pause so the user sees "Ready!" before navigating
258
+ setTimeout(function() {
259
+ location.replace("/_emdash/admin");
260
+ }, 400);
261
+ })
262
+ .catch(function(err) {
263
+ clearTimeout(stepTimer);
264
+ clearTimeout(stepTimer2);
265
+ showError(err.message || "Failed to create playground. Please try again.");
266
+ });
267
+ }
268
+
269
+ document.getElementById("pg-retry").addEventListener("click", init);
270
+
271
+ init();
272
+ })();
273
+ <\/script>
274
+ </body>
275
+ </html>`;
276
+ }
277
+
278
+ //#endregion
10
279
  //#region src/db/playground-toolbar.ts
11
280
  const RE_AMP = /&/g;
12
281
  const RE_QUOT = /"/g;
@@ -485,14 +754,33 @@ const onRequest = defineMiddleware(async (context, next) => {
485
754
  maxAge: ttl
486
755
  });
487
756
  }
757
+ if (initializedSessions.has(token)) return context.redirect("/_emdash/admin");
758
+ return new Response(renderPlaygroundLoadingPage(), {
759
+ status: 200,
760
+ headers: { "content-type": "text/html; charset=utf-8" }
761
+ });
762
+ }
763
+ if (url.pathname === "/_playground/init" && context.request.method === "POST") {
764
+ const token = cookies.get(COOKIE_NAME)?.value;
765
+ if (!token) return Response.json({ error: {
766
+ code: "NO_SESSION",
767
+ message: "No playground session"
768
+ } }, { status: 400 });
769
+ if (initializedSessions.has(token)) return Response.json({ ok: true });
488
770
  const stub = getStub(binding, token);
489
771
  const db = new Kysely({ dialect: new PreviewDODialect({ getStub: () => stub }) });
490
- if (!initializedSessions.has(token)) {
772
+ try {
491
773
  await initializePlayground(db, token);
492
774
  initializedSessions.add(token);
493
775
  await getFullStub(binding, token).setTtlAlarm(ttl);
776
+ return Response.json({ ok: true });
777
+ } catch (error) {
778
+ console.error("Playground initialization failed:", error);
779
+ return Response.json({ error: {
780
+ code: "PLAYGROUND_INIT_ERROR",
781
+ message: "Failed to initialize playground"
782
+ } }, { status: 500 });
494
783
  }
495
- return context.redirect("/_emdash/admin");
496
784
  }
497
785
  if (url.pathname === "/_playground/reset") {
498
786
  cookies.delete(COOKIE_NAME, { path: "/" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emdash-cms/cloudflare",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Cloudflare adapters for EmDash - D1, R2, Access, and Worker Loader sandbox",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -66,7 +66,7 @@
66
66
  "jose": "^6.1.3",
67
67
  "kysely-d1": "^0.4.0",
68
68
  "ulidx": "^2.4.1",
69
- "emdash": "0.1.0"
69
+ "emdash": "0.2.0"
70
70
  },
71
71
  "peerDependencies": {
72
72
  "@cloudflare/workers-types": ">=4.0.0",
@@ -57,6 +57,7 @@ function loadingPage(): string {
57
57
  <meta name="viewport" content="width=device-width, initial-scale=1">
58
58
  <meta http-equiv="refresh" content="2">
59
59
  <title>Loading preview...</title>
60
+ <link rel="icon" href="data:image/svg+xml,<svg width='75' height='75' viewBox='0 0 75 75' fill='none' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='3' width='69' height='69' rx='10.518' stroke='url(%23pb)' stroke-width='6'/><rect x='18' y='34' width='39.366' height='6.561' fill='url(%23pd)'/><defs><linearGradient id='pb' x1='-43' y1='124' x2='92.42' y2='-41.75' gradientUnits='userSpaceOnUse'><stop stop-color='%230F006B'/><stop offset='.08' stop-color='%23281A81'/><stop offset='.17' stop-color='%235D0C83'/><stop offset='.25' stop-color='%23911475'/><stop offset='.33' stop-color='%23CE2F55'/><stop offset='.42' stop-color='%23FF6633'/><stop offset='.5' stop-color='%23F6821F'/><stop offset='.58' stop-color='%23FBAD41'/><stop offset='.67' stop-color='%23FFCD89'/><stop offset='.75' stop-color='%23FFE9CB'/><stop offset='.83' stop-color='%23FFF7EC'/><stop offset='.92' stop-color='%23FFF8EE'/><stop offset='1' stop-color='white'/></linearGradient><linearGradient id='pd' x1='91.5' y1='27.5' x2='28.12' y2='54.18' gradientUnits='userSpaceOnUse'><stop stop-color='white'/><stop offset='.13' stop-color='%23FFF8EE'/><stop offset='.62' stop-color='%23FBAD41'/><stop offset='.85' stop-color='%23F6821F'/><stop offset='1' stop-color='%23FF6633'/></linearGradient></defs></svg>" />
60
61
  <style>
61
62
  body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #fafafa; color: #333; }
62
63
  .spinner { width: 40px; height: 40px; border: 3px solid #e0e0e0; border-top-color: #333; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 16px; }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Playground Loading Page
3
+ *
4
+ * Rendered when a user first hits /playground. Shows an animated loading state
5
+ * while the client-side JS calls /_playground/init to create the DO, run
6
+ * migrations, and apply the seed. Once init completes, redirects to the admin.
7
+ *
8
+ * No dependencies -- plain HTML with inline styles and a <script> tag.
9
+ */
10
+
11
+ export function renderPlaygroundLoadingPage(): string {
12
+ return `<!DOCTYPE html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8" />
16
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
17
+ <title>EmDash Playground</title>
18
+ <link rel="icon" href="data:image/svg+xml,<svg width='75' height='75' viewBox='0 0 75 75' fill='none' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='3' width='69' height='69' rx='10.518' stroke='url(%23pb)' stroke-width='6'/><rect x='18' y='34' width='39.366' height='6.561' fill='url(%23pd)'/><defs><linearGradient id='pb' x1='-43' y1='124' x2='92.42' y2='-41.75' gradientUnits='userSpaceOnUse'><stop stop-color='%230F006B'/><stop offset='.08' stop-color='%23281A81'/><stop offset='.17' stop-color='%235D0C83'/><stop offset='.25' stop-color='%23911475'/><stop offset='.33' stop-color='%23CE2F55'/><stop offset='.42' stop-color='%23FF6633'/><stop offset='.5' stop-color='%23F6821F'/><stop offset='.58' stop-color='%23FBAD41'/><stop offset='.67' stop-color='%23FFCD89'/><stop offset='.75' stop-color='%23FFE9CB'/><stop offset='.83' stop-color='%23FFF7EC'/><stop offset='.92' stop-color='%23FFF8EE'/><stop offset='1' stop-color='white'/></linearGradient><linearGradient id='pd' x1='91.5' y1='27.5' x2='28.12' y2='54.18' gradientUnits='userSpaceOnUse'><stop stop-color='white'/><stop offset='.13' stop-color='%23FFF8EE'/><stop offset='.62' stop-color='%23FBAD41'/><stop offset='.85' stop-color='%23F6821F'/><stop offset='1' stop-color='%23FF6633'/></linearGradient></defs></svg>" />
19
+ <style>
20
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
21
+
22
+ body {
23
+ min-height: 100dvh;
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ background: #0a0a0a;
28
+ color: #e0e0e0;
29
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
30
+ -webkit-font-smoothing: antialiased;
31
+ }
32
+
33
+ .pg-loading {
34
+ text-align: center;
35
+ display: flex;
36
+ flex-direction: column;
37
+ align-items: center;
38
+ gap: 32px;
39
+ }
40
+
41
+ .pg-logo {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ gap: 12px;
46
+ font-size: 28px;
47
+ font-weight: 700;
48
+ letter-spacing: -0.02em;
49
+ color: #fff;
50
+ }
51
+
52
+ .pg-logo svg {
53
+ width: 36px;
54
+ height: 36px;
55
+ flex-shrink: 0;
56
+ }
57
+
58
+ .pg-spinner-wrap {
59
+ position: relative;
60
+ width: 48px;
61
+ height: 48px;
62
+ }
63
+
64
+ .pg-spinner {
65
+ width: 48px;
66
+ height: 48px;
67
+ border: 3px solid rgba(255, 255, 255, 0.08);
68
+ border-top-color: #facc15;
69
+ border-radius: 50%;
70
+ animation: pg-spin 0.8s linear infinite;
71
+ }
72
+
73
+ @keyframes pg-spin {
74
+ to { transform: rotate(360deg); }
75
+ }
76
+
77
+ .pg-message {
78
+ font-size: 15px;
79
+ color: #888;
80
+ line-height: 1.5;
81
+ }
82
+
83
+ .pg-steps {
84
+ display: flex;
85
+ flex-direction: column;
86
+ gap: 8px;
87
+ margin-top: 4px;
88
+ }
89
+
90
+ .pg-step {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 8px;
94
+ font-size: 13px;
95
+ color: #555;
96
+ transition: color 0.3s;
97
+ }
98
+
99
+ .pg-step.active {
100
+ color: #ccc;
101
+ }
102
+
103
+ .pg-step.done {
104
+ color: #4ade80;
105
+ }
106
+
107
+ .pg-step-dot {
108
+ width: 6px;
109
+ height: 6px;
110
+ border-radius: 50%;
111
+ background: #333;
112
+ flex-shrink: 0;
113
+ transition: background 0.3s;
114
+ }
115
+
116
+ .pg-step.active .pg-step-dot {
117
+ background: #facc15;
118
+ box-shadow: 0 0 6px rgba(250, 204, 21, 0.4);
119
+ }
120
+
121
+ .pg-step.done .pg-step-dot {
122
+ background: #4ade80;
123
+ }
124
+
125
+ .pg-error {
126
+ display: none;
127
+ flex-direction: column;
128
+ align-items: center;
129
+ gap: 16px;
130
+ }
131
+
132
+ .pg-error.visible {
133
+ display: flex;
134
+ }
135
+
136
+ .pg-error-message {
137
+ font-size: 14px;
138
+ color: #f87171;
139
+ max-width: 360px;
140
+ line-height: 1.5;
141
+ }
142
+
143
+ .pg-retry-btn {
144
+ display: inline-flex;
145
+ align-items: center;
146
+ gap: 6px;
147
+ padding: 8px 16px;
148
+ background: rgba(250, 204, 21, 0.12);
149
+ color: #facc15;
150
+ border: none;
151
+ border-radius: 999px;
152
+ font-size: 13px;
153
+ font-weight: 500;
154
+ cursor: pointer;
155
+ font-family: inherit;
156
+ transition: background 0.15s;
157
+ }
158
+
159
+ .pg-retry-btn:hover {
160
+ background: rgba(250, 204, 21, 0.22);
161
+ }
162
+ </style>
163
+ </head>
164
+ <body>
165
+ <div class="pg-loading">
166
+ <div class="pg-logo"><svg viewBox="0 0 75 75" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="3" width="69" height="69" rx="10.518" stroke="url(#pl-b)" stroke-width="6"/><rect x="18" y="34" width="39.366" height="6.561" fill="url(#pl-d)"/><defs><linearGradient id="pl-b" x1="-43" y1="124" x2="92.42" y2="-41.75" gradientUnits="userSpaceOnUse"><stop stop-color="#0F006B"/><stop offset=".08" stop-color="#281A81"/><stop offset=".17" stop-color="#5D0C83"/><stop offset=".25" stop-color="#911475"/><stop offset=".33" stop-color="#CE2F55"/><stop offset=".42" stop-color="#FF6633"/><stop offset=".5" stop-color="#F6821F"/><stop offset=".58" stop-color="#FBAD41"/><stop offset=".67" stop-color="#FFCD89"/><stop offset=".75" stop-color="#FFE9CB"/><stop offset=".83" stop-color="#FFF7EC"/><stop offset=".92" stop-color="#FFF8EE"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="pl-d" x1="91.5" y1="27.5" x2="28.12" y2="54.18" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset=".13" stop-color="#FFF8EE"/><stop offset=".62" stop-color="#FBAD41"/><stop offset=".85" stop-color="#F6821F"/><stop offset="1" stop-color="#FF6633"/></linearGradient></defs></svg>EmDash</div>
167
+
168
+ <div class="pg-spinner-wrap">
169
+ <div class="pg-spinner" id="pg-spinner"></div>
170
+ </div>
171
+
172
+ <div>
173
+ <div class="pg-message" id="pg-message">Creating your playground&hellip;</div>
174
+ <div class="pg-steps" id="pg-steps">
175
+ <div class="pg-step active" id="step-db">
176
+ <span class="pg-step-dot"></span>
177
+ Setting up database
178
+ </div>
179
+ <div class="pg-step" id="step-content">
180
+ <span class="pg-step-dot"></span>
181
+ Loading demo content
182
+ </div>
183
+ <div class="pg-step" id="step-ready">
184
+ <span class="pg-step-dot"></span>
185
+ Almost ready
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ <div class="pg-error" id="pg-error">
191
+ <div class="pg-error-message" id="pg-error-message"></div>
192
+ <button class="pg-retry-btn" id="pg-retry">Try again</button>
193
+ </div>
194
+ </div>
195
+
196
+ <script>
197
+ (function() {
198
+ var steps = ["step-db", "step-content", "step-ready"];
199
+ var currentStep = 0;
200
+
201
+ function setStep(index) {
202
+ for (var i = 0; i < steps.length; i++) {
203
+ var el = document.getElementById(steps[i]);
204
+ if (!el) continue;
205
+ el.className = "pg-step" + (i < index ? " done" : i === index ? " active" : "");
206
+ }
207
+ currentStep = index;
208
+ }
209
+
210
+ function showError(message) {
211
+ document.getElementById("pg-spinner").style.display = "none";
212
+ document.getElementById("pg-message").textContent = "Something went wrong";
213
+ document.getElementById("pg-steps").style.display = "none";
214
+ var errorEl = document.getElementById("pg-error");
215
+ var errorMsg = document.getElementById("pg-error-message");
216
+ if (errorEl) errorEl.className = "pg-error visible";
217
+ if (errorMsg) errorMsg.textContent = message;
218
+ }
219
+
220
+ function init() {
221
+ setStep(0);
222
+ document.getElementById("pg-spinner").style.display = "";
223
+ document.getElementById("pg-message").textContent = "Creating your playground\\u2026";
224
+ document.getElementById("pg-steps").style.display = "";
225
+ var errorEl = document.getElementById("pg-error");
226
+ if (errorEl) errorEl.className = "pg-error";
227
+
228
+ // Advance steps on a timer for visual feedback while init runs.
229
+ // The actual init is a single server call -- these steps are cosmetic.
230
+ var stepTimer = setTimeout(function() { setStep(1); }, 800);
231
+ var stepTimer2 = setTimeout(function() { setStep(2); }, 2000);
232
+
233
+ fetch("/_playground/init", { method: "POST", credentials: "same-origin" })
234
+ .then(function(res) {
235
+ clearTimeout(stepTimer);
236
+ clearTimeout(stepTimer2);
237
+ if (!res.ok) {
238
+ return res.json().then(function(body) {
239
+ throw new Error(body.error?.message || "Initialization failed");
240
+ });
241
+ }
242
+ return res.json();
243
+ })
244
+ .then(function() {
245
+ // Mark all steps done
246
+ setStep(steps.length);
247
+ document.getElementById("pg-message").textContent = "Ready!";
248
+ // Brief pause so the user sees "Ready!" before navigating
249
+ setTimeout(function() {
250
+ location.replace("/_emdash/admin");
251
+ }, 400);
252
+ })
253
+ .catch(function(err) {
254
+ clearTimeout(stepTimer);
255
+ clearTimeout(stepTimer2);
256
+ showError(err.message || "Failed to create playground. Please try again.");
257
+ });
258
+ }
259
+
260
+ document.getElementById("pg-retry").addEventListener("click", init);
261
+
262
+ init();
263
+ })();
264
+ </script>
265
+ </body>
266
+ </html>`;
267
+ }
@@ -24,6 +24,7 @@ import type { EmDashPreviewDB } from "./do-class.js";
24
24
  import { PreviewDODialect } from "./do-dialect.js";
25
25
  import type { PreviewDBStub } from "./do-dialect.js";
26
26
  import { isBlockedInPlayground } from "./do-playground-routes.js";
27
+ import { renderPlaygroundLoadingPage } from "./playground-loading.js";
27
28
  import { renderPlaygroundToolbar } from "./playground-toolbar.js";
28
29
 
29
30
  /** Default TTL for playground data (1 hour) */
@@ -244,6 +245,9 @@ export const onRequest = defineMiddleware(async (context, next) => {
244
245
  const binding = getBindingName();
245
246
 
246
247
  // --- Entry point: /playground ---
248
+ // Show a loading page immediately. The page calls /_playground/init via
249
+ // fetch to do the actual setup, then redirects to admin when ready.
250
+ // If the session is already initialized, skip the loading page.
247
251
  if (url.pathname === "/playground") {
248
252
  let token = cookies.get(COOKIE_NAME)?.value;
249
253
  if (!token) {
@@ -256,19 +260,49 @@ export const onRequest = defineMiddleware(async (context, next) => {
256
260
  });
257
261
  }
258
262
 
263
+ // Already initialized? Skip the loading page and go straight to admin.
264
+ if (initializedSessions.has(token)) {
265
+ return context.redirect("/_emdash/admin");
266
+ }
267
+
268
+ return new Response(renderPlaygroundLoadingPage(), {
269
+ status: 200,
270
+ headers: { "content-type": "text/html; charset=utf-8" },
271
+ });
272
+ }
273
+
274
+ // --- Init endpoint: called by the loading page ---
275
+ if (url.pathname === "/_playground/init" && context.request.method === "POST") {
276
+ const token = cookies.get(COOKIE_NAME)?.value;
277
+ if (!token) {
278
+ return Response.json(
279
+ { error: { code: "NO_SESSION", message: "No playground session" } },
280
+ { status: 400 },
281
+ );
282
+ }
283
+
284
+ if (initializedSessions.has(token)) {
285
+ return Response.json({ ok: true });
286
+ }
287
+
259
288
  const stub = getStub(binding, token);
260
289
  const dialect = new PreviewDODialect({ getStub: () => stub });
261
290
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
262
291
  const db = new Kysely<any>({ dialect });
263
292
 
264
- if (!initializedSessions.has(token)) {
293
+ try {
265
294
  await initializePlayground(db, token);
266
295
  initializedSessions.add(token);
267
296
  const fullStub = getFullStub(binding, token);
268
297
  await fullStub.setTtlAlarm(ttl);
298
+ return Response.json({ ok: true });
299
+ } catch (error) {
300
+ console.error("Playground initialization failed:", error);
301
+ return Response.json(
302
+ { error: { code: "PLAYGROUND_INIT_ERROR", message: "Failed to initialize playground" } },
303
+ { status: 500 },
304
+ );
269
305
  }
270
-
271
- return context.redirect("/_emdash/admin");
272
306
  }
273
307
 
274
308
  // --- Reset endpoint ---