@authrim/setup 0.1.5 → 0.1.8

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
@@ -4,9 +4,10 @@
4
4
  * A simple, self-contained UI for the setup wizard.
5
5
  * Follows the setup flow defined in the design document.
6
6
  */
7
- export function getHtmlTemplate(sessionToken) {
7
+ export function getHtmlTemplate(sessionToken, manageOnly) {
8
8
  // Escape token for safe embedding in JavaScript
9
9
  const safeToken = sessionToken ? sessionToken.replace(/['"\\]/g, '') : '';
10
+ const manageOnlyFlag = manageOnly ? 'true' : 'false';
10
11
  return `<!DOCTYPE html>
11
12
  <html lang="en">
12
13
  <head>
@@ -365,6 +366,256 @@ export function getHtmlTemplate(sessionToken) {
365
366
  }
366
367
 
367
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
+ }
368
619
  </style>
369
620
  </head>
370
621
  <body>
@@ -395,12 +646,12 @@ export function getHtmlTemplate(sessionToken) {
395
646
  </div>
396
647
  </div>
397
648
 
398
- <!-- Step 1.5: Top Menu (New Setup / Load Config) -->
649
+ <!-- Step 1.5: Top Menu (New Setup / Load Config / Manage) -->
399
650
  <div id="section-top-menu" class="card hidden">
400
651
  <h2 class="card-title">Get Started</h2>
401
652
  <p style="margin-bottom: 1.5rem; color: var(--text-muted);">Choose an option to continue:</p>
402
653
 
403
- <div class="mode-cards">
654
+ <div class="mode-cards" style="grid-template-columns: repeat(3, 1fr);">
404
655
  <div class="mode-card" id="menu-new-setup">
405
656
  <div class="mode-icon">🆕</div>
406
657
  <h3>New Setup</h3>
@@ -409,8 +660,14 @@ export function getHtmlTemplate(sessionToken) {
409
660
 
410
661
  <div class="mode-card" id="menu-load-config">
411
662
  <div class="mode-icon">📂</div>
412
- <h3>Load Existing Config</h3>
413
- <p>Resume or redeploy using an existing authrim-config.json</p>
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>
414
671
  </div>
415
672
  </div>
416
673
  </div>
@@ -545,19 +802,43 @@ export function getHtmlTemplate(sessionToken) {
545
802
  </h2>
546
803
 
547
804
  <p style="margin-bottom: 1rem;">The following resources will be created:</p>
548
- <ul style="margin-left: 1.5rem; margin-bottom: 1rem;">
549
- <li>2 D1 Databases (core, PII)</li>
550
- <li>8 KV Namespaces</li>
551
- <li>RSA Key Pair for JWT signing</li>
552
- </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>
553
824
 
554
825
  <div class="progress-log hidden" id="provision-log">
555
826
  <pre id="provision-output"></pre>
556
827
  </div>
557
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
+
558
838
  <div class="button-group">
559
839
  <button class="btn-secondary" id="btn-back-config">Back</button>
560
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>
561
842
  </div>
562
843
  </div>
563
844
 
@@ -601,17 +882,178 @@ export function getHtmlTemplate(sessionToken) {
601
882
  </ol>
602
883
  </div>
603
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>
604
1043
  </div>
605
1044
 
606
1045
  <script>
607
1046
  // Session token for API authentication (embedded by server)
608
1047
  const SESSION_TOKEN = '${safeToken}';
1048
+ const MANAGE_ONLY = ${manageOnlyFlag};
609
1049
 
610
1050
  // State
611
1051
  let currentStep = 1;
612
1052
  let setupMode = 'quick'; // 'quick' or 'custom'
613
1053
  let config = {};
614
1054
  let loadedConfig = null;
1055
+ let provisioningCompleted = false;
1056
+ let provisionPollInterval = null;
615
1057
 
616
1058
  // Elements
617
1059
  const steps = {
@@ -630,8 +1072,16 @@ export function getHtmlTemplate(sessionToken) {
630
1072
  provision: document.getElementById('section-provision'),
631
1073
  deploy: document.getElementById('section-deploy'),
632
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'),
633
1078
  };
634
1079
 
1080
+ // Environment management state
1081
+ let detectedEnvironments = [];
1082
+ let selectedEnvForDetail = null;
1083
+ let selectedEnvForDelete = null;
1084
+
635
1085
  // API helpers (with session token authentication)
636
1086
  async function api(endpoint, options = {}) {
637
1087
  const response = await fetch('/api' + endpoint, {
@@ -948,6 +1398,10 @@ export function getHtmlTemplate(sessionToken) {
948
1398
  body: { env, domain },
949
1399
  });
950
1400
 
1401
+ // Update resource preview with the selected env
1402
+ updateResourcePreview(env);
1403
+ updateProvisionButtons();
1404
+
951
1405
  setStep(3);
952
1406
  showSection('provision');
953
1407
  });
@@ -960,52 +1414,119 @@ export function getHtmlTemplate(sessionToken) {
960
1414
  // Provision
961
1415
  document.getElementById('btn-provision').addEventListener('click', async () => {
962
1416
  const btn = document.getElementById('btn-provision');
1417
+ const btnGotoDeploy = document.getElementById('btn-goto-deploy');
963
1418
  const status = document.getElementById('provision-status');
964
1419
  const log = document.getElementById('provision-log');
965
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');
966
1424
 
967
1425
  btn.disabled = true;
1426
+ btnGotoDeploy.classList.add('hidden');
968
1427
  status.textContent = 'Running...';
969
1428
  status.className = 'status-badge status-running';
970
1429
  log.classList.remove('hidden');
1430
+ resourcePreview.classList.add('hidden');
1431
+ keysSavedInfo.classList.add('hidden');
971
1432
  output.textContent = '';
972
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
+
973
1454
  try {
974
1455
  // Generate keys
975
- output.textContent += 'Generating cryptographic keys...\\n';
976
- await api('/keys/generate', {
1456
+ output.textContent += '🔐 Generating cryptographic keys...\\n';
1457
+ const keyResult = await api('/keys/generate', {
977
1458
  method: 'POST',
978
1459
  body: { keyId: config.env + '-key-' + Date.now() },
979
1460
  });
980
- 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/';
981
1468
 
982
1469
  // Provision resources
983
- output.textContent += 'Provisioning Cloudflare resources...\\n';
1470
+ output.textContent += '☁️ Provisioning Cloudflare resources...\\n';
1471
+
984
1472
  const result = await api('/provision', {
985
1473
  method: 'POST',
986
1474
  body: { env: config.env },
987
1475
  });
988
1476
 
1477
+ // Stop polling
1478
+ if (provisionPollInterval) {
1479
+ clearInterval(provisionPollInterval);
1480
+ provisionPollInterval = null;
1481
+ }
1482
+
989
1483
  if (result.success) {
990
- output.textContent += '\\n Provisioning complete!\\n';
1484
+ output.textContent += '\\n Provisioning complete!\\n';
991
1485
  status.textContent = 'Complete';
992
1486
  status.className = 'status-badge status-success';
993
1487
 
994
- setStep(4);
995
- 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');
996
1498
  } else {
997
1499
  throw new Error(result.error);
998
1500
  }
999
1501
  } catch (error) {
1000
- output.textContent += '\\n✗ Error: ' + 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';
1001
1509
  status.textContent = 'Error';
1002
1510
  status.className = 'status-badge status-error';
1003
1511
  btn.disabled = false;
1512
+ resourcePreview.classList.remove('hidden');
1004
1513
  }
1005
1514
  });
1006
1515
 
1516
+ // Continue to Deploy button
1517
+ document.getElementById('btn-goto-deploy').addEventListener('click', () => {
1518
+ setStep(4);
1519
+ showSection('deploy');
1520
+ });
1521
+
1007
1522
  document.getElementById('btn-back-provision').addEventListener('click', () => {
1008
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
+ }
1009
1530
  showSection('provision');
1010
1531
  });
1011
1532
 
@@ -1096,8 +1617,517 @@ export function getHtmlTemplate(sessionToken) {
1096
1617
  showSection('complete');
1097
1618
  }
1098
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
+
1099
2122
  // Initialize
1100
- 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
+ }
1101
2131
  </script>
1102
2132
  </body>
1103
2133
  </html>`;