@authrim/setup 0.1.4 → 0.1.7

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/dist/web/ui.js CHANGED
@@ -2,10 +2,12 @@
2
2
  * HTML Template for Authrim Setup Web UI
3
3
  *
4
4
  * A simple, self-contained UI for the setup wizard.
5
+ * Follows the setup flow defined in the design document.
5
6
  */
6
- export function getHtmlTemplate(sessionToken) {
7
+ export function getHtmlTemplate(sessionToken, manageOnly) {
7
8
  // Escape token for safe embedding in JavaScript
8
9
  const safeToken = sessionToken ? sessionToken.replace(/['"\\]/g, '') : '';
10
+ const manageOnlyFlag = manageOnly ? 'true' : 'false';
9
11
  return `<!DOCTYPE html>
10
12
  <html lang="en">
11
13
  <head>
@@ -90,6 +92,71 @@ export function getHtmlTemplate(sessionToken) {
90
92
  .status-success { background: #d1fae5; color: var(--success); }
91
93
  .status-error { background: #fee2e2; color: var(--error); }
92
94
 
95
+ /* Mode selection cards */
96
+ .mode-cards {
97
+ display: grid;
98
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
99
+ gap: 1rem;
100
+ margin-bottom: 1rem;
101
+ }
102
+
103
+ .mode-card {
104
+ border: 2px solid var(--border);
105
+ border-radius: 12px;
106
+ padding: 1.5rem;
107
+ cursor: pointer;
108
+ transition: all 0.2s;
109
+ position: relative;
110
+ }
111
+
112
+ .mode-card:hover {
113
+ border-color: var(--primary);
114
+ background: #f8fafc;
115
+ }
116
+
117
+ .mode-card.selected {
118
+ border-color: var(--primary);
119
+ background: #eff6ff;
120
+ }
121
+
122
+ .mode-card .mode-icon {
123
+ font-size: 2rem;
124
+ margin-bottom: 0.5rem;
125
+ }
126
+
127
+ .mode-card h3 {
128
+ font-size: 1.1rem;
129
+ margin-bottom: 0.5rem;
130
+ }
131
+
132
+ .mode-card p {
133
+ font-size: 0.875rem;
134
+ color: var(--text-muted);
135
+ margin-bottom: 0.75rem;
136
+ }
137
+
138
+ .mode-card ul {
139
+ font-size: 0.8rem;
140
+ color: var(--text-muted);
141
+ margin-left: 1rem;
142
+ }
143
+
144
+ .mode-card ul li {
145
+ margin-bottom: 0.25rem;
146
+ }
147
+
148
+ .mode-badge {
149
+ position: absolute;
150
+ top: -8px;
151
+ right: 10px;
152
+ background: var(--primary);
153
+ color: white;
154
+ font-size: 0.7rem;
155
+ padding: 0.2rem 0.5rem;
156
+ border-radius: 4px;
157
+ font-weight: 500;
158
+ }
159
+
93
160
  .form-group {
94
161
  margin-bottom: 1rem;
95
162
  }
@@ -102,6 +169,7 @@ export function getHtmlTemplate(sessionToken) {
102
169
 
103
170
  input[type="text"],
104
171
  input[type="password"],
172
+ input[type="file"],
105
173
  select {
106
174
  width: 100%;
107
175
  padding: 0.75rem;
@@ -257,7 +325,297 @@ export function getHtmlTemplate(sessionToken) {
257
325
  color: var(--primary);
258
326
  }
259
327
 
328
+ .file-input-wrapper {
329
+ position: relative;
330
+ overflow: hidden;
331
+ display: inline-block;
332
+ }
333
+
334
+ .file-input-wrapper input[type=file] {
335
+ position: absolute;
336
+ left: 0;
337
+ top: 0;
338
+ opacity: 0;
339
+ cursor: pointer;
340
+ width: 100%;
341
+ height: 100%;
342
+ }
343
+
344
+ .file-input-btn {
345
+ display: inline-block;
346
+ padding: 0.75rem 1.5rem;
347
+ background: var(--border);
348
+ color: var(--text);
349
+ border-radius: 8px;
350
+ cursor: pointer;
351
+ }
352
+
353
+ .file-input-btn:hover {
354
+ background: #cbd5e1;
355
+ }
356
+
357
+ .config-preview {
358
+ background: var(--bg);
359
+ border-radius: 8px;
360
+ padding: 1rem;
361
+ margin-top: 1rem;
362
+ font-family: 'Monaco', 'Menlo', monospace;
363
+ font-size: 0.8rem;
364
+ max-height: 200px;
365
+ overflow-y: auto;
366
+ }
367
+
260
368
  .hidden { display: none; }
369
+
370
+ /* Resource preview styles */
371
+ .resource-preview {
372
+ background: var(--bg);
373
+ border: 1px solid var(--border);
374
+ border-radius: 8px;
375
+ padding: 1rem;
376
+ margin-bottom: 1rem;
377
+ }
378
+
379
+ .resource-list {
380
+ display: grid;
381
+ gap: 1rem;
382
+ }
383
+
384
+ .resource-category {
385
+ font-size: 0.875rem;
386
+ }
387
+
388
+ .resource-category strong {
389
+ display: block;
390
+ margin-bottom: 0.5rem;
391
+ color: var(--text);
392
+ }
393
+
394
+ .resource-category ul {
395
+ margin: 0;
396
+ padding-left: 1.5rem;
397
+ color: var(--text-muted);
398
+ }
399
+
400
+ .resource-category li {
401
+ font-family: 'Monaco', 'Menlo', monospace;
402
+ font-size: 0.8rem;
403
+ margin-bottom: 0.25rem;
404
+ }
405
+
406
+ /* Progress spinner */
407
+ .spinner {
408
+ display: inline-block;
409
+ width: 16px;
410
+ height: 16px;
411
+ border: 2px solid var(--border);
412
+ border-top-color: var(--primary);
413
+ border-radius: 50%;
414
+ animation: spin 1s linear infinite;
415
+ margin-right: 0.5rem;
416
+ }
417
+
418
+ @keyframes spin {
419
+ to { transform: rotate(360deg); }
420
+ }
421
+
422
+ .progress-item {
423
+ display: flex;
424
+ align-items: center;
425
+ margin-bottom: 0.5rem;
426
+ color: #e2e8f0;
427
+ }
428
+
429
+ .progress-item.complete {
430
+ color: var(--success);
431
+ }
432
+
433
+ .progress-item.error {
434
+ color: var(--error);
435
+ }
436
+
437
+ /* Environment cards */
438
+ .env-cards {
439
+ display: grid;
440
+ gap: 1rem;
441
+ margin-bottom: 1rem;
442
+ }
443
+
444
+ .env-card {
445
+ border: 1px solid var(--border);
446
+ border-radius: 8px;
447
+ padding: 1rem;
448
+ display: flex;
449
+ justify-content: space-between;
450
+ align-items: center;
451
+ }
452
+
453
+ .env-card:hover {
454
+ border-color: var(--primary);
455
+ background: #f8fafc;
456
+ }
457
+
458
+ .env-card-info {
459
+ flex: 1;
460
+ }
461
+
462
+ .env-card-name {
463
+ font-size: 1.1rem;
464
+ font-weight: 600;
465
+ margin-bottom: 0.5rem;
466
+ }
467
+
468
+ .env-card-stats {
469
+ display: flex;
470
+ gap: 1rem;
471
+ font-size: 0.8rem;
472
+ color: var(--text-muted);
473
+ }
474
+
475
+ .env-card-stat {
476
+ display: flex;
477
+ align-items: center;
478
+ gap: 0.25rem;
479
+ }
480
+
481
+ .env-card-actions {
482
+ display: flex;
483
+ gap: 0.5rem;
484
+ }
485
+
486
+ .btn-danger {
487
+ background: var(--error);
488
+ color: white;
489
+ padding: 0.5rem 1rem;
490
+ font-size: 0.875rem;
491
+ }
492
+
493
+ .btn-danger:hover {
494
+ background: #dc2626;
495
+ }
496
+
497
+ .btn-info {
498
+ background: var(--primary);
499
+ color: white;
500
+ padding: 0.5rem 1rem;
501
+ font-size: 0.875rem;
502
+ }
503
+
504
+ .btn-info:hover {
505
+ background: var(--primary-dark);
506
+ }
507
+
508
+ /* Resource list in details view */
509
+ .resource-section {
510
+ margin-bottom: 1.5rem;
511
+ }
512
+
513
+ .resource-section-title {
514
+ font-size: 1rem;
515
+ font-weight: 600;
516
+ margin-bottom: 0.75rem;
517
+ display: flex;
518
+ align-items: center;
519
+ gap: 0.5rem;
520
+ }
521
+
522
+ .resource-section-title .count {
523
+ font-size: 0.8rem;
524
+ color: var(--text-muted);
525
+ font-weight: normal;
526
+ }
527
+
528
+ .resource-list {
529
+ background: var(--bg);
530
+ border-radius: 8px;
531
+ padding: 0.75rem;
532
+ max-height: 200px;
533
+ overflow-y: auto;
534
+ }
535
+
536
+ .resource-item {
537
+ font-family: 'Monaco', 'Menlo', monospace;
538
+ font-size: 0.8rem;
539
+ padding: 0.25rem 0.5rem;
540
+ margin-bottom: 0.25rem;
541
+ background: var(--card-bg);
542
+ border-radius: 4px;
543
+ border: 1px solid var(--border);
544
+ }
545
+
546
+ .resource-item:last-child {
547
+ margin-bottom: 0;
548
+ }
549
+
550
+ .resource-item-name {
551
+ font-weight: 500;
552
+ }
553
+
554
+ .resource-item-details {
555
+ font-size: 0.75rem;
556
+ color: var(--text-muted);
557
+ margin-top: 0.25rem;
558
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
559
+ }
560
+
561
+ .resource-item-details span {
562
+ margin-right: 1rem;
563
+ }
564
+
565
+ .resource-item-loading {
566
+ color: var(--text-muted);
567
+ font-style: italic;
568
+ }
569
+
570
+ .resource-item-error {
571
+ color: var(--error);
572
+ }
573
+
574
+ .resource-item-not-deployed {
575
+ color: var(--warning);
576
+ font-style: italic;
577
+ }
578
+
579
+ .resource-empty {
580
+ color: var(--text-muted);
581
+ font-style: italic;
582
+ font-size: 0.875rem;
583
+ }
584
+
585
+ /* Delete options */
586
+ .delete-options {
587
+ display: grid;
588
+ gap: 0.75rem;
589
+ }
590
+
591
+ .delete-option {
592
+ display: flex;
593
+ align-items: center;
594
+ gap: 0.75rem;
595
+ padding: 0.75rem;
596
+ border: 1px solid var(--border);
597
+ border-radius: 8px;
598
+ cursor: pointer;
599
+ }
600
+
601
+ .delete-option:hover {
602
+ background: var(--bg);
603
+ }
604
+
605
+ .delete-option input[type="checkbox"] {
606
+ width: 18px;
607
+ height: 18px;
608
+ }
609
+
610
+ .delete-option span {
611
+ display: flex;
612
+ flex-direction: column;
613
+ }
614
+
615
+ .delete-option small {
616
+ color: var(--text-muted);
617
+ font-size: 0.8rem;
618
+ }
261
619
  </style>
262
620
  </head>
263
621
  <body>
@@ -267,7 +625,7 @@ export function getHtmlTemplate(sessionToken) {
267
625
  <p class="subtitle">OIDC Provider on Cloudflare Workers</p>
268
626
  </header>
269
627
 
270
- <div class="step-indicator">
628
+ <div class="step-indicator" id="step-indicator">
271
629
  <div class="step step-active" id="step-1">1</div>
272
630
  <div class="step-connector"></div>
273
631
  <div class="step step-pending" id="step-2">2</div>
@@ -288,6 +646,91 @@ export function getHtmlTemplate(sessionToken) {
288
646
  </div>
289
647
  </div>
290
648
 
649
+ <!-- Step 1.5: Top Menu (New Setup / Load Config / Manage) -->
650
+ <div id="section-top-menu" class="card hidden">
651
+ <h2 class="card-title">Get Started</h2>
652
+ <p style="margin-bottom: 1.5rem; color: var(--text-muted);">Choose an option to continue:</p>
653
+
654
+ <div class="mode-cards" style="grid-template-columns: repeat(3, 1fr);">
655
+ <div class="mode-card" id="menu-new-setup">
656
+ <div class="mode-icon">🆕</div>
657
+ <h3>New Setup</h3>
658
+ <p>Create a new Authrim deployment from scratch</p>
659
+ </div>
660
+
661
+ <div class="mode-card" id="menu-load-config">
662
+ <div class="mode-icon">📂</div>
663
+ <h3>Load Config</h3>
664
+ <p>Resume or redeploy using existing config</p>
665
+ </div>
666
+
667
+ <div class="mode-card" id="menu-manage-env">
668
+ <div class="mode-icon">🗑️</div>
669
+ <h3>Manage Environments</h3>
670
+ <p>View, inspect, or delete existing environments</p>
671
+ </div>
672
+ </div>
673
+ </div>
674
+
675
+ <!-- Step 1.6: Setup Mode Selection (Quick / Custom) -->
676
+ <div id="section-mode" class="card hidden">
677
+ <h2 class="card-title">Setup Mode</h2>
678
+ <p style="margin-bottom: 1.5rem; color: var(--text-muted);">Choose how you want to set up Authrim:</p>
679
+
680
+ <div class="mode-cards">
681
+ <div class="mode-card" id="mode-quick">
682
+ <div class="mode-icon">⚡</div>
683
+ <h3>Quick Setup</h3>
684
+ <p>Get started in ~5 minutes</p>
685
+ <ul>
686
+ <li>Environment selection</li>
687
+ <li>Optional custom domain</li>
688
+ <li>Default components</li>
689
+ </ul>
690
+ <span class="mode-badge">Recommended</span>
691
+ </div>
692
+
693
+ <div class="mode-card" id="mode-custom">
694
+ <div class="mode-icon">🔧</div>
695
+ <h3>Custom Setup</h3>
696
+ <p>Full control over configuration</p>
697
+ <ul>
698
+ <li>Component selection</li>
699
+ <li>URL configuration</li>
700
+ <li>Advanced settings</li>
701
+ </ul>
702
+ </div>
703
+ </div>
704
+
705
+ <div class="button-group">
706
+ <button class="btn-secondary" id="btn-back-top">Back</button>
707
+ </div>
708
+ </div>
709
+
710
+ <!-- Step 1.7: Load Config -->
711
+ <div id="section-load-config" class="card hidden">
712
+ <h2 class="card-title">Load Configuration</h2>
713
+ <p style="margin-bottom: 1rem; color: var(--text-muted);">Select your authrim-config.json file:</p>
714
+
715
+ <div class="form-group">
716
+ <div class="file-input-wrapper">
717
+ <span class="file-input-btn">📁 Choose File</span>
718
+ <input type="file" id="config-file" accept=".json">
719
+ </div>
720
+ <span id="config-file-name" style="margin-left: 1rem; color: var(--text-muted);"></span>
721
+ </div>
722
+
723
+ <div id="config-preview-section" class="hidden">
724
+ <h3 style="font-size: 1rem; margin-bottom: 0.5rem;">Configuration Preview</h3>
725
+ <div class="config-preview" id="config-preview"></div>
726
+ </div>
727
+
728
+ <div class="button-group">
729
+ <button class="btn-secondary" id="btn-back-top-2">Back</button>
730
+ <button class="btn-primary" id="btn-load-config" disabled>Load & Continue</button>
731
+ </div>
732
+ </div>
733
+
291
734
  <!-- Step 2: Configuration -->
292
735
  <div id="section-config" class="card hidden">
293
736
  <h2 class="card-title">Configuration</h2>
@@ -298,40 +741,55 @@ export function getHtmlTemplate(sessionToken) {
298
741
  <option value="prod">Production (prod)</option>
299
742
  <option value="staging">Staging</option>
300
743
  <option value="dev">Development (dev)</option>
744
+ <option value="custom">Custom...</option>
301
745
  </select>
302
746
  </div>
303
747
 
748
+ <div class="form-group hidden" id="custom-env-group">
749
+ <label for="custom-env">Custom Environment Name</label>
750
+ <input type="text" id="custom-env" placeholder="e.g., test, demo, myenv">
751
+ <small style="color: var(--text-muted)">Lowercase letters, numbers, and hyphens only</small>
752
+ </div>
753
+
304
754
  <div class="form-group">
305
755
  <label for="domain">Custom Domain (optional)</label>
306
756
  <input type="text" id="domain" placeholder="auth.example.com">
307
757
  <small style="color: var(--text-muted)">Leave empty to use workers.dev / pages.dev</small>
308
758
  </div>
309
759
 
310
- <h3 style="margin: 1.5rem 0 1rem; font-size: 1rem;">Components</h3>
311
- <div class="checkbox-group">
312
- <label class="checkbox-item">
313
- <input type="checkbox" id="comp-api" checked disabled>
314
- API (required)
315
- </label>
316
- <label class="checkbox-item">
317
- <input type="checkbox" id="comp-login-ui" checked>
318
- Login UI
319
- </label>
320
- <label class="checkbox-item">
321
- <input type="checkbox" id="comp-admin-ui" checked>
322
- Admin UI
323
- </label>
324
- <label class="checkbox-item">
325
- <input type="checkbox" id="comp-saml">
326
- SAML IdP
327
- </label>
328
- <label class="checkbox-item">
329
- <input type="checkbox" id="comp-vc">
330
- Verifiable Credentials
331
- </label>
760
+ <!-- Advanced options (shown in custom mode) -->
761
+ <div id="advanced-options" class="hidden">
762
+ <h3 style="margin: 1.5rem 0 1rem; font-size: 1rem;">Components</h3>
763
+ <div class="checkbox-group">
764
+ <label class="checkbox-item">
765
+ <input type="checkbox" id="comp-api" checked disabled>
766
+ API (required)
767
+ </label>
768
+ <label class="checkbox-item">
769
+ <input type="checkbox" id="comp-login-ui" checked>
770
+ Login UI
771
+ </label>
772
+ <label class="checkbox-item">
773
+ <input type="checkbox" id="comp-admin-ui" checked>
774
+ Admin UI
775
+ </label>
776
+ <label class="checkbox-item">
777
+ <input type="checkbox" id="comp-saml">
778
+ SAML IdP
779
+ </label>
780
+ <label class="checkbox-item">
781
+ <input type="checkbox" id="comp-async">
782
+ Device Flow / CIBA
783
+ </label>
784
+ <label class="checkbox-item">
785
+ <input type="checkbox" id="comp-vc">
786
+ Verifiable Credentials
787
+ </label>
788
+ </div>
332
789
  </div>
333
790
 
334
791
  <div class="button-group">
792
+ <button class="btn-secondary" id="btn-back-mode">Back</button>
335
793
  <button class="btn-primary" id="btn-configure">Continue</button>
336
794
  </div>
337
795
  </div>
@@ -344,19 +802,43 @@ export function getHtmlTemplate(sessionToken) {
344
802
  </h2>
345
803
 
346
804
  <p style="margin-bottom: 1rem;">The following resources will be created:</p>
347
- <ul style="margin-left: 1.5rem; margin-bottom: 1rem;">
348
- <li>2 D1 Databases (core, PII)</li>
349
- <li>8 KV Namespaces</li>
350
- <li>RSA Key Pair for JWT signing</li>
351
- </ul>
805
+
806
+ <!-- Resource names preview -->
807
+ <div id="resource-preview" class="resource-preview">
808
+ <h4 style="font-size: 0.9rem; margin-bottom: 0.75rem; color: var(--text-muted);">📋 Resource Names:</h4>
809
+ <div class="resource-list">
810
+ <div class="resource-category">
811
+ <strong>D1 Databases:</strong>
812
+ <ul id="preview-d1"></ul>
813
+ </div>
814
+ <div class="resource-category">
815
+ <strong>KV Namespaces:</strong>
816
+ <ul id="preview-kv"></ul>
817
+ </div>
818
+ <div class="resource-category">
819
+ <strong>Cryptographic Keys:</strong>
820
+ <ul id="preview-keys"></ul>
821
+ </div>
822
+ </div>
823
+ </div>
352
824
 
353
825
  <div class="progress-log hidden" id="provision-log">
354
826
  <pre id="provision-output"></pre>
355
827
  </div>
356
828
 
829
+ <!-- Keys saved location (shown after completion) -->
830
+ <div id="keys-saved-info" class="alert alert-info hidden" style="margin-top: 1rem;">
831
+ <strong>🔑 Keys saved to:</strong>
832
+ <code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: #f1f5f9; border-radius: 4px;" id="keys-path"></code>
833
+ <p style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-muted);">
834
+ ⚠️ Keep this directory safe and add it to .gitignore
835
+ </p>
836
+ </div>
837
+
357
838
  <div class="button-group">
358
839
  <button class="btn-secondary" id="btn-back-config">Back</button>
359
840
  <button class="btn-primary" id="btn-provision">Create Resources</button>
841
+ <button class="btn-primary hidden" id="btn-goto-deploy">Continue to Deploy →</button>
360
842
  </div>
361
843
  </div>
362
844
 
@@ -382,7 +864,7 @@ export function getHtmlTemplate(sessionToken) {
382
864
  <!-- Complete -->
383
865
  <div id="section-complete" class="card hidden">
384
866
  <h2 class="card-title" style="color: var(--success);">
385
- Setup Complete!
867
+ Setup Complete!
386
868
  </h2>
387
869
 
388
870
  <p>Authrim has been successfully deployed.</p>
@@ -394,20 +876,184 @@ export function getHtmlTemplate(sessionToken) {
394
876
  <div class="alert alert-info" style="margin-top: 1rem;">
395
877
  <strong>Next Steps:</strong>
396
878
  <ol style="margin-left: 1.5rem; margin-top: 0.5rem;">
397
- <li>Visit the Admin UI to create your first client</li>
879
+ <li>Visit the setup URL to create your first admin account</li>
880
+ <li>Log in to the Admin UI to create OAuth clients</li>
398
881
  <li>Configure your application to use the OIDC endpoints</li>
399
882
  </ol>
400
883
  </div>
401
884
  </div>
885
+
886
+ <!-- Environment Management: List -->
887
+ <div id="section-env-list" class="card hidden">
888
+ <h2 class="card-title">
889
+ Manage Environments
890
+ <span class="status-badge status-pending" id="env-list-status">Loading...</span>
891
+ </h2>
892
+
893
+ <p style="margin-bottom: 1rem; color: var(--text-muted);">
894
+ Detected Authrim environments in your Cloudflare account:
895
+ </p>
896
+
897
+ <div id="env-list-loading" class="progress-log">
898
+ <pre id="env-scan-output"></pre>
899
+ </div>
900
+
901
+ <div id="env-list-content" class="hidden">
902
+ <div id="env-cards" class="env-cards">
903
+ <!-- Environment cards will be inserted here -->
904
+ </div>
905
+
906
+ <div id="no-envs-message" class="alert alert-info hidden">
907
+ No Authrim environments detected in this Cloudflare account.
908
+ </div>
909
+ </div>
910
+
911
+ <div class="button-group">
912
+ <button class="btn-secondary" id="btn-back-env-list">Back</button>
913
+ <button class="btn-secondary" id="btn-refresh-env-list">🔄 Refresh</button>
914
+ </div>
915
+ </div>
916
+
917
+ <!-- Environment Management: Details -->
918
+ <div id="section-env-detail" class="card hidden">
919
+ <h2 class="card-title">
920
+ 📋 Environment Details
921
+ <code id="detail-env-name" style="background: var(--bg); padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 1rem;"></code>
922
+ </h2>
923
+
924
+ <div id="detail-resources">
925
+ <!-- Workers -->
926
+ <div class="resource-section">
927
+ <div class="resource-section-title">
928
+ 🔧 Workers <span class="count" id="detail-workers-count">(0)</span>
929
+ </div>
930
+ <div class="resource-list" id="detail-workers-list"></div>
931
+ </div>
932
+
933
+ <!-- D1 Databases -->
934
+ <div class="resource-section">
935
+ <div class="resource-section-title">
936
+ 📊 D1 Databases <span class="count" id="detail-d1-count">(0)</span>
937
+ </div>
938
+ <div class="resource-list" id="detail-d1-list"></div>
939
+ </div>
940
+
941
+ <!-- KV Namespaces -->
942
+ <div class="resource-section">
943
+ <div class="resource-section-title">
944
+ 🗄️ KV Namespaces <span class="count" id="detail-kv-count">(0)</span>
945
+ </div>
946
+ <div class="resource-list" id="detail-kv-list"></div>
947
+ </div>
948
+
949
+ <!-- Queues -->
950
+ <div class="resource-section" id="detail-queues-section">
951
+ <div class="resource-section-title">
952
+ 📨 Queues <span class="count" id="detail-queues-count">(0)</span>
953
+ </div>
954
+ <div class="resource-list" id="detail-queues-list"></div>
955
+ </div>
956
+
957
+ <!-- R2 Buckets -->
958
+ <div class="resource-section" id="detail-r2-section">
959
+ <div class="resource-section-title">
960
+ 📁 R2 Buckets <span class="count" id="detail-r2-count">(0)</span>
961
+ </div>
962
+ <div class="resource-list" id="detail-r2-list"></div>
963
+ </div>
964
+ </div>
965
+
966
+ <div class="button-group">
967
+ <button class="btn-secondary" id="btn-back-env-detail">← Back to List</button>
968
+ <button class="btn-danger" id="btn-delete-from-detail">🗑️ Delete Environment...</button>
969
+ </div>
970
+ </div>
971
+
972
+ <!-- Environment Management: Delete Confirmation -->
973
+ <div id="section-env-delete" class="card hidden">
974
+ <h2 class="card-title" style="color: var(--error);">
975
+ ⚠️ Delete Environment
976
+ </h2>
977
+
978
+ <div class="alert alert-warning">
979
+ <strong>Warning:</strong> This action is irreversible. All selected resources will be permanently deleted.
980
+ </div>
981
+
982
+ <div style="margin: 1.5rem 0;">
983
+ <h3 style="font-size: 1.1rem; margin-bottom: 1rem;">
984
+ Environment: <code id="delete-env-name" style="background: var(--bg); padding: 0.25rem 0.5rem; border-radius: 4px;"></code>
985
+ </h3>
986
+
987
+ <p style="margin-bottom: 1rem; color: var(--text-muted);">Select resources to delete:</p>
988
+
989
+ <div class="delete-options">
990
+ <label class="checkbox-item delete-option">
991
+ <input type="checkbox" id="delete-workers" checked>
992
+ <span>
993
+ <strong>Workers</strong>
994
+ <small id="delete-workers-count">(0 workers)</small>
995
+ </span>
996
+ </label>
997
+
998
+ <label class="checkbox-item delete-option">
999
+ <input type="checkbox" id="delete-d1" checked>
1000
+ <span>
1001
+ <strong>D1 Databases</strong>
1002
+ <small id="delete-d1-count">(0 databases)</small>
1003
+ </span>
1004
+ </label>
1005
+
1006
+ <label class="checkbox-item delete-option">
1007
+ <input type="checkbox" id="delete-kv" checked>
1008
+ <span>
1009
+ <strong>KV Namespaces</strong>
1010
+ <small id="delete-kv-count">(0 namespaces)</small>
1011
+ </span>
1012
+ </label>
1013
+
1014
+ <label class="checkbox-item delete-option">
1015
+ <input type="checkbox" id="delete-queues" checked>
1016
+ <span>
1017
+ <strong>Queues</strong>
1018
+ <small id="delete-queues-count">(0 queues)</small>
1019
+ </span>
1020
+ </label>
1021
+
1022
+ <label class="checkbox-item delete-option">
1023
+ <input type="checkbox" id="delete-r2" checked>
1024
+ <span>
1025
+ <strong>R2 Buckets</strong>
1026
+ <small id="delete-r2-count">(0 buckets)</small>
1027
+ </span>
1028
+ </label>
1029
+ </div>
1030
+ </div>
1031
+
1032
+ <div class="progress-log hidden" id="delete-log">
1033
+ <pre id="delete-output"></pre>
1034
+ </div>
1035
+
1036
+ <div id="delete-result" class="hidden"></div>
1037
+
1038
+ <div class="button-group">
1039
+ <button class="btn-secondary" id="btn-back-env-delete">Cancel</button>
1040
+ <button class="btn-primary" id="btn-confirm-delete" style="background: var(--error);">🗑️ Delete Selected</button>
1041
+ </div>
1042
+ </div>
402
1043
  </div>
403
1044
 
404
1045
  <script>
405
1046
  // Session token for API authentication (embedded by server)
406
1047
  const SESSION_TOKEN = '${safeToken}';
1048
+ const MANAGE_ONLY = ${manageOnlyFlag};
407
1049
 
408
1050
  // State
409
1051
  let currentStep = 1;
1052
+ let setupMode = 'quick'; // 'quick' or 'custom'
410
1053
  let config = {};
1054
+ let loadedConfig = null;
1055
+ let provisioningCompleted = false;
1056
+ let provisionPollInterval = null;
411
1057
 
412
1058
  // Elements
413
1059
  const steps = {
@@ -419,12 +1065,23 @@ export function getHtmlTemplate(sessionToken) {
419
1065
 
420
1066
  const sections = {
421
1067
  prerequisites: document.getElementById('section-prerequisites'),
1068
+ topMenu: document.getElementById('section-top-menu'),
1069
+ mode: document.getElementById('section-mode'),
1070
+ loadConfig: document.getElementById('section-load-config'),
422
1071
  config: document.getElementById('section-config'),
423
1072
  provision: document.getElementById('section-provision'),
424
1073
  deploy: document.getElementById('section-deploy'),
425
1074
  complete: document.getElementById('section-complete'),
1075
+ envList: document.getElementById('section-env-list'),
1076
+ envDetail: document.getElementById('section-env-detail'),
1077
+ envDelete: document.getElementById('section-env-delete'),
426
1078
  };
427
1079
 
1080
+ // Environment management state
1081
+ let detectedEnvironments = [];
1082
+ let selectedEnvForDetail = null;
1083
+ let selectedEnvForDelete = null;
1084
+
428
1085
  // API helpers (with session token authentication)
429
1086
  async function api(endpoint, options = {}) {
430
1087
  const response = await fetch('/api' + endpoint, {
@@ -512,6 +1169,9 @@ export function getHtmlTemplate(sessionToken) {
512
1169
  const code = document.createElement('code');
513
1170
  code.style.display = 'block';
514
1171
  code.style.marginTop = '0.5rem';
1172
+ code.style.padding = '0.5rem';
1173
+ code.style.background = '#f1f5f9';
1174
+ code.style.borderRadius = '4px';
515
1175
  code.textContent = 'npm install -g wrangler';
516
1176
  alertDiv.appendChild(code);
517
1177
 
@@ -537,6 +1197,9 @@ export function getHtmlTemplate(sessionToken) {
537
1197
  const code = document.createElement('code');
538
1198
  code.style.display = 'block';
539
1199
  code.style.marginTop = '0.5rem';
1200
+ code.style.padding = '0.5rem';
1201
+ code.style.background = '#f1f5f9';
1202
+ code.style.borderRadius = '4px';
540
1203
  code.textContent = 'wrangler login';
541
1204
  alertDiv.appendChild(code);
542
1205
 
@@ -556,11 +1219,11 @@ export function getHtmlTemplate(sessionToken) {
556
1219
  alertDiv.className = 'alert alert-success';
557
1220
 
558
1221
  const p1 = document.createElement('p');
559
- p1.textContent = 'Wrangler installed';
1222
+ p1.textContent = 'Wrangler installed';
560
1223
  alertDiv.appendChild(p1);
561
1224
 
562
1225
  const p2 = document.createElement('p');
563
- p2.textContent = 'Logged in as ' + (result.auth.email || 'Unknown');
1226
+ p2.textContent = 'Logged in as ' + (result.auth.email || 'Unknown');
564
1227
  alertDiv.appendChild(p2);
565
1228
 
566
1229
  prereqContent.appendChild(alertDiv);
@@ -570,8 +1233,8 @@ export function getHtmlTemplate(sessionToken) {
570
1233
 
571
1234
  const btn = document.createElement('button');
572
1235
  btn.className = 'btn-primary';
573
- btn.textContent = 'Start Setup';
574
- btn.addEventListener('click', startSetup);
1236
+ btn.textContent = 'Continue';
1237
+ btn.addEventListener('click', showTopMenu);
575
1238
  buttonGroup.appendChild(btn);
576
1239
 
577
1240
  prereqContent.appendChild(buttonGroup);
@@ -586,15 +1249,134 @@ export function getHtmlTemplate(sessionToken) {
586
1249
  }
587
1250
  }
588
1251
 
589
- // Start setup
590
- function startSetup() {
1252
+ // Show top menu
1253
+ function showTopMenu() {
1254
+ showSection('topMenu');
1255
+ }
1256
+
1257
+ // Top menu handlers
1258
+ document.getElementById('menu-new-setup').addEventListener('click', () => {
1259
+ showSection('mode');
1260
+ });
1261
+
1262
+ document.getElementById('menu-load-config').addEventListener('click', () => {
1263
+ showSection('loadConfig');
1264
+ });
1265
+
1266
+ // Setup mode handlers
1267
+ document.getElementById('mode-quick').addEventListener('click', () => {
1268
+ setupMode = 'quick';
1269
+ document.getElementById('mode-quick').classList.add('selected');
1270
+ document.getElementById('mode-custom').classList.remove('selected');
1271
+ document.getElementById('advanced-options').classList.add('hidden');
591
1272
  setStep(2);
592
1273
  showSection('config');
593
- }
1274
+ });
1275
+
1276
+ document.getElementById('mode-custom').addEventListener('click', () => {
1277
+ setupMode = 'custom';
1278
+ document.getElementById('mode-custom').classList.add('selected');
1279
+ document.getElementById('mode-quick').classList.remove('selected');
1280
+ document.getElementById('advanced-options').classList.remove('hidden');
1281
+ setStep(2);
1282
+ showSection('config');
1283
+ });
1284
+
1285
+ document.getElementById('btn-back-top').addEventListener('click', () => {
1286
+ showSection('topMenu');
1287
+ });
1288
+
1289
+ document.getElementById('btn-back-top-2').addEventListener('click', () => {
1290
+ showSection('topMenu');
1291
+ });
1292
+
1293
+ // Load config handlers
1294
+ document.getElementById('config-file').addEventListener('change', (e) => {
1295
+ const file = e.target.files[0];
1296
+ if (!file) return;
1297
+
1298
+ document.getElementById('config-file-name').textContent = file.name;
1299
+
1300
+ const reader = new FileReader();
1301
+ reader.onload = (event) => {
1302
+ try {
1303
+ loadedConfig = JSON.parse(event.target.result);
1304
+ document.getElementById('config-preview').textContent = JSON.stringify(loadedConfig, null, 2);
1305
+ document.getElementById('config-preview-section').classList.remove('hidden');
1306
+ document.getElementById('btn-load-config').disabled = false;
1307
+ } catch (err) {
1308
+ alert('Invalid JSON file: ' + err.message);
1309
+ loadedConfig = null;
1310
+ document.getElementById('btn-load-config').disabled = true;
1311
+ }
1312
+ };
1313
+ reader.readAsText(file);
1314
+ });
1315
+
1316
+ document.getElementById('btn-load-config').addEventListener('click', async () => {
1317
+ if (!loadedConfig) return;
1318
+
1319
+ // Use loaded config
1320
+ config = {
1321
+ env: loadedConfig.environment?.prefix || 'prod',
1322
+ domain: loadedConfig.urls?.api?.custom || null,
1323
+ components: loadedConfig.components || {
1324
+ api: true,
1325
+ loginUi: true,
1326
+ adminUi: true,
1327
+ saml: false,
1328
+ async: false,
1329
+ vc: false,
1330
+ },
1331
+ };
1332
+
1333
+ // Set form values
1334
+ const envSelect = document.getElementById('env');
1335
+ if (['prod', 'staging', 'dev'].includes(config.env)) {
1336
+ envSelect.value = config.env;
1337
+ } else {
1338
+ envSelect.value = 'custom';
1339
+ document.getElementById('custom-env').value = config.env;
1340
+ document.getElementById('custom-env-group').classList.remove('hidden');
1341
+ }
1342
+ document.getElementById('domain').value = config.domain || '';
1343
+
1344
+ // Skip to provisioning if resources already exist
1345
+ if (loadedConfig.resources) {
1346
+ setStep(4);
1347
+ showSection('deploy');
1348
+ } else {
1349
+ setStep(3);
1350
+ showSection('provision');
1351
+ }
1352
+ });
1353
+
1354
+ // Environment dropdown handler
1355
+ document.getElementById('env').addEventListener('change', (e) => {
1356
+ const customGroup = document.getElementById('custom-env-group');
1357
+ if (e.target.value === 'custom') {
1358
+ customGroup.classList.remove('hidden');
1359
+ } else {
1360
+ customGroup.classList.add('hidden');
1361
+ }
1362
+ });
1363
+
1364
+ // Configuration handlers
1365
+ document.getElementById('btn-back-mode').addEventListener('click', () => {
1366
+ setStep(1);
1367
+ showSection('mode');
1368
+ });
594
1369
 
595
- // Configure
596
1370
  document.getElementById('btn-configure').addEventListener('click', async () => {
597
- const env = document.getElementById('env').value;
1371
+ let env = document.getElementById('env').value;
1372
+ if (env === 'custom') {
1373
+ env = document.getElementById('custom-env').value.toLowerCase().replace(/[^a-z0-9-]/g, '');
1374
+ if (!env) {
1375
+ alert('Please enter a valid environment name');
1376
+ return;
1377
+ }
1378
+ }
1379
+
598
1380
  const domain = document.getElementById('domain').value;
599
1381
 
600
1382
  config = {
@@ -602,10 +1384,11 @@ export function getHtmlTemplate(sessionToken) {
602
1384
  domain: domain || null,
603
1385
  components: {
604
1386
  api: true,
605
- loginUi: document.getElementById('comp-login-ui').checked,
606
- adminUi: document.getElementById('comp-admin-ui').checked,
607
- saml: document.getElementById('comp-saml').checked,
608
- vc: document.getElementById('comp-vc').checked,
1387
+ loginUi: setupMode === 'quick' || document.getElementById('comp-login-ui').checked,
1388
+ adminUi: setupMode === 'quick' || document.getElementById('comp-admin-ui').checked,
1389
+ saml: setupMode === 'custom' && document.getElementById('comp-saml').checked,
1390
+ async: setupMode === 'custom' && document.getElementById('comp-async').checked,
1391
+ vc: setupMode === 'custom' && document.getElementById('comp-vc').checked,
609
1392
  },
610
1393
  };
611
1394
 
@@ -615,6 +1398,10 @@ export function getHtmlTemplate(sessionToken) {
615
1398
  body: { env, domain },
616
1399
  });
617
1400
 
1401
+ // Update resource preview with the selected env
1402
+ updateResourcePreview(env);
1403
+ updateProvisionButtons();
1404
+
618
1405
  setStep(3);
619
1406
  showSection('provision');
620
1407
  });
@@ -627,52 +1414,119 @@ export function getHtmlTemplate(sessionToken) {
627
1414
  // Provision
628
1415
  document.getElementById('btn-provision').addEventListener('click', async () => {
629
1416
  const btn = document.getElementById('btn-provision');
1417
+ const btnGotoDeploy = document.getElementById('btn-goto-deploy');
630
1418
  const status = document.getElementById('provision-status');
631
1419
  const log = document.getElementById('provision-log');
632
1420
  const output = document.getElementById('provision-output');
1421
+ const resourcePreview = document.getElementById('resource-preview');
1422
+ const keysSavedInfo = document.getElementById('keys-saved-info');
1423
+ const keysPath = document.getElementById('keys-path');
633
1424
 
634
1425
  btn.disabled = true;
1426
+ btnGotoDeploy.classList.add('hidden');
635
1427
  status.textContent = 'Running...';
636
1428
  status.className = 'status-badge status-running';
637
1429
  log.classList.remove('hidden');
1430
+ resourcePreview.classList.add('hidden');
1431
+ keysSavedInfo.classList.add('hidden');
638
1432
  output.textContent = '';
639
1433
 
1434
+ // Start polling for progress
1435
+ let lastProgressLength = 0;
1436
+ provisionPollInterval = setInterval(async () => {
1437
+ try {
1438
+ const statusResult = await api('/deploy/status');
1439
+ if (statusResult.progress && statusResult.progress.length > lastProgressLength) {
1440
+ // Append new progress messages
1441
+ const newMessages = statusResult.progress.slice(lastProgressLength);
1442
+ newMessages.forEach(msg => {
1443
+ output.textContent += msg + '\\n';
1444
+ });
1445
+ lastProgressLength = statusResult.progress.length;
1446
+ // Auto-scroll to bottom
1447
+ log.scrollTop = log.scrollHeight;
1448
+ }
1449
+ } catch (e) {
1450
+ // Ignore polling errors
1451
+ }
1452
+ }, 500);
1453
+
640
1454
  try {
641
1455
  // Generate keys
642
- output.textContent += 'Generating cryptographic keys...\\n';
643
- await api('/keys/generate', {
1456
+ output.textContent += '🔐 Generating cryptographic keys...\\n';
1457
+ const keyResult = await api('/keys/generate', {
644
1458
  method: 'POST',
645
1459
  body: { keyId: config.env + '-key-' + Date.now() },
646
1460
  });
647
- output.textContent += 'Keys generated\\n\\n';
1461
+ output.textContent += ' RSA key pair generated\\n';
1462
+ output.textContent += ' ✓ Encryption keys generated\\n';
1463
+ output.textContent += ' ✓ Admin secrets generated\\n';
1464
+ output.textContent += '\\n';
1465
+
1466
+ // Show keys saved location (relative to authrim source directory)
1467
+ keysPath.textContent = './.keys/';
648
1468
 
649
1469
  // Provision resources
650
- output.textContent += 'Provisioning Cloudflare resources...\\n';
1470
+ output.textContent += '☁️ Provisioning Cloudflare resources...\\n';
1471
+
651
1472
  const result = await api('/provision', {
652
1473
  method: 'POST',
653
1474
  body: { env: config.env },
654
1475
  });
655
1476
 
1477
+ // Stop polling
1478
+ if (provisionPollInterval) {
1479
+ clearInterval(provisionPollInterval);
1480
+ provisionPollInterval = null;
1481
+ }
1482
+
656
1483
  if (result.success) {
657
- output.textContent += '\\nProvisioning complete!\\n';
1484
+ output.textContent += '\\n✅ Provisioning complete!\\n';
658
1485
  status.textContent = 'Complete';
659
1486
  status.className = 'status-badge status-success';
660
1487
 
661
- setStep(4);
662
- setTimeout(() => showSection('deploy'), 1000);
1488
+ // Mark provisioning as completed
1489
+ provisioningCompleted = true;
1490
+
1491
+ // Show keys saved info
1492
+ keysSavedInfo.classList.remove('hidden');
1493
+
1494
+ // Update buttons
1495
+ btn.textContent = 'Re-provision (Delete & Create)';
1496
+ btn.disabled = false;
1497
+ btnGotoDeploy.classList.remove('hidden');
663
1498
  } else {
664
1499
  throw new Error(result.error);
665
1500
  }
666
1501
  } catch (error) {
667
- output.textContent += '\\nError: ' + error.message + '\\n';
1502
+ // Stop polling
1503
+ if (provisionPollInterval) {
1504
+ clearInterval(provisionPollInterval);
1505
+ provisionPollInterval = null;
1506
+ }
1507
+
1508
+ output.textContent += '\\n❌ Error: ' + error.message + '\\n';
668
1509
  status.textContent = 'Error';
669
1510
  status.className = 'status-badge status-error';
670
1511
  btn.disabled = false;
1512
+ resourcePreview.classList.remove('hidden');
671
1513
  }
672
1514
  });
673
1515
 
1516
+ // Continue to Deploy button
1517
+ document.getElementById('btn-goto-deploy').addEventListener('click', () => {
1518
+ setStep(4);
1519
+ showSection('deploy');
1520
+ });
1521
+
674
1522
  document.getElementById('btn-back-provision').addEventListener('click', () => {
675
1523
  setStep(3);
1524
+ // Update buttons based on provisioning status
1525
+ updateProvisionButtons();
1526
+ // Show resource preview if not completed
1527
+ if (!provisioningCompleted) {
1528
+ document.getElementById('resource-preview').classList.remove('hidden');
1529
+ }
676
1530
  showSection('provision');
677
1531
  });
678
1532
 
@@ -696,7 +1550,7 @@ export function getHtmlTemplate(sessionToken) {
696
1550
  method: 'POST',
697
1551
  body: { env: config.env },
698
1552
  });
699
- output.textContent += 'Config files generated\\n\\n';
1553
+ output.textContent += 'Config files generated\\n\\n';
700
1554
 
701
1555
  // Start deployment
702
1556
  output.textContent += 'Deploying workers...\\n';
@@ -704,7 +1558,7 @@ export function getHtmlTemplate(sessionToken) {
704
1558
  // Poll for status updates
705
1559
  const pollInterval = setInterval(async () => {
706
1560
  const statusResult = await api('/deploy/status');
707
- if (statusResult.progress.length > 0) {
1561
+ if (statusResult.progress && statusResult.progress.length > 0) {
708
1562
  output.textContent = statusResult.progress.join('\\n') + '\\n';
709
1563
  }
710
1564
  }, 1000);
@@ -720,17 +1574,17 @@ export function getHtmlTemplate(sessionToken) {
720
1574
  clearInterval(pollInterval);
721
1575
 
722
1576
  if (result.success) {
723
- output.textContent += '\\nDeployment complete!\\n';
1577
+ output.textContent += '\\n✓ Deployment complete!\\n';
724
1578
  status.textContent = 'Complete';
725
1579
  status.className = 'status-badge status-success';
726
1580
 
727
1581
  // Show completion
728
- showComplete();
1582
+ showComplete(result);
729
1583
  } else {
730
1584
  throw new Error(result.error || 'Deployment failed');
731
1585
  }
732
1586
  } catch (error) {
733
- output.textContent += '\\nError: ' + error.message + '\\n';
1587
+ output.textContent += '\\n✗ Error: ' + error.message + '\\n';
734
1588
  status.textContent = 'Error';
735
1589
  status.className = 'status-badge status-error';
736
1590
  btn.disabled = false;
@@ -738,7 +1592,7 @@ export function getHtmlTemplate(sessionToken) {
738
1592
  });
739
1593
 
740
1594
  // Show completion
741
- function showComplete() {
1595
+ function showComplete(result) {
742
1596
  const urlsEl = document.getElementById('urls');
743
1597
  const env = config.env;
744
1598
  const domain = config.domain;
@@ -753,11 +1607,527 @@ export function getHtmlTemplate(sessionToken) {
753
1607
  urlsEl.appendChild(createUrlItem('Login UI:', loginUrl));
754
1608
  urlsEl.appendChild(createUrlItem('Admin UI:', adminUrl));
755
1609
 
1610
+ // Add setup URL if available
1611
+ if (result && result.setupUrl) {
1612
+ const setupItem = createUrlItem('Admin Setup:', result.setupUrl);
1613
+ setupItem.querySelector('a').style.fontWeight = 'bold';
1614
+ urlsEl.appendChild(setupItem);
1615
+ }
1616
+
756
1617
  showSection('complete');
757
1618
  }
758
1619
 
1620
+ // Resource naming functions
1621
+ function getResourceNames(env) {
1622
+ const envUpper = env.toUpperCase();
1623
+ return {
1624
+ d1: [
1625
+ env + '-authrim-core-db',
1626
+ env + '-authrim-pii-db'
1627
+ ],
1628
+ kv: [
1629
+ envUpper + '-CLIENTS_CACHE',
1630
+ envUpper + '-INITIAL_ACCESS_TOKENS',
1631
+ envUpper + '-SETTINGS',
1632
+ envUpper + '-REBAC_CACHE',
1633
+ envUpper + '-USER_CACHE',
1634
+ envUpper + '-AUTHRIM_CONFIG',
1635
+ envUpper + '-STATE_STORE',
1636
+ envUpper + '-CONSENT_CACHE'
1637
+ ],
1638
+ keys: [
1639
+ '.keys/private.pem (RSA Private Key)',
1640
+ '.keys/public.jwk.json (JWK Public Key)',
1641
+ '.keys/rp_token_encryption_key.txt',
1642
+ '.keys/admin_api_secret.txt',
1643
+ '.keys/key_manager_secret.txt',
1644
+ '.keys/setup_token.txt'
1645
+ ]
1646
+ };
1647
+ }
1648
+
1649
+ function updateResourcePreview(env) {
1650
+ const resources = getResourceNames(env);
1651
+
1652
+ const d1List = document.getElementById('preview-d1');
1653
+ const kvList = document.getElementById('preview-kv');
1654
+ const keysList = document.getElementById('preview-keys');
1655
+
1656
+ d1List.innerHTML = '';
1657
+ kvList.innerHTML = '';
1658
+ keysList.innerHTML = '';
1659
+
1660
+ resources.d1.forEach(name => {
1661
+ const li = document.createElement('li');
1662
+ li.textContent = name;
1663
+ d1List.appendChild(li);
1664
+ });
1665
+
1666
+ resources.kv.forEach(name => {
1667
+ const li = document.createElement('li');
1668
+ li.textContent = name;
1669
+ kvList.appendChild(li);
1670
+ });
1671
+
1672
+ resources.keys.forEach(name => {
1673
+ const li = document.createElement('li');
1674
+ li.textContent = name;
1675
+ keysList.appendChild(li);
1676
+ });
1677
+ }
1678
+
1679
+ // Update provision button state based on completion status
1680
+ function updateProvisionButtons() {
1681
+ const btnProvision = document.getElementById('btn-provision');
1682
+ const btnGotoDeploy = document.getElementById('btn-goto-deploy');
1683
+
1684
+ if (provisioningCompleted) {
1685
+ btnProvision.textContent = 'Re-provision (Delete & Create)';
1686
+ btnProvision.disabled = false;
1687
+ btnGotoDeploy.classList.remove('hidden');
1688
+ } else {
1689
+ btnProvision.textContent = 'Create Resources';
1690
+ btnProvision.disabled = false;
1691
+ btnGotoDeploy.classList.add('hidden');
1692
+ }
1693
+ }
1694
+
1695
+ // =============================================================================
1696
+ // Environment Management
1697
+ // =============================================================================
1698
+
1699
+ // Menu handler for environment management
1700
+ document.getElementById('menu-manage-env').addEventListener('click', () => {
1701
+ loadEnvironments();
1702
+ showSection('envList');
1703
+ });
1704
+
1705
+ // Load environments
1706
+ async function loadEnvironments() {
1707
+ const status = document.getElementById('env-list-status');
1708
+ const loading = document.getElementById('env-list-loading');
1709
+ const content = document.getElementById('env-list-content');
1710
+ const output = document.getElementById('env-scan-output');
1711
+ const noEnvsMessage = document.getElementById('no-envs-message');
1712
+
1713
+ status.textContent = 'Scanning...';
1714
+ status.className = 'status-badge status-running';
1715
+ loading.classList.remove('hidden');
1716
+ content.classList.add('hidden');
1717
+ output.textContent = '';
1718
+
1719
+ // Poll for progress
1720
+ let lastProgressLength = 0;
1721
+ const pollInterval = setInterval(async () => {
1722
+ try {
1723
+ const statusResult = await api('/deploy/status');
1724
+ if (statusResult.progress && statusResult.progress.length > lastProgressLength) {
1725
+ const newMessages = statusResult.progress.slice(lastProgressLength);
1726
+ newMessages.forEach(msg => {
1727
+ output.textContent += msg + '\\n';
1728
+ });
1729
+ lastProgressLength = statusResult.progress.length;
1730
+ }
1731
+ } catch (e) {}
1732
+ }, 500);
1733
+
1734
+ try {
1735
+ const result = await api('/environments');
1736
+ clearInterval(pollInterval);
1737
+
1738
+ if (result.success) {
1739
+ detectedEnvironments = result.environments || [];
1740
+
1741
+ status.textContent = detectedEnvironments.length + ' found';
1742
+ status.className = 'status-badge status-success';
1743
+ loading.classList.add('hidden');
1744
+ content.classList.remove('hidden');
1745
+
1746
+ renderEnvironmentCards();
1747
+ } else {
1748
+ throw new Error(result.error);
1749
+ }
1750
+ } catch (error) {
1751
+ clearInterval(pollInterval);
1752
+ status.textContent = 'Error';
1753
+ status.className = 'status-badge status-error';
1754
+ output.textContent += '\\n❌ Error: ' + error.message;
1755
+ }
1756
+ }
1757
+
1758
+ // Render environment cards
1759
+ function renderEnvironmentCards() {
1760
+ const container = document.getElementById('env-cards');
1761
+ const noEnvsMessage = document.getElementById('no-envs-message');
1762
+
1763
+ container.innerHTML = '';
1764
+
1765
+ if (detectedEnvironments.length === 0) {
1766
+ noEnvsMessage.classList.remove('hidden');
1767
+ return;
1768
+ }
1769
+
1770
+ noEnvsMessage.classList.add('hidden');
1771
+
1772
+ for (const env of detectedEnvironments) {
1773
+ const card = document.createElement('div');
1774
+ card.className = 'env-card';
1775
+
1776
+ const info = document.createElement('div');
1777
+ info.className = 'env-card-info';
1778
+
1779
+ const name = document.createElement('div');
1780
+ name.className = 'env-card-name';
1781
+ name.textContent = env.env;
1782
+ info.appendChild(name);
1783
+
1784
+ const stats = document.createElement('div');
1785
+ stats.className = 'env-card-stats';
1786
+
1787
+ const statItems = [
1788
+ { icon: '🔧', label: 'Workers', count: env.workers.length },
1789
+ { icon: '📊', label: 'D1', count: env.d1.length },
1790
+ { icon: '🗄️', label: 'KV', count: env.kv.length },
1791
+ { icon: '📨', label: 'Queues', count: env.queues.length },
1792
+ { icon: '📁', label: 'R2', count: env.r2.length },
1793
+ ];
1794
+
1795
+ for (const item of statItems) {
1796
+ if (item.count > 0) {
1797
+ const stat = document.createElement('span');
1798
+ stat.className = 'env-card-stat';
1799
+ stat.textContent = item.icon + ' ' + item.count + ' ' + item.label;
1800
+ stats.appendChild(stat);
1801
+ }
1802
+ }
1803
+
1804
+ info.appendChild(stats);
1805
+ card.appendChild(info);
1806
+
1807
+ const actions = document.createElement('div');
1808
+ actions.className = 'env-card-actions';
1809
+
1810
+ const detailBtn = document.createElement('button');
1811
+ detailBtn.className = 'btn-info';
1812
+ detailBtn.textContent = '📋 Details';
1813
+ detailBtn.addEventListener('click', () => showEnvDetail(env));
1814
+ actions.appendChild(detailBtn);
1815
+
1816
+ card.appendChild(actions);
1817
+ container.appendChild(card);
1818
+ }
1819
+ }
1820
+
1821
+ // Show environment details
1822
+ function showEnvDetail(env) {
1823
+ selectedEnvForDetail = env;
1824
+
1825
+ document.getElementById('detail-env-name').textContent = env.env;
1826
+
1827
+ // Render resource lists with loading state
1828
+ renderResourceList('detail-workers-list', 'detail-workers-count', env.workers, 'name', 'worker');
1829
+ renderResourceList('detail-d1-list', 'detail-d1-count', env.d1, 'name', 'd1');
1830
+ renderResourceList('detail-kv-list', 'detail-kv-count', env.kv, 'name', 'kv');
1831
+ renderResourceList('detail-queues-list', 'detail-queues-count', env.queues, 'name', 'queue');
1832
+ renderResourceList('detail-r2-list', 'detail-r2-count', env.r2, 'name', 'r2');
1833
+
1834
+ // Hide empty sections
1835
+ document.getElementById('detail-queues-section').style.display = env.queues.length === 0 ? 'none' : 'block';
1836
+ document.getElementById('detail-r2-section').style.display = env.r2.length === 0 ? 'none' : 'block';
1837
+
1838
+ showSection('envDetail');
1839
+
1840
+ // Load details asynchronously
1841
+ loadResourceDetails(env);
1842
+ }
1843
+
1844
+ // Helper to render resource list
1845
+ function renderResourceList(listId, countId, resources, nameKey, resourceType) {
1846
+ const list = document.getElementById(listId);
1847
+ const count = document.getElementById(countId);
1848
+
1849
+ list.innerHTML = '';
1850
+ count.textContent = '(' + resources.length + ')';
1851
+
1852
+ if (resources.length === 0) {
1853
+ const empty = document.createElement('div');
1854
+ empty.className = 'resource-empty';
1855
+ empty.textContent = 'None';
1856
+ list.appendChild(empty);
1857
+ return;
1858
+ }
1859
+
1860
+ for (const resource of resources) {
1861
+ const item = document.createElement('div');
1862
+ item.className = 'resource-item';
1863
+ item.id = 'resource-' + resourceType + '-' + (resource.name || resource.title || '').replace(/[^a-zA-Z0-9-]/g, '_');
1864
+
1865
+ const nameDiv = document.createElement('div');
1866
+ nameDiv.className = 'resource-item-name';
1867
+ nameDiv.textContent = resource[nameKey] || resource.title || resource.id || 'Unknown';
1868
+ item.appendChild(nameDiv);
1869
+
1870
+ // Add loading placeholder for D1 and Workers
1871
+ if (resourceType === 'd1' || resourceType === 'worker') {
1872
+ const detailsDiv = document.createElement('div');
1873
+ detailsDiv.className = 'resource-item-details resource-item-loading';
1874
+ detailsDiv.textContent = 'Loading...';
1875
+ item.appendChild(detailsDiv);
1876
+ }
1877
+
1878
+ list.appendChild(item);
1879
+ }
1880
+ }
1881
+
1882
+ // Load resource details asynchronously
1883
+ async function loadResourceDetails(env) {
1884
+ // Load D1 and Worker details in parallel
1885
+ const d1Promises = env.d1.map(db => loadD1Details(db.name));
1886
+ const workerPromises = env.workers.map(w => loadWorkerDetails(w.name));
1887
+
1888
+ // Wait for all to complete (don't block on errors)
1889
+ await Promise.allSettled([...d1Promises, ...workerPromises]);
1890
+ }
1891
+
1892
+ // Load D1 database details
1893
+ async function loadD1Details(name) {
1894
+ try {
1895
+ const result = await fetch('/api/d1/' + encodeURIComponent(name) + '/info').then(r => r.json());
1896
+
1897
+ const itemId = 'resource-d1-' + name.replace(/[^a-zA-Z0-9-]/g, '_');
1898
+ const item = document.getElementById(itemId);
1899
+ if (!item) return;
1900
+
1901
+ const detailsDiv = item.querySelector('.resource-item-details');
1902
+ if (!detailsDiv) return;
1903
+
1904
+ if (result.success && result.info) {
1905
+ const info = result.info;
1906
+ detailsDiv.className = 'resource-item-details';
1907
+ detailsDiv.innerHTML = '';
1908
+
1909
+ if (info.databaseSize) {
1910
+ const span = document.createElement('span');
1911
+ span.textContent = '📦 ' + info.databaseSize;
1912
+ detailsDiv.appendChild(span);
1913
+ }
1914
+ if (info.region) {
1915
+ const span = document.createElement('span');
1916
+ span.textContent = '🌍 ' + info.region;
1917
+ detailsDiv.appendChild(span);
1918
+ }
1919
+ if (info.createdAt) {
1920
+ const span = document.createElement('span');
1921
+ span.textContent = '📅 ' + formatDate(info.createdAt);
1922
+ detailsDiv.appendChild(span);
1923
+ }
1924
+ } else {
1925
+ detailsDiv.className = 'resource-item-details resource-item-error';
1926
+ detailsDiv.textContent = 'Failed to load';
1927
+ }
1928
+ } catch (e) {
1929
+ console.error('Failed to load D1 details:', e);
1930
+ }
1931
+ }
1932
+
1933
+ // Load Worker deployment details
1934
+ async function loadWorkerDetails(name) {
1935
+ try {
1936
+ const result = await fetch('/api/worker/' + encodeURIComponent(name) + '/deployments').then(r => r.json());
1937
+
1938
+ const itemId = 'resource-worker-' + name.replace(/[^a-zA-Z0-9-]/g, '_');
1939
+ const item = document.getElementById(itemId);
1940
+ if (!item) return;
1941
+
1942
+ const detailsDiv = item.querySelector('.resource-item-details');
1943
+ if (!detailsDiv) return;
1944
+
1945
+ if (result.success && result.deployments) {
1946
+ const info = result.deployments;
1947
+ detailsDiv.className = 'resource-item-details';
1948
+ detailsDiv.innerHTML = '';
1949
+
1950
+ if (!info.exists) {
1951
+ detailsDiv.className = 'resource-item-details resource-item-not-deployed';
1952
+ detailsDiv.textContent = '⚠️ Not deployed';
1953
+ return;
1954
+ }
1955
+
1956
+ if (info.lastDeployedAt) {
1957
+ const span = document.createElement('span');
1958
+ span.textContent = '🚀 ' + formatDate(info.lastDeployedAt);
1959
+ detailsDiv.appendChild(span);
1960
+ }
1961
+ if (info.author) {
1962
+ const span = document.createElement('span');
1963
+ span.textContent = '👤 ' + info.author;
1964
+ detailsDiv.appendChild(span);
1965
+ }
1966
+ if (info.versionId) {
1967
+ const span = document.createElement('span');
1968
+ span.textContent = '🏷️ ' + info.versionId.substring(0, 8) + '...';
1969
+ detailsDiv.appendChild(span);
1970
+ }
1971
+ } else {
1972
+ detailsDiv.className = 'resource-item-details resource-item-not-deployed';
1973
+ detailsDiv.textContent = '⚠️ Not deployed';
1974
+ }
1975
+ } catch (e) {
1976
+ console.error('Failed to load Worker details:', e);
1977
+ }
1978
+ }
1979
+
1980
+ // Format ISO date to readable format with timezone
1981
+ function formatDate(isoString) {
1982
+ try {
1983
+ const date = new Date(isoString);
1984
+ const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1985
+ // Get timezone abbreviation
1986
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
1987
+ const tzAbbr = date.toLocaleTimeString('en-US', { timeZoneName: 'short' }).split(' ').pop();
1988
+ return dateStr + ' (' + tzAbbr + ')';
1989
+ } catch {
1990
+ return isoString;
1991
+ }
1992
+ }
1993
+
1994
+ // Show delete confirmation
1995
+ function showDeleteConfirmation(env) {
1996
+ selectedEnvForDelete = env;
1997
+
1998
+ document.getElementById('delete-env-name').textContent = env.env;
1999
+ document.getElementById('delete-workers-count').textContent = '(' + env.workers.length + ' workers)';
2000
+ document.getElementById('delete-d1-count').textContent = '(' + env.d1.length + ' databases)';
2001
+ document.getElementById('delete-kv-count').textContent = '(' + env.kv.length + ' namespaces)';
2002
+ document.getElementById('delete-queues-count').textContent = '(' + env.queues.length + ' queues)';
2003
+ document.getElementById('delete-r2-count').textContent = '(' + env.r2.length + ' buckets)';
2004
+
2005
+ // Reset checkboxes
2006
+ document.getElementById('delete-workers').checked = true;
2007
+ document.getElementById('delete-d1').checked = true;
2008
+ document.getElementById('delete-kv').checked = true;
2009
+ document.getElementById('delete-queues').checked = true;
2010
+ document.getElementById('delete-r2').checked = true;
2011
+
2012
+ // Reset UI state
2013
+ document.getElementById('delete-log').classList.add('hidden');
2014
+ document.getElementById('delete-result').classList.add('hidden');
2015
+ document.getElementById('delete-result').innerHTML = '';
2016
+ document.getElementById('btn-confirm-delete').disabled = false;
2017
+
2018
+ showSection('envDelete');
2019
+ }
2020
+
2021
+ // Back buttons for environment management
2022
+ document.getElementById('btn-back-env-list').addEventListener('click', () => {
2023
+ showSection('topMenu');
2024
+ });
2025
+
2026
+ document.getElementById('btn-refresh-env-list').addEventListener('click', () => {
2027
+ loadEnvironments();
2028
+ });
2029
+
2030
+ document.getElementById('btn-back-env-detail').addEventListener('click', () => {
2031
+ showSection('envList');
2032
+ });
2033
+
2034
+ document.getElementById('btn-delete-from-detail').addEventListener('click', () => {
2035
+ if (selectedEnvForDetail) {
2036
+ showDeleteConfirmation(selectedEnvForDetail);
2037
+ }
2038
+ });
2039
+
2040
+ document.getElementById('btn-back-env-delete').addEventListener('click', () => {
2041
+ // Go back to detail view if we came from there
2042
+ if (selectedEnvForDetail) {
2043
+ showSection('envDetail');
2044
+ } else {
2045
+ showSection('envList');
2046
+ }
2047
+ });
2048
+
2049
+ // Delete environment
2050
+ document.getElementById('btn-confirm-delete').addEventListener('click', async () => {
2051
+ if (!selectedEnvForDelete) return;
2052
+
2053
+ const btn = document.getElementById('btn-confirm-delete');
2054
+ const log = document.getElementById('delete-log');
2055
+ const output = document.getElementById('delete-output');
2056
+ const result = document.getElementById('delete-result');
2057
+
2058
+ btn.disabled = true;
2059
+ log.classList.remove('hidden');
2060
+ result.classList.add('hidden');
2061
+ output.textContent = '';
2062
+
2063
+ const deleteOptions = {
2064
+ deleteWorkers: document.getElementById('delete-workers').checked,
2065
+ deleteD1: document.getElementById('delete-d1').checked,
2066
+ deleteKV: document.getElementById('delete-kv').checked,
2067
+ deleteQueues: document.getElementById('delete-queues').checked,
2068
+ deleteR2: document.getElementById('delete-r2').checked,
2069
+ };
2070
+
2071
+ // Poll for progress
2072
+ let lastProgressLength = 0;
2073
+ const pollInterval = setInterval(async () => {
2074
+ try {
2075
+ const statusResult = await api('/deploy/status');
2076
+ if (statusResult.progress && statusResult.progress.length > lastProgressLength) {
2077
+ const newMessages = statusResult.progress.slice(lastProgressLength);
2078
+ newMessages.forEach(msg => {
2079
+ output.textContent += msg + '\\n';
2080
+ });
2081
+ lastProgressLength = statusResult.progress.length;
2082
+ log.scrollTop = log.scrollHeight;
2083
+ }
2084
+ } catch (e) {}
2085
+ }, 500);
2086
+
2087
+ try {
2088
+ const deleteResult = await api('/environments/' + selectedEnvForDelete.env + '/delete', {
2089
+ method: 'POST',
2090
+ body: deleteOptions,
2091
+ });
2092
+
2093
+ clearInterval(pollInterval);
2094
+
2095
+ // Show final progress
2096
+ if (deleteResult.progress) {
2097
+ output.textContent = deleteResult.progress.join('\\n');
2098
+ }
2099
+
2100
+ result.classList.remove('hidden');
2101
+
2102
+ if (deleteResult.success) {
2103
+ result.innerHTML = '<div class="alert alert-success">✅ Environment deleted successfully!</div>';
2104
+
2105
+ // Refresh environment list after a short delay
2106
+ setTimeout(() => {
2107
+ loadEnvironments();
2108
+ showSection('envList');
2109
+ }, 2000);
2110
+ } else {
2111
+ result.innerHTML = '<div class="alert alert-error">❌ Some errors occurred: ' + (deleteResult.errors || []).join(', ') + '</div>';
2112
+ btn.disabled = false;
2113
+ }
2114
+ } catch (error) {
2115
+ clearInterval(pollInterval);
2116
+ result.classList.remove('hidden');
2117
+ result.innerHTML = '<div class="alert alert-error">❌ Error: ' + error.message + '</div>';
2118
+ btn.disabled = false;
2119
+ }
2120
+ });
2121
+
759
2122
  // Initialize
760
- checkPrerequisites();
2123
+ if (MANAGE_ONLY) {
2124
+ // Skip prerequisites UI and go directly to environment management
2125
+ // Prerequisites were already checked by CLI
2126
+ loadEnvironments();
2127
+ showSection('envList');
2128
+ } else {
2129
+ checkPrerequisites();
2130
+ }
761
2131
  </script>
762
2132
  </body>
763
2133
  </html>`;