@cluesmith/codev 2.0.0-rc.64 → 2.0.0-rc.69
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/dashboard/dist/assets/{index-DZuzzh0T.js → index-CG7nUttd.js} +22 -22
- package/dashboard/dist/assets/index-CG7nUttd.js.map +1 -0
- package/dashboard/dist/index.html +1 -1
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +4 -1
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts.map +1 -1
- package/dist/agent-farm/commands/architect.js +4 -6
- package/dist/agent-farm/commands/architect.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +54 -6
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/tower-cloud.d.ts +1 -0
- package/dist/agent-farm/commands/tower-cloud.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower-cloud.js +9 -8
- package/dist/agent-farm/commands/tower-cloud.js.map +1 -1
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +18 -0
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/lib/cloud-config.d.ts +13 -0
- package/dist/agent-farm/lib/cloud-config.d.ts.map +1 -1
- package/dist/agent-farm/lib/cloud-config.js +38 -1
- package/dist/agent-farm/lib/cloud-config.js.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +15 -6
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +166 -138
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/utils/session.d.ts +22 -0
- package/dist/agent-farm/utils/session.d.ts.map +1 -1
- package/dist/agent-farm/utils/session.js +45 -0
- package/dist/agent-farm/utils/session.js.map +1 -1
- package/dist/commands/consult/index.d.ts +10 -2
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +133 -37
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +96 -52
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/porch/index.d.ts +4 -0
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +40 -11
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/state.d.ts +18 -0
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +41 -2
- package/dist/commands/porch/state.js.map +1 -1
- package/package.json +2 -1
- package/skeleton/protocols/bugfix/builder-prompt.md +3 -3
- package/skeleton/protocols/bugfix/prompts/pr.md +8 -4
- package/skeleton/protocols/bugfix/protocol.json +2 -32
- package/skeleton/protocols/experiment/builder-prompt.md +1 -1
- package/skeleton/protocols/maintain/builder-prompt.md +1 -1
- package/skeleton/protocols/spir/builder-prompt.md +1 -1
- package/skeleton/protocols/tick/builder-prompt.md +1 -1
- package/skeleton/protocols/tick/protocol.json +1 -1
- package/skeleton/roles/builder.md +9 -8
- package/templates/tower.html +275 -41
- package/dashboard/dist/assets/index-DZuzzh0T.js.map +0 -1
package/templates/tower.html
CHANGED
|
@@ -422,6 +422,11 @@
|
|
|
422
422
|
gap: 24px;
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
.new-shell-row {
|
|
426
|
+
margin-top: 8px;
|
|
427
|
+
padding-top: 8px;
|
|
428
|
+
}
|
|
429
|
+
|
|
425
430
|
/* Recents section */
|
|
426
431
|
.recents-section {
|
|
427
432
|
margin-top: 32px;
|
|
@@ -637,6 +642,61 @@
|
|
|
637
642
|
}
|
|
638
643
|
}
|
|
639
644
|
|
|
645
|
+
/* Cloud status */
|
|
646
|
+
.cloud-status {
|
|
647
|
+
display: inline-flex;
|
|
648
|
+
align-items: center;
|
|
649
|
+
gap: 8px;
|
|
650
|
+
font-size: 14px;
|
|
651
|
+
color: var(--text-secondary);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.cloud-dot {
|
|
655
|
+
width: 8px;
|
|
656
|
+
height: 8px;
|
|
657
|
+
border-radius: 50%;
|
|
658
|
+
display: inline-block;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.cloud-dot--green { background: var(--status-running); }
|
|
662
|
+
.cloud-dot--yellow { background: #eab308; }
|
|
663
|
+
.cloud-dot--red { background: #ef4444; }
|
|
664
|
+
.cloud-dot--gray { background: var(--text-muted); }
|
|
665
|
+
|
|
666
|
+
.cloud-link {
|
|
667
|
+
color: var(--accent);
|
|
668
|
+
text-decoration: none;
|
|
669
|
+
font-size: 13px;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.cloud-link:hover {
|
|
673
|
+
text-decoration: underline;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.cloud-uptime {
|
|
677
|
+
color: var(--text-muted);
|
|
678
|
+
font-size: 12px;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.cloud-btn {
|
|
682
|
+
padding: 4px 10px;
|
|
683
|
+
border-radius: 4px;
|
|
684
|
+
border: 1px solid var(--border);
|
|
685
|
+
background: var(--bg-tertiary);
|
|
686
|
+
color: var(--text-secondary);
|
|
687
|
+
cursor: pointer;
|
|
688
|
+
font-size: 12px;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.cloud-btn:hover {
|
|
692
|
+
background: #333;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.cloud-btn:disabled {
|
|
696
|
+
opacity: 0.5;
|
|
697
|
+
cursor: default;
|
|
698
|
+
}
|
|
699
|
+
|
|
640
700
|
/* Reduced motion */
|
|
641
701
|
@media (prefers-reduced-motion: reduce) {
|
|
642
702
|
.status-dot.running,
|
|
@@ -672,10 +732,24 @@
|
|
|
672
732
|
justify-content: flex-end;
|
|
673
733
|
}
|
|
674
734
|
|
|
735
|
+
/* 1. Hide Share button on mobile (tunnel is for reaching phone, not FROM phone) */
|
|
736
|
+
#share-btn {
|
|
737
|
+
display: none !important;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/* 7. Reduce section spacing */
|
|
675
741
|
.main {
|
|
676
|
-
padding:
|
|
677
|
-
padding-left: calc(
|
|
678
|
-
padding-right: calc(
|
|
742
|
+
padding: 12px;
|
|
743
|
+
padding-left: calc(12px + env(safe-area-inset-left, 0));
|
|
744
|
+
padding-right: calc(12px + env(safe-area-inset-right, 0));
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.section-header {
|
|
748
|
+
margin-bottom: 8px;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.instances {
|
|
752
|
+
gap: 10px;
|
|
679
753
|
}
|
|
680
754
|
|
|
681
755
|
.btn {
|
|
@@ -689,34 +763,78 @@
|
|
|
689
763
|
padding: 10px 14px;
|
|
690
764
|
}
|
|
691
765
|
|
|
766
|
+
/* 2. Keep project name + status badge + Restart/Stop on one line */
|
|
692
767
|
.instance-header {
|
|
693
|
-
flex-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
padding: 16px;
|
|
768
|
+
flex-wrap: wrap;
|
|
769
|
+
gap: 8px;
|
|
770
|
+
padding: 12px 16px;
|
|
697
771
|
}
|
|
698
772
|
|
|
699
773
|
.instance-actions {
|
|
700
|
-
|
|
701
|
-
justify-content: flex-end;
|
|
774
|
+
margin-left: auto;
|
|
702
775
|
}
|
|
703
776
|
|
|
777
|
+
/* 3. Hide project path row on mobile */
|
|
778
|
+
.instance-path-row {
|
|
779
|
+
display: none;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/* 4. Compact terminal list (Overview, Architect, shells) */
|
|
704
783
|
.port-item {
|
|
705
|
-
flex-direction:
|
|
706
|
-
align-items:
|
|
784
|
+
flex-direction: row;
|
|
785
|
+
align-items: center;
|
|
786
|
+
padding: 8px 12px;
|
|
707
787
|
gap: 8px;
|
|
708
788
|
}
|
|
709
789
|
|
|
710
790
|
.port-actions {
|
|
711
|
-
width:
|
|
791
|
+
width: auto;
|
|
712
792
|
}
|
|
713
793
|
|
|
714
794
|
.port-actions a {
|
|
715
|
-
min-height: 44px;
|
|
716
795
|
display: flex;
|
|
717
796
|
align-items: center;
|
|
718
797
|
justify-content: center;
|
|
719
|
-
|
|
798
|
+
padding: 6px 12px;
|
|
799
|
+
flex: 0;
|
|
800
|
+
/* min-height handled by @media (pointer: coarse) at 44px */
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.instance-body {
|
|
804
|
+
padding: 12px;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/* 5. Compact New Shell row */
|
|
808
|
+
.new-shell-row {
|
|
809
|
+
margin-top: 4px;
|
|
810
|
+
padding-top: 4px;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/* 6. Compact Recent Projects */
|
|
814
|
+
.recent-item {
|
|
815
|
+
flex-direction: row;
|
|
816
|
+
align-items: center;
|
|
817
|
+
padding: 12px 16px;
|
|
818
|
+
gap: 8px;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.recent-path {
|
|
822
|
+
display: none;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
.recent-time {
|
|
826
|
+
font-size: 12px;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/* 7. Reduce section spacing (continued) */
|
|
830
|
+
.recents-section {
|
|
831
|
+
margin-top: 16px;
|
|
832
|
+
padding-top: 16px;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.instance-meta {
|
|
836
|
+
margin-top: 8px;
|
|
837
|
+
padding-top: 8px;
|
|
720
838
|
}
|
|
721
839
|
|
|
722
840
|
.launch-section {
|
|
@@ -736,13 +854,6 @@
|
|
|
736
854
|
min-height: 44px;
|
|
737
855
|
}
|
|
738
856
|
|
|
739
|
-
.recent-item {
|
|
740
|
-
flex-direction: column;
|
|
741
|
-
align-items: flex-start;
|
|
742
|
-
gap: 12px;
|
|
743
|
-
padding: 16px;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
857
|
.dialog-box {
|
|
747
858
|
min-width: auto;
|
|
748
859
|
width: calc(100% - 32px);
|
|
@@ -840,6 +951,7 @@
|
|
|
840
951
|
Agent Farm Control Tower
|
|
841
952
|
</h1>
|
|
842
953
|
<div class="header-actions">
|
|
954
|
+
<span id="cloud-status"></span>
|
|
843
955
|
<button class="btn" onclick="refresh()">Refresh</button>
|
|
844
956
|
</div>
|
|
845
957
|
</header>
|
|
@@ -905,7 +1017,7 @@
|
|
|
905
1017
|
|
|
906
1018
|
// Auth helper: get headers with auth token if available
|
|
907
1019
|
function getAuthHeaders() {
|
|
908
|
-
const key = localStorage.getItem('
|
|
1020
|
+
const key = localStorage.getItem('codev-web-key');
|
|
909
1021
|
return key ? { 'Authorization': `Bearer ${key}` } : {};
|
|
910
1022
|
}
|
|
911
1023
|
|
|
@@ -916,7 +1028,7 @@
|
|
|
916
1028
|
|
|
917
1029
|
// If 401, clear key and redirect to login
|
|
918
1030
|
if (response.status === 401) {
|
|
919
|
-
localStorage.removeItem('
|
|
1031
|
+
localStorage.removeItem('codev-web-key');
|
|
920
1032
|
location.reload();
|
|
921
1033
|
throw new Error('Unauthorized');
|
|
922
1034
|
}
|
|
@@ -925,8 +1037,8 @@
|
|
|
925
1037
|
|
|
926
1038
|
// Logout function
|
|
927
1039
|
function logout() {
|
|
928
|
-
localStorage.removeItem('
|
|
929
|
-
window.location.href = '
|
|
1040
|
+
localStorage.removeItem('codev-web-key');
|
|
1041
|
+
window.location.href = './';
|
|
930
1042
|
}
|
|
931
1043
|
|
|
932
1044
|
// Initialize
|
|
@@ -961,7 +1073,7 @@
|
|
|
961
1073
|
sseController = new AbortController();
|
|
962
1074
|
|
|
963
1075
|
try {
|
|
964
|
-
const response = await fetch('
|
|
1076
|
+
const response = await fetch('./api/events', {
|
|
965
1077
|
headers: getAuthHeaders(),
|
|
966
1078
|
signal: sseController.signal,
|
|
967
1079
|
});
|
|
@@ -1038,13 +1150,18 @@
|
|
|
1038
1150
|
// Refresh data from API
|
|
1039
1151
|
async function refresh() {
|
|
1040
1152
|
try {
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1153
|
+
const [statusResponse, cloudStatus] = await Promise.all([
|
|
1154
|
+
authFetch('./api/status'),
|
|
1155
|
+
fetchCloudStatus(),
|
|
1156
|
+
]);
|
|
1043
1157
|
|
|
1044
|
-
|
|
1158
|
+
if (!statusResponse.ok) throw new Error('Failed to fetch status');
|
|
1159
|
+
|
|
1160
|
+
const data = await statusResponse.json();
|
|
1045
1161
|
runningInstances = (data.instances || []).filter(i => i.running);
|
|
1046
1162
|
recentInstances = (data.instances || []).filter(i => !i.running);
|
|
1047
1163
|
render();
|
|
1164
|
+
renderCloudStatus(cloudStatus);
|
|
1048
1165
|
} catch (err) {
|
|
1049
1166
|
console.error('Refresh error:', err);
|
|
1050
1167
|
showToast('Failed to refresh: ' + err.message, 'error');
|
|
@@ -1111,7 +1228,7 @@
|
|
|
1111
1228
|
<span class="port-type">Overview</span>
|
|
1112
1229
|
</div>
|
|
1113
1230
|
<div class="port-actions">
|
|
1114
|
-
<a href="${escapeHtml(instance.proxyUrl)}" target="_blank">Open</a>
|
|
1231
|
+
<a href="${escapeHtml(relUrl(instance.proxyUrl))}" target="_blank">Open</a>
|
|
1115
1232
|
</div>
|
|
1116
1233
|
</div>
|
|
1117
1234
|
`;
|
|
@@ -1125,7 +1242,7 @@
|
|
|
1125
1242
|
<span class="port-type">${escapeHtml(terminal.label)}</span>
|
|
1126
1243
|
</div>
|
|
1127
1244
|
<div class="port-actions">
|
|
1128
|
-
<a href="${escapeHtml(terminal.url)}&fullscreen=1" target="_blank">Open</a>
|
|
1245
|
+
<a href="${escapeHtml(relUrl(terminal.url))}&fullscreen=1" target="_blank">Open</a>
|
|
1129
1246
|
</div>
|
|
1130
1247
|
</div>
|
|
1131
1248
|
`).join('');
|
|
@@ -1133,7 +1250,7 @@
|
|
|
1133
1250
|
|
|
1134
1251
|
// Add "New Shell" button for this instance
|
|
1135
1252
|
terminalsHtml += `
|
|
1136
|
-
<div class="port-item" style="border-top: 1px dashed var(--border);
|
|
1253
|
+
<div class="port-item new-shell-row" style="border-top: 1px dashed var(--border);">
|
|
1137
1254
|
<div class="port-info">
|
|
1138
1255
|
<span class="port-status" style="background: var(--accent);"></span>
|
|
1139
1256
|
<span class="port-type">New Shell</span>
|
|
@@ -1279,7 +1396,7 @@
|
|
|
1279
1396
|
}
|
|
1280
1397
|
|
|
1281
1398
|
try {
|
|
1282
|
-
const response = await authFetch('
|
|
1399
|
+
const response = await authFetch('./api/browse?path=' + encodeURIComponent(inputPath));
|
|
1283
1400
|
const data = await response.json();
|
|
1284
1401
|
suggestions = data.suggestions || [];
|
|
1285
1402
|
selectedIndex = -1;
|
|
@@ -1334,7 +1451,7 @@
|
|
|
1334
1451
|
// Launch a specific path (from recents)
|
|
1335
1452
|
async function launchPath(projectPath) {
|
|
1336
1453
|
try {
|
|
1337
|
-
const response = await authFetch('
|
|
1454
|
+
const response = await authFetch('./api/launch', {
|
|
1338
1455
|
method: 'POST',
|
|
1339
1456
|
headers: { 'Content-Type': 'application/json' },
|
|
1340
1457
|
body: JSON.stringify({ projectPath })
|
|
@@ -1360,7 +1477,7 @@
|
|
|
1360
1477
|
// Stop an instance by project path
|
|
1361
1478
|
async function stopInstance(projectPath) {
|
|
1362
1479
|
try {
|
|
1363
|
-
const response = await authFetch('
|
|
1480
|
+
const response = await authFetch('./api/stop', {
|
|
1364
1481
|
method: 'POST',
|
|
1365
1482
|
headers: { 'Content-Type': 'application/json' },
|
|
1366
1483
|
body: JSON.stringify({ projectPath })
|
|
@@ -1383,7 +1500,7 @@
|
|
|
1383
1500
|
async function restartInstance(projectPath) {
|
|
1384
1501
|
try {
|
|
1385
1502
|
// First stop
|
|
1386
|
-
await authFetch('
|
|
1503
|
+
await authFetch('./api/stop', {
|
|
1387
1504
|
method: 'POST',
|
|
1388
1505
|
headers: { 'Content-Type': 'application/json' },
|
|
1389
1506
|
body: JSON.stringify({ projectPath })
|
|
@@ -1393,7 +1510,7 @@
|
|
|
1393
1510
|
await new Promise(r => setTimeout(r, 1000));
|
|
1394
1511
|
|
|
1395
1512
|
// Then start
|
|
1396
|
-
const response = await authFetch('
|
|
1513
|
+
const response = await authFetch('./api/launch', {
|
|
1397
1514
|
method: 'POST',
|
|
1398
1515
|
headers: { 'Content-Type': 'application/json' },
|
|
1399
1516
|
body: JSON.stringify({ projectPath })
|
|
@@ -1423,7 +1540,7 @@
|
|
|
1423
1540
|
}
|
|
1424
1541
|
|
|
1425
1542
|
try {
|
|
1426
|
-
const response = await authFetch('
|
|
1543
|
+
const response = await authFetch('./api/launch', {
|
|
1427
1544
|
method: 'POST',
|
|
1428
1545
|
headers: { 'Content-Type': 'application/json' },
|
|
1429
1546
|
body: JSON.stringify({ projectPath })
|
|
@@ -1454,7 +1571,7 @@
|
|
|
1454
1571
|
try {
|
|
1455
1572
|
// Use tower proxy to route to the project's dashboard API
|
|
1456
1573
|
const encodedPath = toBase64URL(projectPath);
|
|
1457
|
-
const response = await authFetch(
|
|
1574
|
+
const response = await authFetch(`./project/${encodedPath}/api/tabs/shell`, {
|
|
1458
1575
|
method: 'POST',
|
|
1459
1576
|
headers: { 'Content-Type': 'application/json' },
|
|
1460
1577
|
});
|
|
@@ -1514,6 +1631,13 @@
|
|
|
1514
1631
|
}
|
|
1515
1632
|
}
|
|
1516
1633
|
|
|
1634
|
+
// Convert absolute paths to relative so links work behind reverse proxies.
|
|
1635
|
+
// E.g. "/project/abc/" → "./project/abc/"
|
|
1636
|
+
function relUrl(path) {
|
|
1637
|
+
if (path && path.startsWith('/')) return '.' + path;
|
|
1638
|
+
return path || '';
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1517
1641
|
// HTML escape
|
|
1518
1642
|
function escapeHtml(str) {
|
|
1519
1643
|
if (!str) return '';
|
|
@@ -1539,7 +1663,7 @@
|
|
|
1539
1663
|
// The React dashboard handles tab selection internally
|
|
1540
1664
|
function getProxyUrl(instance, portType) {
|
|
1541
1665
|
const encodedPath = toBase64URL(instance.projectPath);
|
|
1542
|
-
return
|
|
1666
|
+
return `./project/${encodedPath}/`;
|
|
1543
1667
|
}
|
|
1544
1668
|
|
|
1545
1669
|
// Toast notifications
|
|
@@ -1591,7 +1715,7 @@
|
|
|
1591
1715
|
hideCreateProjectDialog();
|
|
1592
1716
|
|
|
1593
1717
|
try {
|
|
1594
|
-
const response = await authFetch('
|
|
1718
|
+
const response = await authFetch('./api/create', {
|
|
1595
1719
|
method: 'POST',
|
|
1596
1720
|
headers: { 'Content-Type': 'application/json' },
|
|
1597
1721
|
body: JSON.stringify({ parent, name })
|
|
@@ -1624,6 +1748,116 @@
|
|
|
1624
1748
|
}
|
|
1625
1749
|
});
|
|
1626
1750
|
|
|
1751
|
+
// Cloud status
|
|
1752
|
+
let cloudLoading = false;
|
|
1753
|
+
|
|
1754
|
+
async function fetchCloudStatus() {
|
|
1755
|
+
try {
|
|
1756
|
+
const res = await authFetch('./api/tunnel/status');
|
|
1757
|
+
if (res.status === 404) return null;
|
|
1758
|
+
if (!res.ok) return { state: 'error' };
|
|
1759
|
+
return await res.json();
|
|
1760
|
+
} catch {
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function formatUptime(ms) {
|
|
1766
|
+
const s = Math.floor(ms / 1000);
|
|
1767
|
+
if (s < 60) return s + 's';
|
|
1768
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's';
|
|
1769
|
+
const h = Math.floor(s / 3600);
|
|
1770
|
+
const m = Math.floor((s % 3600) / 60);
|
|
1771
|
+
return h + 'h ' + m + 'm';
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
function renderCloudStatus(status) {
|
|
1775
|
+
const el = document.getElementById('cloud-status');
|
|
1776
|
+
if (!status || status.state === 'error') {
|
|
1777
|
+
el.innerHTML = `
|
|
1778
|
+
<span class="cloud-status">
|
|
1779
|
+
<span class="cloud-dot cloud-dot--gray"></span>
|
|
1780
|
+
Cloud: not registered
|
|
1781
|
+
</span>`;
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
if (!status.registered) {
|
|
1786
|
+
el.innerHTML = `
|
|
1787
|
+
<span class="cloud-status">
|
|
1788
|
+
<span class="cloud-dot cloud-dot--gray"></span>
|
|
1789
|
+
Cloud: not registered
|
|
1790
|
+
</span>`;
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
if (status.state === 'auth_failed') {
|
|
1795
|
+
el.innerHTML = `
|
|
1796
|
+
<span class="cloud-status">
|
|
1797
|
+
<span class="cloud-dot cloud-dot--red"></span>
|
|
1798
|
+
Cloud: auth failed
|
|
1799
|
+
</span>`;
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
if (status.state === 'connecting') {
|
|
1804
|
+
el.innerHTML = `
|
|
1805
|
+
<span class="cloud-status">
|
|
1806
|
+
<span class="cloud-dot cloud-dot--yellow"></span>
|
|
1807
|
+
Cloud: connecting...
|
|
1808
|
+
</span>`;
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
if (status.state === 'connected') {
|
|
1813
|
+
const uptimeStr = status.uptime != null ? ' <span class="cloud-uptime">' + formatUptime(status.uptime) + '</span>' : '';
|
|
1814
|
+
const openLink = status.accessUrl ? ' <a href="' + escapeHtml(status.accessUrl) + '" target="_blank" rel="noopener" class="cloud-link">Open</a>' : '';
|
|
1815
|
+
el.innerHTML = `
|
|
1816
|
+
<span class="cloud-status">
|
|
1817
|
+
<span class="cloud-dot cloud-dot--green"></span>
|
|
1818
|
+
Cloud: ${escapeHtml(status.towerName || 'connected')}
|
|
1819
|
+
${uptimeStr}
|
|
1820
|
+
${openLink}
|
|
1821
|
+
<button class="cloud-btn" onclick="cloudDisconnect()" ${cloudLoading ? 'disabled' : ''}>Disconnect</button>
|
|
1822
|
+
</span>`;
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// Disconnected
|
|
1827
|
+
el.innerHTML = `
|
|
1828
|
+
<span class="cloud-status">
|
|
1829
|
+
<span class="cloud-dot cloud-dot--gray"></span>
|
|
1830
|
+
Cloud: disconnected
|
|
1831
|
+
<button class="cloud-btn" onclick="cloudConnect()" ${cloudLoading ? 'disabled' : ''}>Connect</button>
|
|
1832
|
+
</span>`;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
async function cloudConnect() {
|
|
1836
|
+
cloudLoading = true;
|
|
1837
|
+
renderCloudStatus({ registered: true, state: 'connecting' });
|
|
1838
|
+
try {
|
|
1839
|
+
await authFetch('./api/tunnel/connect', { method: 'POST' });
|
|
1840
|
+
showToast('Connecting to cloud...', 'success');
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
showToast('Connect failed: ' + err.message, 'error');
|
|
1843
|
+
}
|
|
1844
|
+
cloudLoading = false;
|
|
1845
|
+
// Refresh will pick up new status
|
|
1846
|
+
setTimeout(refresh, 2000);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
async function cloudDisconnect() {
|
|
1850
|
+
cloudLoading = true;
|
|
1851
|
+
try {
|
|
1852
|
+
await authFetch('./api/tunnel/disconnect', { method: 'POST' });
|
|
1853
|
+
showToast('Disconnected from cloud', 'success');
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
showToast('Disconnect failed: ' + err.message, 'error');
|
|
1856
|
+
}
|
|
1857
|
+
cloudLoading = false;
|
|
1858
|
+
setTimeout(refresh, 1000);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1627
1861
|
// Initialize
|
|
1628
1862
|
init();
|
|
1629
1863
|
</script>
|