pinokiod 7.5.44 → 7.5.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/server/index.js +403 -24
  3. package/server/public/style.css +0 -264
  4. package/server/public/urldropdown.css +7 -0
  5. package/server/views/app.ejs +2 -0
  6. package/server/views/app_search_test.ejs +2 -0
  7. package/server/views/connect/x.ejs +2 -0
  8. package/server/views/container.ejs +2 -0
  9. package/server/views/create.ejs +3 -0
  10. package/server/views/editor.ejs +3 -0
  11. package/server/views/env_editor.ejs +2 -0
  12. package/server/views/explore.ejs +2 -0
  13. package/server/views/general_editor.ejs +3 -0
  14. package/server/views/help.ejs +2 -0
  15. package/server/views/index.ejs +539 -2
  16. package/server/views/init/index.ejs +2 -0
  17. package/server/views/keys.ejs +2 -0
  18. package/server/views/mini.ejs +2 -0
  19. package/server/views/net.ejs +2 -0
  20. package/server/views/network.ejs +887 -94
  21. package/server/views/partials/app_navheader.ejs +2 -0
  22. package/server/views/partials/home_action_modal.ejs +22 -0
  23. package/server/views/partials/home_server_popover.ejs +21 -0
  24. package/server/views/partials/home_server_popover_assets.ejs +1262 -0
  25. package/server/views/partials/main_sidebar.ejs +1 -221
  26. package/server/views/pro.ejs +7 -0
  27. package/server/views/prototype/index.ejs +2 -0
  28. package/server/views/review.ejs +2 -0
  29. package/server/views/screenshots.ejs +2 -0
  30. package/server/views/settings.ejs +2 -0
  31. package/server/views/shell.ejs +7 -0
  32. package/server/views/task.ejs +2 -0
  33. package/server/views/terminal.ejs +2 -0
  34. package/server/views/terminals.ejs +2 -0
  35. package/test/launch-settings-ui.test.js +355 -0
  36. package/test/main-sidebar.test.js +26 -16
@@ -1079,14 +1079,14 @@ body.network-page .container {
1079
1079
  margin: 0;
1080
1080
  }
1081
1081
  body.network-page .network-config-section {
1082
- padding-top: 18px;
1082
+ padding-top: 12px;
1083
1083
  border-top: 0;
1084
1084
  }
1085
1085
  body.network-page .network-config-head {
1086
1086
  align-items: center;
1087
- margin-bottom: 16px;
1088
- padding-bottom: 16px;
1089
- border-bottom: 1px solid var(--task-border);
1087
+ margin-bottom: 10px;
1088
+ padding-bottom: 0;
1089
+ border-bottom: 0;
1090
1090
  }
1091
1091
  body.network-page .config {
1092
1092
  margin: 0;
@@ -1098,13 +1098,13 @@ body.network-page .config-header {
1098
1098
  }
1099
1099
  body.network-page .config-body {
1100
1100
  display: grid;
1101
- grid-template-columns: minmax(0, 1fr);
1102
- gap: 16px;
1101
+ grid-template-columns: minmax(0, 420px);
1102
+ gap: 8px;
1103
1103
  padding: 0;
1104
1104
  border: 0;
1105
1105
  border-radius: 0;
1106
1106
  background: transparent;
1107
- align-items: stretch;
1107
+ align-items: center;
1108
1108
  }
1109
1109
  body.network-page .config-row {
1110
1110
  padding: 0;
@@ -1168,18 +1168,23 @@ body.network-page .switcher input:checked + .slider {
1168
1168
  }
1169
1169
  body.network-page #network_name {
1170
1170
  display: grid;
1171
- gap: 8px;
1171
+ grid-template-columns: auto minmax(180px, 320px);
1172
+ align-items: center;
1173
+ gap: 10px;
1174
+ width: fit-content;
1175
+ max-width: 100%;
1172
1176
  }
1173
1177
  body.network-page .network-name {
1174
1178
  display: block;
1175
1179
  }
1176
1180
  body.network-page .network-name input[type=text] {
1177
- width: 100%;
1181
+ width: 320px;
1182
+ max-width: 100%;
1178
1183
  min-width: 0;
1179
- min-height: 38px;
1180
- padding: 7px 12px;
1184
+ min-height: 34px;
1185
+ padding: 6px 11px;
1181
1186
  border: 1px solid var(--task-border-strong);
1182
- border-radius: 9px;
1187
+ border-radius: 8px;
1183
1188
  background: color-mix(in srgb, var(--task-panel) 92%, var(--task-soft));
1184
1189
  color: var(--task-text);
1185
1190
  box-sizing: border-box;
@@ -1191,14 +1196,14 @@ body.network-page #network_name .label {
1191
1196
  margin-bottom: 0;
1192
1197
  }
1193
1198
  body.network-page .advanced {
1194
- margin-top: 14px;
1199
+ margin-top: 8px;
1195
1200
  justify-content: flex-start;
1196
1201
  }
1197
1202
  body.network-page .troubleshoot,
1198
1203
  body.dark.network-page .troubleshoot {
1199
1204
  color: var(--task-muted);
1200
1205
  padding: 0;
1201
- font-size: 13px;
1206
+ font-size: 12px;
1202
1207
  line-height: 1.5;
1203
1208
  }
1204
1209
  body.network-page .troubleshoot .link-label {
@@ -1215,33 +1220,97 @@ body.network-page .loading {
1215
1220
  background: transparent;
1216
1221
  color: var(--task-muted);
1217
1222
  }
1223
+ body.network-page .net-summary {
1224
+ display: flex;
1225
+ align-items: center;
1226
+ justify-content: flex-end;
1227
+ gap: 10px;
1228
+ color: var(--task-muted);
1229
+ font-size: 13px;
1230
+ white-space: nowrap;
1231
+ }
1232
+ body.network-page .net-summary-item + .net-summary-item {
1233
+ padding-left: 10px;
1234
+ border-left: 1px solid var(--task-border);
1235
+ }
1236
+ body.network-page .net-summary strong {
1237
+ color: var(--task-text);
1238
+ }
1239
+ body.network-page .network-routes-section {
1240
+ margin-top: 14px;
1241
+ padding-top: 14px;
1242
+ border-top: 1px solid var(--task-border);
1243
+ }
1244
+ body.network-page .net-section-head {
1245
+ display: flex;
1246
+ align-items: center;
1247
+ justify-content: space-between;
1248
+ gap: 16px;
1249
+ margin-bottom: 10px;
1250
+ }
1251
+ body.network-page .net-toolbar {
1252
+ flex: 0 0 auto;
1253
+ padding: 0;
1254
+ }
1255
+ body.network-page .net-search-field {
1256
+ width: 260px;
1257
+ min-height: 34px;
1258
+ padding: 0 10px;
1259
+ gap: 8px;
1260
+ border-radius: 8px;
1261
+ background: color-mix(in srgb, var(--task-panel) 94%, var(--task-soft));
1262
+ border: 1px solid var(--task-border-strong);
1263
+ box-shadow: none;
1264
+ }
1265
+ body.network-page .net-search-field .task-library-search-input {
1266
+ min-height: 0;
1267
+ height: 32px;
1268
+ padding: 0;
1269
+ border: 0 !important;
1270
+ border-radius: 0;
1271
+ background: transparent !important;
1272
+ box-shadow: none;
1273
+ color: var(--task-text);
1274
+ }
1275
+ body.network-page .net-search-field .task-library-search-input:focus {
1276
+ outline: 0;
1277
+ box-shadow: none;
1278
+ }
1218
1279
  body.network-page .running-apps {
1219
1280
  display: grid;
1220
- gap: 14px;
1281
+ gap: 0;
1221
1282
  }
1222
1283
  body.network-page .line {
1223
- padding: 18px 20px;
1224
- border: 1px solid var(--task-border);
1225
- border-radius: 18px;
1226
- background: color-mix(in srgb, var(--task-panel) 94%, var(--task-soft));
1284
+ box-sizing: border-box;
1285
+ padding: 11px 14px;
1286
+ border-top: 1px solid var(--task-border);
1287
+ border-radius: 6px;
1288
+ background: transparent;
1289
+ transition: background-color 120ms ease-out;
1290
+ }
1291
+ body.network-page .line:hover,
1292
+ body.network-page .line.selected {
1293
+ background: color-mix(in srgb, var(--task-soft) 34%, transparent);
1227
1294
  }
1228
1295
  body.network-page .line.align-top h3 {
1229
1296
  margin: 0;
1230
1297
  display: grid;
1231
- grid-template-columns: 48px minmax(0, 1fr);
1232
- gap: 16px;
1298
+ grid-template-columns: 24px minmax(0, 1fr);
1299
+ gap: 9px;
1233
1300
  align-items: start;
1301
+ font-size: inherit;
1234
1302
  }
1235
1303
  body.network-page .placeholder-icon,
1236
1304
  body.network-page .line img {
1237
- width: 48px;
1238
- height: 48px;
1239
- border-radius: 14px;
1240
- background: color-mix(in srgb, var(--task-accent) 12%, transparent);
1305
+ width: 22px;
1306
+ height: 22px;
1307
+ border-radius: 5px;
1308
+ background: transparent;
1241
1309
  display: flex;
1242
1310
  align-items: center;
1243
1311
  justify-content: center;
1244
- color: var(--task-accent);
1312
+ color: var(--task-muted);
1313
+ font-size: 11px;
1245
1314
  }
1246
1315
  body.network-page .line img {
1247
1316
  object-fit: cover;
@@ -1249,60 +1318,274 @@ body.network-page .line img {
1249
1318
  }
1250
1319
  body.network-page .line .col {
1251
1320
  min-width: 0;
1321
+ padding-left: 0;
1322
+ display: grid;
1323
+ grid-template-columns: minmax(160px, 240px) minmax(0, 1fr);
1324
+ gap: 4px 18px;
1325
+ align-items: start;
1252
1326
  }
1253
1327
  body.network-page .line .title {
1254
1328
  display: flex;
1255
1329
  align-items: center;
1256
1330
  gap: 8px;
1257
1331
  color: var(--task-text);
1258
- font-size: 18px;
1332
+ font-size: 14px;
1259
1333
  font-weight: 700;
1334
+ line-height: 1.25;
1335
+ margin: 0;
1336
+ min-width: 0;
1260
1337
  }
1261
1338
  body.network-page .line .title i {
1262
- font-size: 10px;
1339
+ flex: 0 0 auto;
1340
+ font-size: 8px;
1263
1341
  color: color-mix(in srgb, #84cc16 75%, var(--task-panel));
1264
1342
  }
1343
+ body.network-page .line .title span {
1344
+ min-width: 0;
1345
+ overflow-wrap: anywhere;
1346
+ }
1347
+ body.network-page .line .description {
1348
+ grid-column: 1;
1349
+ margin: 2px 0 0;
1350
+ padding: 0;
1351
+ color: var(--task-muted);
1352
+ font-size: 12px;
1353
+ line-height: 1.45;
1354
+ }
1355
+ body.network-page .line .grid-3,
1356
+ body.network-page .line .grid-1,
1357
+ body.network-page .line .launch-btns {
1358
+ grid-column: 2;
1359
+ grid-row: 1 / span 3;
1360
+ }
1265
1361
  body.network-page .grid-3 {
1266
- padding: 10px 0 0;
1362
+ display: grid;
1363
+ grid-template-columns: minmax(0, 1.15fr) repeat(2, minmax(0, 1fr));
1364
+ padding: 4px 0 0;
1267
1365
  gap: 14px;
1366
+ background: none !important;
1367
+ margin-top: 0;
1268
1368
  }
1269
- body.network-page .grid-3 .section {
1270
- border: 1px solid var(--task-border);
1271
- border-radius: 14px;
1272
- padding: 14px;
1273
- background: color-mix(in srgb, var(--task-panel) 96%, var(--task-soft));
1369
+ body.network-page .grid-1 {
1370
+ padding: 0;
1371
+ background: none !important;
1372
+ margin-top: 0;
1274
1373
  }
1275
- body.network-page .grid-3 .section h2 {
1276
- margin: 0 0 10px;
1277
- font-size: 13px;
1374
+ body.network-page .line .grid-3 .section,
1375
+ body.network-page .line .grid-1 .section {
1376
+ border: 0;
1377
+ border-radius: 0;
1378
+ padding: 0;
1379
+ background: none;
1380
+ min-height: 0;
1381
+ }
1382
+ body.network-page .line .grid-3 .section:first-child,
1383
+ body.network-page .line .grid-1 .section:first-child {
1384
+ border-left: 0;
1385
+ padding-left: 0;
1386
+ }
1387
+ body.network-page .line .grid-3 .section:last-child,
1388
+ body.network-page .line .grid-1 .section:last-child {
1389
+ padding-right: 0;
1390
+ }
1391
+ body.network-page .grid-3 .section h2,
1392
+ body.network-page .grid-1 .section h2 {
1393
+ display: flex;
1394
+ align-items: center;
1395
+ gap: 6px;
1396
+ margin: 0 0 5px;
1397
+ font-size: 11px;
1278
1398
  font-weight: 700;
1279
1399
  color: var(--task-text);
1400
+ line-height: 1.2;
1401
+ letter-spacing: 0;
1402
+ }
1403
+ body.network-page .grid-3 .section h2 i,
1404
+ body.network-page .grid-1 .section h2 i {
1405
+ width: 14px;
1406
+ color: var(--task-muted);
1407
+ text-align: center;
1280
1408
  }
1281
1409
  body.network-page .net {
1282
- margin: 0 0 6px;
1410
+ display: flex;
1411
+ align-items: center;
1412
+ gap: 6px;
1413
+ max-width: 100%;
1414
+ width: fit-content;
1415
+ margin: 0 0 4px;
1283
1416
  padding: 0;
1284
1417
  border: 0;
1285
1418
  color: var(--task-muted);
1419
+ line-height: 1.45;
1420
+ overflow-wrap: anywhere;
1286
1421
  }
1287
1422
  body.dark.network-page .net {
1288
1423
  color: var(--task-muted);
1289
1424
  }
1425
+ body.network-page .net:hover,
1426
+ body.dark.network-page .net:hover {
1427
+ color: var(--task-text);
1428
+ }
1429
+ body.network-page .access-point-list {
1430
+ list-style: none;
1431
+ padding: 0;
1432
+ margin: 0;
1433
+ display: grid;
1434
+ gap: 4px;
1435
+ }
1436
+ body.network-page .access-point-list li {
1437
+ align-items: center;
1438
+ gap: 8px;
1439
+ min-width: 0;
1440
+ }
1441
+ body.network-page .access-point-list .net {
1442
+ margin: 0;
1443
+ display: inline-flex;
1444
+ }
1290
1445
  body.network-page .get-dns-btn,
1291
1446
  body.network-page .btn,
1292
1447
  body.network-page .proxy a {
1293
- border-radius: 12px;
1448
+ border-radius: 6px;
1294
1449
  }
1295
1450
  body.network-page .get-dns-btn {
1296
- margin-top: 10px;
1297
- max-width: none;
1298
- background: color-mix(in srgb, var(--task-panel) 94%, var(--task-soft));
1451
+ margin-top: 6px;
1452
+ max-width: fit-content;
1453
+ background: transparent;
1299
1454
  border: 1px solid var(--task-border);
1300
1455
  color: var(--task-text);
1456
+ padding: 0 10px;
1457
+ min-height: 30px;
1458
+ font-weight: 650;
1459
+ width: auto;
1301
1460
  }
1302
1461
  body.network-page .badge,
1303
1462
  body.dark.network-page .badge {
1304
- background: color-mix(in srgb, var(--task-accent) 18%, transparent);
1305
- color: var(--task-accent);
1463
+ min-height: 18px;
1464
+ padding: 0 6px;
1465
+ border: 1px solid color-mix(in srgb, var(--task-accent) 22%, var(--task-border));
1466
+ border-radius: 999px;
1467
+ background: color-mix(in srgb, var(--task-accent) 10%, var(--task-panel));
1468
+ color: color-mix(in srgb, var(--task-accent) 62%, var(--task-text));
1469
+ display: inline-flex;
1470
+ align-items: center;
1471
+ justify-content: center;
1472
+ font-size: 10px;
1473
+ font-weight: 650;
1474
+ line-height: 1;
1475
+ transform: translateY(-1px);
1476
+ }
1477
+ body.network-page .dns-modal-popup,
1478
+ body.network-page .dns-progress-popup {
1479
+ width: min(560px, calc(100vw - 32px)) !important;
1480
+ padding: 18px !important;
1481
+ border-radius: 8px !important;
1482
+ border: 1px solid var(--task-border) !important;
1483
+ background: var(--task-panel) !important;
1484
+ color: var(--task-text) !important;
1485
+ box-shadow: 0 18px 50px rgba(15, 23, 42, 0.16) !important;
1486
+ }
1487
+ body.dark.network-page .dns-modal-popup,
1488
+ body.dark.network-page .dns-progress-popup {
1489
+ background: #1b1c1d !important;
1490
+ box-shadow: 0 24px 72px rgba(0, 0, 0, 0.42) !important;
1491
+ }
1492
+ body.network-page .dns-modal-popup .swal2-title,
1493
+ body.network-page .dns-progress-popup .swal2-title {
1494
+ margin: 0 0 12px !important;
1495
+ font-size: 18px !important;
1496
+ line-height: 1.25 !important;
1497
+ font-weight: 600 !important;
1498
+ letter-spacing: 0 !important;
1499
+ color: var(--task-text) !important;
1500
+ text-align: left !important;
1501
+ }
1502
+ body.network-page .dns-modal-popup .swal2-html-container,
1503
+ body.network-page .dns-progress-popup .swal2-html-container {
1504
+ margin: 0 !important;
1505
+ padding: 0 !important;
1506
+ color: var(--task-text) !important;
1507
+ text-align: left !important;
1508
+ }
1509
+ body.network-page .dns-modal-popup .swal2-actions,
1510
+ body.network-page .dns-progress-popup .swal2-actions {
1511
+ gap: 8px !important;
1512
+ justify-content: flex-end !important;
1513
+ margin-top: 16px !important;
1514
+ }
1515
+ body.network-page .dns-modal-popup .swal2-confirm.swal2-styled,
1516
+ body.network-page .dns-progress-popup .swal2-confirm.swal2-styled,
1517
+ body.network-page .dns-modal-popup .swal2-cancel.swal2-styled,
1518
+ body.network-page .dns-progress-popup .swal2-cancel.swal2-styled {
1519
+ min-height: 32px !important;
1520
+ padding: 0 12px !important;
1521
+ border-radius: 6px !important;
1522
+ box-shadow: none !important;
1523
+ font-size: 13px !important;
1524
+ font-weight: 600 !important;
1525
+ }
1526
+ body.network-page .dns-modal-popup .swal2-confirm.swal2-styled,
1527
+ body.network-page .dns-progress-popup .swal2-confirm.swal2-styled {
1528
+ background: color-mix(in srgb, var(--task-panel) 78%, var(--task-accent)) !important;
1529
+ border: 1px solid color-mix(in srgb, var(--task-panel) 72%, var(--task-accent)) !important;
1530
+ color: var(--task-accent) !important;
1531
+ }
1532
+ body.network-page .dns-modal-popup .swal2-cancel.swal2-styled,
1533
+ body.network-page .dns-progress-popup .swal2-cancel.swal2-styled {
1534
+ background: color-mix(in srgb, var(--task-panel) 96%, var(--task-soft)) !important;
1535
+ border: 1px solid var(--task-border) !important;
1536
+ color: var(--task-text) !important;
1537
+ }
1538
+ body.network-page .dns-modal .address-section {
1539
+ display: grid;
1540
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1541
+ gap: 10px;
1542
+ margin-bottom: 12px;
1543
+ }
1544
+ body.network-page .dns-modal .address-section input {
1545
+ min-width: 0;
1546
+ height: 38px;
1547
+ padding: 0 12px;
1548
+ border-radius: 6px;
1549
+ border: 1px solid var(--task-border-strong);
1550
+ background: color-mix(in srgb, var(--task-panel) 98%, white);
1551
+ color: var(--task-text);
1552
+ font-size: 13px;
1553
+ box-sizing: border-box;
1554
+ }
1555
+ body.dark.network-page .dns-modal .address-section input {
1556
+ background: color-mix(in srgb, var(--task-panel) 95%, black);
1557
+ }
1558
+ body.network-page .dns-modal .address-result,
1559
+ body.network-page .dns-modal-terminal-container {
1560
+ border: 1px solid var(--task-border);
1561
+ border-radius: 8px;
1562
+ background: color-mix(in srgb, var(--task-panel) 96%, var(--task-soft));
1563
+ }
1564
+ body.network-page .dns-modal .address-result {
1565
+ min-height: 52px;
1566
+ padding: 10px 12px;
1567
+ display: flex;
1568
+ align-items: center;
1569
+ justify-content: space-between;
1570
+ gap: 10px;
1571
+ font-size: 13px;
1572
+ font-weight: 600;
1573
+ color: var(--task-text);
1574
+ }
1575
+ body.network-page .dns-modal .address-result i {
1576
+ color: var(--task-muted);
1577
+ flex: 0 0 auto;
1578
+ }
1579
+ body.network-page .dns-progress-modal {
1580
+ margin-top: 0;
1581
+ }
1582
+ body.network-page .dns-modal-terminal-container {
1583
+ width: 100%;
1584
+ height: 300px;
1585
+ overflow: hidden;
1586
+ }
1587
+ body.network-page .dns-modal-terminal-container .xterm .xterm-viewport {
1588
+ width: initial !important;
1306
1589
  }
1307
1590
  @media only screen and (max-width: 820px) {
1308
1591
  body.network-page .task-shell-header,
@@ -1310,6 +1593,25 @@ body.dark.network-page .badge {
1310
1593
  padding-left: 18px;
1311
1594
  padding-right: 18px;
1312
1595
  }
1596
+ body.network-page .net-summary {
1597
+ justify-content: flex-start;
1598
+ }
1599
+ body.network-page .net-section-head {
1600
+ align-items: flex-start;
1601
+ }
1602
+ body.network-page .line .col {
1603
+ grid-template-columns: 1fr;
1604
+ }
1605
+ body.network-page .line .grid-3,
1606
+ body.network-page .line .grid-1,
1607
+ body.network-page .line .launch-btns {
1608
+ grid-column: 1;
1609
+ grid-row: auto;
1610
+ }
1611
+ body.network-page .grid-3 {
1612
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1613
+ padding-top: 6px;
1614
+ }
1313
1615
  body.network-page .config-save-btn {
1314
1616
  width: auto;
1315
1617
  }
@@ -1324,11 +1626,33 @@ body.dark.network-page .badge {
1324
1626
  body.network-page .network-config-head {
1325
1627
  align-items: flex-start;
1326
1628
  }
1629
+ body.network-page .config-body,
1630
+ body.network-page #network_name {
1631
+ width: 100%;
1632
+ }
1633
+ body.network-page #network_name {
1634
+ grid-template-columns: 1fr;
1635
+ gap: 6px;
1636
+ }
1637
+ body.network-page .network-name input[type=text] {
1638
+ width: 100%;
1639
+ }
1327
1640
  body.network-page .config-toggle-actions {
1328
1641
  gap: 10px;
1329
1642
  }
1643
+ body.network-page .net-section-head {
1644
+ flex-direction: column;
1645
+ gap: 10px;
1646
+ }
1647
+ body.network-page .net-toolbar,
1648
+ body.network-page .net-search-field {
1649
+ width: 100%;
1650
+ }
1330
1651
  body.network-page .line.align-top h3 {
1331
- grid-template-columns: 1fr;
1652
+ grid-template-columns: 28px minmax(0, 1fr);
1653
+ }
1654
+ body.network-page .line .title {
1655
+ margin-bottom: 2px;
1332
1656
  }
1333
1657
  body.network-page .item-icon {
1334
1658
  margin-bottom: 2px;
@@ -1336,6 +1660,18 @@ body.dark.network-page .badge {
1336
1660
  body.network-page .grid-3 {
1337
1661
  grid-template-columns: 1fr;
1338
1662
  }
1663
+ body.network-page .line .grid-3 .section,
1664
+ body.network-page .line .grid-1 .section {
1665
+ border-left: 0;
1666
+ padding: 0;
1667
+ }
1668
+ body.network-page .dns-modal .address-section {
1669
+ grid-template-columns: 1fr;
1670
+ }
1671
+ body.network-page .dns-modal .address-result {
1672
+ align-items: flex-start;
1673
+ flex-direction: column;
1674
+ }
1339
1675
  body.network-page .network-name {
1340
1676
  display: block;
1341
1677
  }
@@ -1349,6 +1685,13 @@ body.dark.network-page .badge {
1349
1685
  <script src="/tippy-bundle.umd.min.js"></script>
1350
1686
  <script src="/hotkeys.min.js"></script>
1351
1687
  <script src="/sweetalert2.js"></script>
1688
+ <script src="/xterm.js"></script>
1689
+ <script src="/xterm-inline-bridge.js"></script>
1690
+ <script src="/xterm-addon-fit.js"></script>
1691
+ <script src="/xterm-addon-web-links.js"></script>
1692
+ <script src="/xterm-theme.js"></script>
1693
+ <script src="/Socket.js"></script>
1694
+ <script src="/install.js"></script>
1352
1695
  <script src="/common.js"></script>
1353
1696
  <script src="/opener.js"></script>
1354
1697
  <script src="/nav.js"></script>
@@ -1385,6 +1728,7 @@ document.addEventListener('DOMContentLoaded', function() {
1385
1728
  <div class='url-dropdown' id='url-dropdown'></div>
1386
1729
  </div>
1387
1730
  </form>
1731
+ <%- include('partials/home_server_popover') %>
1388
1732
  <a class='btn2' href="/columns" data-tippy-content="split into 2 columns">
1389
1733
  <div><i class="fa-solid fa-table-columns"></i></div>
1390
1734
  </a>
@@ -1400,13 +1744,17 @@ document.addEventListener('DOMContentLoaded', function() {
1400
1744
  </button>
1401
1745
  </h1>
1402
1746
  </header>
1747
+ <%- include('partials/home_server_popover_assets') %>
1403
1748
  <main>
1404
1749
  <div class='task-container'>
1405
1750
  <section class='task-shell network-shell'>
1406
1751
  <header class='task-shell-header'>
1407
1752
  <div class='task-shell-header-main'>
1408
- <h1 class='task-title'>Network settings</h1>
1409
- <p class='task-description'>Instantly add HTTPS, CORS, and peer sharing for apps on your local network.</p>
1753
+ <h1 class='task-title'>Home Server</h1>
1754
+ <p class='task-description'>Share this Pinokio and running app Web UIs on your home network.</p>
1755
+ </div>
1756
+ <div class='task-header-actions net-summary'>
1757
+ <%- include('partials/net_summary') %>
1410
1758
  </div>
1411
1759
  </header>
1412
1760
  <div class='task-shell-body network-shell-body'>
@@ -1451,60 +1799,505 @@ document.addEventListener('DOMContentLoaded', function() {
1451
1799
  <i class="fa-solid fa-circle-notch fa-spin"></i> Stand by. Do not close this window..
1452
1800
  </div>
1453
1801
  </section>
1454
- <div class='container'>
1455
- <div class='running-apps'>
1456
- <% if (requirements_pending || peer_active && processes.length === 0) { %>
1457
- <div class='loader'>
1458
- <i class="fa-solid fa-circle-notch fa-spin"></i>
1459
- </div>
1460
- <% } %>
1461
- <!--
1462
- <% static_routes.forEach((route, index) => { %>
1463
- <div class='line align-top' data-index="<%=processes.length+index%>" data-name="<%=route.name%>" data-title="<%=route.name%>" data-description="<%=route.internal_router.join(' ')%>">
1464
- <h3>
1465
- <div class='item-icon'>
1466
- <div class='placeholder-icon'>
1467
- <i class="fa-solid fa-clock-rotate-left"></i>
1468
- </div>
1469
- </div>
1470
- <div class='col'>
1471
- <div class='title'><i class="fa-solid fa-circle"></i><span><%=route.name%></span></div>
1472
- <div class='grid-3'>
1473
- <div class='section'>
1474
- <% route.internal_router.forEach((domain) => { %>
1475
- <% if (domain.endsWith(".localhost")) { %>
1476
- <a class='net' target="_blank" href="https://<%=domain%>">https://<%=domain%></a>
1477
- <% } else { %>
1478
- <a class='net' target="_blank" href="http://<%=domain%>">http://<%=domain%></a>
1479
- <% } %>
1480
- <% }) %>
1481
- </div>
1482
- <div class='section'>
1483
- <% if (route.external_ip) { %>
1484
- <a class='net' target="_blank" href="http://<%=route.external_ip%>">http://<%=route.external_ip%></a>
1485
- <% } %>
1486
- </div>
1487
- <div class='section'>
1488
- <% route.external_router.forEach((domain) => { %>
1489
- <a class='net' target="_blank" href="https://<%=domain%>">https://<%=domain%></a>
1490
- <% }) %>
1491
- </div>
1492
- </div>
1493
- </div>
1494
- </h3>
1495
- </div>
1496
- <% }) %>
1497
- -->
1802
+ <section class='task-section network-routes-section net-routes-section' aria-label='This machine routes'>
1803
+ <div class='task-section-head net-section-head'>
1804
+ <div>
1805
+ <h2 class='task-section-title'>This machine routes</h2>
1806
+ <p class='task-section-subcopy'>Open local, LAN, and peer URLs for apps running here.</p>
1807
+ </div>
1808
+ <form class='search net-toolbar' role='search'>
1809
+ <label class='task-library-search-field net-search-field'>
1810
+ <i class='fa-solid fa-magnifying-glass' aria-hidden='true'></i>
1811
+ <input type='search' class="task-library-search-input flexible" placeholder='Search routes' autocomplete='off'>
1812
+ </label>
1813
+ </form>
1498
1814
  </div>
1499
- </div>
1815
+ <div class='container'>
1816
+ <div
1817
+ class='running-apps net-route-list'
1818
+ data-net-routes-active="<%= peer_active && !requirements_pending ? 'true' : 'false' %>"
1819
+ data-net-routes-endpoint="/net/<%= encodeURIComponent(selected_name) %>/routes"
1820
+ >
1821
+ <% if (requirements_pending) { %>
1822
+ <div class='loader'>
1823
+ <i class="fa-solid fa-circle-notch fa-spin"></i>
1824
+ </div>
1825
+ <% } else if (!peer_active) { %>
1826
+ <div class='task-empty-state'>
1827
+ <p class='task-empty-copy'>Home Server is off. Turn it on above to create home-network routes.</p>
1828
+ </div>
1829
+ <% } else { %>
1830
+ <%- include('partials/net_route_list') %>
1831
+ <% } %>
1832
+ </div>
1833
+ </div>
1834
+ </section>
1500
1835
  </div>
1501
1836
  </div>
1502
1837
  </section>
1503
1838
  </div>
1839
+ <template id='dns-modal-template'>
1840
+ <div class='dns-modal'>
1841
+ <div class='address-section'>
1842
+ <input type='text' placeholder='domain name' id='dns-modal-name'>
1843
+ <input type='number' placeholder='port number' id='dns-modal-port' min='1'>
1844
+ </div>
1845
+ <div class='address-result' id='dns-modal-result'>
1846
+ <div>https://[NAME].localhost</div>
1847
+ <i class='fa-solid fa-arrow-right'></i>
1848
+ <div>http://localhost:[PORT]</div>
1849
+ </div>
1850
+ </div>
1851
+ </template>
1504
1852
  <%- include('partials/main_sidebar', { selected: 'network' }) %>
1505
1853
  </main>
1506
1854
  <script>
1507
1855
  let interval
1856
+ const dnsCwd = "<%- typeof cwd !== 'undefined' ? JSON.stringify(cwd).slice(1, -1) : '' %>"
1857
+ const dnsModalTemplate = document.querySelector('#dns-modal-template')
1858
+ const peerName = "<%= peer && peer.name ? peer.name : '' %>".trim().toLowerCase()
1859
+ const allowDnsCreation = <%= allow_dns_creation ? 'true' : 'false' %>
1860
+ let dnsModalActive = false
1861
+ let dnsSocketInstance = null
1862
+ let dnsResizeObserver = null
1863
+
1864
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
1865
+ const normalizeHost = (value) => {
1866
+ if (!value) return null
1867
+ let str = String(value).trim().toLowerCase()
1868
+ if (!str) return null
1869
+ str = str.replace(/^https?:\/\//, '')
1870
+ const host = str.split('/')[0]
1871
+ return host || null
1872
+ }
1873
+ const waitForDnsPropagation = async (dnsName, { timeout = 20000, interval = 1000 } = {}) => {
1874
+ const base = (dnsName || '').trim().toLowerCase()
1875
+ if (!base) return false
1876
+ const targets = new Set()
1877
+ const pushTarget = (val) => {
1878
+ const host = normalizeHost(val)
1879
+ if (host) targets.add(host)
1880
+ }
1881
+ pushTarget(`${base}.localhost`)
1882
+ if (peerName) {
1883
+ pushTarget(`${base}.${peerName}.localhost`)
1884
+ }
1885
+ const deadline = Date.now() + timeout
1886
+ while (Date.now() < deadline) {
1887
+ try {
1888
+ const info = await fetch('/pinokio/peer', { headers: { 'cache-control': 'no-cache' } }).then((res) => {
1889
+ if (!res.ok) throw new Error('peer info unavailable')
1890
+ return res.json()
1891
+ })
1892
+ const advertisedHosts = new Set()
1893
+ const addHost = (value) => {
1894
+ const host = normalizeHost(value)
1895
+ if (host) advertisedHosts.add(host)
1896
+ }
1897
+ if (info && Array.isArray(info.router_info)) {
1898
+ info.router_info.forEach((proc) => {
1899
+ ;(proc.internal_router || []).forEach(addHost)
1900
+ ;(proc.external_router || []).forEach(addHost)
1901
+ })
1902
+ }
1903
+ if (info && info.router) {
1904
+ Object.values(info.router).forEach((list) => {
1905
+ if (Array.isArray(list)) {
1906
+ list.forEach(addHost)
1907
+ }
1908
+ })
1909
+ }
1910
+ if (Array.from(targets).some((host) => advertisedHosts.has(host))) {
1911
+ return true
1912
+ }
1913
+ } catch (err) {
1914
+ console.warn('router poll failed', err)
1915
+ }
1916
+ await delay(interval)
1917
+ }
1918
+ return false
1919
+ }
1920
+ const renderDnsPreview = (popup, name, port) => {
1921
+ const preview = popup.querySelector('#dns-modal-result')
1922
+ if (!preview) return
1923
+ const safeName = (name && name.length > 0) ? name : '[NAME]'
1924
+ const safePort = (port && port.length > 0) ? port : '[PORT]'
1925
+ preview.innerHTML = ''
1926
+ const left = document.createElement('div')
1927
+ left.textContent = `https://${safeName}.localhost`
1928
+ const icon = document.createElement('i')
1929
+ icon.className = 'fa-solid fa-arrow-right'
1930
+ const right = document.createElement('div')
1931
+ right.textContent = `http://localhost:${safePort}`
1932
+ preview.append(left, icon, right)
1933
+ }
1934
+ const promptForDnsMapping = async ({ port, name }) => {
1935
+ if (!dnsModalTemplate) {
1936
+ alert('DNS modal unavailable')
1937
+ return null
1938
+ }
1939
+ const result = await Swal.fire({
1940
+ title: 'Get .localhost domain',
1941
+ html: dnsModalTemplate.innerHTML,
1942
+ confirmButtonText: 'Create domain',
1943
+ customClass: {
1944
+ popup: 'dns-modal-popup'
1945
+ },
1946
+ didOpen: () => {
1947
+ dnsModalActive = true
1948
+ const popup = Swal.getPopup()
1949
+ const nameInput = popup.querySelector('#dns-modal-name')
1950
+ const portInput = popup.querySelector('#dns-modal-port')
1951
+ if (nameInput) nameInput.value = name || ''
1952
+ if (portInput) portInput.value = port || ''
1953
+ const updatePreview = () => {
1954
+ renderDnsPreview(popup, nameInput ? nameInput.value.trim() : '', portInput ? portInput.value.trim() : '')
1955
+ }
1956
+ nameInput && nameInput.addEventListener('input', updatePreview)
1957
+ portInput && portInput.addEventListener('input', updatePreview)
1958
+ updatePreview()
1959
+ if (nameInput) {
1960
+ setTimeout(() => nameInput.focus(), 50)
1961
+ }
1962
+ },
1963
+ willClose: () => {
1964
+ dnsModalActive = false
1965
+ },
1966
+ preConfirm: () => {
1967
+ const popup = Swal.getPopup()
1968
+ const nameInput = popup.querySelector('#dns-modal-name')
1969
+ const portInput = popup.querySelector('#dns-modal-port')
1970
+ const dnsName = nameInput ? nameInput.value.trim() : ''
1971
+ const dnsPort = portInput ? portInput.value.trim() : ''
1972
+ if (!dnsName) {
1973
+ Swal.showValidationMessage('Enter a domain name')
1974
+ return false
1975
+ }
1976
+ if (!dnsPort) {
1977
+ Swal.showValidationMessage('Enter a port number')
1978
+ return false
1979
+ }
1980
+ if (!/^\d+$/.test(dnsPort)) {
1981
+ Swal.showValidationMessage('Port must be numeric')
1982
+ return false
1983
+ }
1984
+ return { dnsName, dnsPort }
1985
+ }
1986
+ })
1987
+ return result.isConfirmed ? result.value : null
1988
+ }
1989
+ const createDnsTerminal = async (container) => {
1990
+ if (!container) {
1991
+ throw new Error('Terminal container missing')
1992
+ }
1993
+ if (typeof Terminal === 'undefined') {
1994
+ throw new Error('Terminal unavailable')
1995
+ }
1996
+ let config = {
1997
+ scrollback: 9999999,
1998
+ fontSize: 12,
1999
+ fontFamily: 'monospace',
2000
+ theme: document.body.classList.contains('dark') ? xtermTheme.FrontEndDelight : xtermTheme.Tomorrow,
2001
+ }
2002
+ try {
2003
+ const res = await fetch('/xterm_config').then((r) => r.json())
2004
+ if (res && res.config) {
2005
+ config = res.config
2006
+ }
2007
+ } catch (err) {
2008
+ console.warn('Failed to load xterm config', err)
2009
+ }
2010
+ const term = new Terminal(config)
2011
+ const fitAddon = new FitAddon.FitAddon()
2012
+ term.loadAddon(fitAddon)
2013
+ if (typeof WebLinksAddon !== 'undefined' && WebLinksAddon.WebLinksAddon) {
2014
+ <% if (agent === "electron") { %>
2015
+ term.loadAddon(new WebLinksAddon.WebLinksAddon((event, uri) => {
2016
+ window.open(uri, "_blank", "browser")
2017
+ }))
2018
+ <% } else { %>
2019
+ term.loadAddon(new WebLinksAddon.WebLinksAddon())
2020
+ <% } %>
2021
+ }
2022
+ container.innerHTML = ''
2023
+ term.open(container)
2024
+ fitAddon.fit()
2025
+ if (window.PinokioTouch && typeof window.PinokioTouch.bindTerminalFocus === 'function') {
2026
+ window.PinokioTouch.bindTerminalFocus(term, container)
2027
+ }
2028
+ const observer = new ResizeObserver(() => {
2029
+ try {
2030
+ fitAddon.fit()
2031
+ } catch (_) {}
2032
+ })
2033
+ observer.observe(container)
2034
+ return { term, observer }
2035
+ }
2036
+ const showDnsError = (message) => {
2037
+ Swal.close()
2038
+ Swal.fire('Domain error', message, 'error')
2039
+ }
2040
+ const startDnsCreationSocket = (term, dnsName, dnsPort) => {
2041
+ if (dnsSocketInstance && typeof dnsSocketInstance.close === 'function') {
2042
+ try { dnsSocketInstance.close() } catch (_) {}
2043
+ }
2044
+ const socket = new Socket()
2045
+ dnsSocketInstance = socket
2046
+ const write = (text) => {
2047
+ if (text && text !== '\u0007') {
2048
+ term.write(text)
2049
+ }
2050
+ }
2051
+ socket.run({
2052
+ id: 'kernel.proto.create',
2053
+ method: 'kernel.proto.create',
2054
+ cwd: dnsCwd,
2055
+ client: {
2056
+ cols: term.cols,
2057
+ rows: term.rows,
2058
+ },
2059
+ params: {
2060
+ name: dnsName,
2061
+ startType: 'new',
2062
+ projectType: 'dns',
2063
+ dnsPort,
2064
+ },
2065
+ }, (packet) => {
2066
+ if (packet.type === 'stream') {
2067
+ if (packet.data && packet.data.raw) {
2068
+ write(packet.data.raw)
2069
+ } else if (packet.data && packet.data.json) {
2070
+ write(JSON.stringify(packet.data.json).replace(/\n/g, '\r\n'))
2071
+ write('\r\n')
2072
+ } else if (packet.data && packet.data.json2) {
2073
+ write(JSON.stringify(packet.data.json2, null, 2).replace(/\n/g, '\r\n'))
2074
+ write('\r\n')
2075
+ }
2076
+ } else if (packet.type === 'result') {
2077
+ if (packet.data && packet.data.error) {
2078
+ write(`\r\n${packet.data.error}\r\n`)
2079
+ showDnsError(packet.data.error)
2080
+ } else {
2081
+ write('\r\nDomain created. Waiting for router to update...\r\n')
2082
+ const hostname = `${dnsName}.localhost`
2083
+ waitForDnsPropagation(dnsName)
2084
+ .then((ready) => {
2085
+ write(ready ? `Router detected ${hostname}. Reloading...\r\n` : 'Router update took longer than expected. Reloading anyway...\r\n')
2086
+ })
2087
+ .catch((err) => {
2088
+ console.warn('dns propagation wait failed', err)
2089
+ write('Unable to confirm router update. Reloading...\r\n')
2090
+ })
2091
+ .finally(() => {
2092
+ setTimeout(() => {
2093
+ window.location.reload()
2094
+ }, 500)
2095
+ })
2096
+ }
2097
+ } else if (packet.type === 'error') {
2098
+ const message = (packet.data && (packet.data.error || packet.data.message)) || 'Unknown error'
2099
+ write(`\r\n${message}\r\n`)
2100
+ showDnsError(message)
2101
+ }
2102
+ }).then(() => {
2103
+ dnsSocketInstance = null
2104
+ }).catch((err) => {
2105
+ console.error('DNS creation failed', err)
2106
+ showDnsError(err.message || 'Unable to create domain')
2107
+ })
2108
+ }
2109
+ const runDnsCreation = ({ dnsName, dnsPort }) => {
2110
+ Swal.fire({
2111
+ title: 'Creating domain',
2112
+ html: `<div class='dns-progress-modal'><div class='dns-modal-terminal-container'></div></div>`,
2113
+ showConfirmButton: false,
2114
+ allowOutsideClick: false,
2115
+ allowEscapeKey: false,
2116
+ customClass: {
2117
+ popup: 'dns-progress-popup'
2118
+ },
2119
+ didOpen: async () => {
2120
+ dnsModalActive = true
2121
+ try {
2122
+ const popup = Swal.getPopup()
2123
+ const container = popup.querySelector('.dns-modal-terminal-container')
2124
+ const { term, observer } = await createDnsTerminal(container)
2125
+ dnsResizeObserver = observer
2126
+ startDnsCreationSocket(term, dnsName, dnsPort)
2127
+ } catch (err) {
2128
+ console.error('Failed to initialize DNS terminal', err)
2129
+ showDnsError(err.message || 'Unable to start terminal')
2130
+ }
2131
+ },
2132
+ willClose: () => {
2133
+ if (dnsSocketInstance && typeof dnsSocketInstance.close === 'function') {
2134
+ try { dnsSocketInstance.close() } catch (_) {}
2135
+ dnsSocketInstance = null
2136
+ }
2137
+ if (dnsResizeObserver) {
2138
+ try { dnsResizeObserver.disconnect() } catch (_) {}
2139
+ dnsResizeObserver = null
2140
+ }
2141
+ dnsModalActive = false
2142
+ }
2143
+ })
2144
+ }
2145
+ document.addEventListener('click', async (event) => {
2146
+ if (!allowDnsCreation) return
2147
+ const trigger = event.target.closest('.get-dns-btn')
2148
+ if (!trigger) return
2149
+ event.preventDefault()
2150
+ const response = await promptForDnsMapping({
2151
+ port: trigger.getAttribute('data-port') || '',
2152
+ name: trigger.getAttribute('data-default-dns') || '',
2153
+ })
2154
+ if (response) {
2155
+ runDnsCreation(response)
2156
+ }
2157
+ })
2158
+
2159
+ let routeSearchList = []
2160
+ const routeSearchInput = () => document.querySelector(".net-toolbar input[type=search]")
2161
+ const rebuildNetRouteSearchIndex = () => {
2162
+ routeSearchList = []
2163
+ document.querySelectorAll(".line[data-index]").forEach((el) => {
2164
+ const titleEl = el.querySelector(".title span, .title")
2165
+ const routeEls = [...el.querySelectorAll(".net")]
2166
+ const routes = routeEls.map((node) => (node.textContent || '').trim()).filter(Boolean)
2167
+ const haystack = [
2168
+ el.getAttribute("data-name") || '',
2169
+ titleEl ? titleEl.textContent || '' : '',
2170
+ el.getAttribute("data-description") || '',
2171
+ ...routes
2172
+ ].join(' ').toLowerCase()
2173
+ routeSearchList.push({
2174
+ index: parseInt(el.getAttribute("data-index")),
2175
+ haystack
2176
+ })
2177
+ })
2178
+ filteredCount = routeSearchList.length
2179
+ filteredBtnsCount = 0
2180
+ }
2181
+ const normalizeSearchValue = (value) => {
2182
+ return String(value || '').toLowerCase().trim().replace(/\s+/g, ' ')
2183
+ }
2184
+ const searchRoutes = (items, value) => {
2185
+ const query = normalizeSearchValue(value)
2186
+ if (!query) {
2187
+ return items.map((item) => ({ item }))
2188
+ }
2189
+ const terms = query.split(' ').filter(Boolean)
2190
+ return items
2191
+ .filter((item) => {
2192
+ const haystack = normalizeSearchValue(item.haystack)
2193
+ return terms.every((term) => haystack.includes(term))
2194
+ })
2195
+ .map((item) => ({ item }))
2196
+ }
2197
+ let selectedIndex = -1
2198
+ let selectedBtnIndex = 0
2199
+ let filteredCount = 0
2200
+ let filteredBtnsCount = 0
2201
+ const renderSearch = () => {
2202
+ const target = routeSearchInput()
2203
+ if (!target) return
2204
+ const result = target.value.trim().length === 0
2205
+ ? routeSearchList.map((item) => ({ item }))
2206
+ : searchRoutes(routeSearchList, target.value)
2207
+ document.querySelectorAll(".line[data-index]").forEach((el) => {
2208
+ el.classList.add("hidden")
2209
+ el.classList.remove("selected")
2210
+ })
2211
+ filteredCount = result.length
2212
+ result.forEach((selector, index) => {
2213
+ const el = document.querySelector(".line[data-index='" + selector.item.index + "']")
2214
+ if (!el) return
2215
+ el.classList.remove("hidden")
2216
+ const btns = el.querySelectorAll(".btns a")
2217
+ if (index === selectedIndex) {
2218
+ el.classList.add("selected")
2219
+ filteredBtnsCount = btns.length
2220
+ btns.forEach((btn, btnIndex) => {
2221
+ btn.classList.toggle("selected", selectedBtnIndex === btnIndex)
2222
+ })
2223
+ } else {
2224
+ btns.forEach((btn) => btn.classList.remove("selected"))
2225
+ }
2226
+ })
2227
+ }
2228
+ rebuildNetRouteSearchIndex()
2229
+ renderSearch()
2230
+ const netRouteInput = routeSearchInput()
2231
+ if (netRouteInput) {
2232
+ netRouteInput.addEventListener("input", (e) => {
2233
+ selectedIndex = 0
2234
+ selectedBtnIndex = 0
2235
+ renderSearch()
2236
+ const p = e.target.value && e.target.value.trim().length > 0
2237
+ ? location.pathname + "?selected=" + encodeURIComponent(e.target.value)
2238
+ : location.pathname
2239
+ window.history.pushState({ path: p }, '', p)
2240
+ })
2241
+ const params = new URLSearchParams(location.search)
2242
+ const selected = params.get("selected")
2243
+ if (selected) {
2244
+ netRouteInput.value = selected
2245
+ netRouteInput.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }))
2246
+ }
2247
+ }
2248
+ const routeListEl = document.querySelector(".net-route-list")
2249
+ const netRoutesEndpoint = routeListEl ? routeListEl.getAttribute("data-net-routes-endpoint") : ""
2250
+ let netRoutesFingerprint = null
2251
+ let netRoutesInFlight = false
2252
+ const netRoutesActive = () => {
2253
+ return routeListEl && routeListEl.getAttribute("data-net-routes-active") === "true"
2254
+ }
2255
+ const applyNetRouteFragment = (payload) => {
2256
+ const routesEl = document.querySelector(".net-route-list")
2257
+ const summaryEl = document.querySelector(".net-summary")
2258
+ if (!routesEl || !summaryEl) return
2259
+ const searchInput = routeSearchInput()
2260
+ const searchHadFocus = searchInput && document.activeElement === searchInput
2261
+ const scrollX = window.scrollX
2262
+ const scrollY = window.scrollY
2263
+ summaryEl.innerHTML = payload.summary_html || ''
2264
+ routesEl.innerHTML = payload.routes_html || ''
2265
+ selectedIndex = -1
2266
+ selectedBtnIndex = 0
2267
+ rebuildNetRouteSearchIndex()
2268
+ renderSearch()
2269
+ if (searchHadFocus && searchInput) {
2270
+ searchInput.focus()
2271
+ }
2272
+ window.scrollTo(scrollX, scrollY)
2273
+ }
2274
+ const refreshNetRouteFragment = async () => {
2275
+ if (!netRoutesEndpoint || !netRoutesActive() || dnsModalActive || netRoutesInFlight) return
2276
+ netRoutesInFlight = true
2277
+ try {
2278
+ const response = await fetch(netRoutesEndpoint, { cache: "no-store" })
2279
+ const payload = await response.json()
2280
+ if (!payload || !payload.success || !payload.fingerprint) {
2281
+ return
2282
+ }
2283
+ if (netRoutesFingerprint === null) {
2284
+ netRoutesFingerprint = payload.fingerprint
2285
+ return
2286
+ }
2287
+ if (payload.fingerprint !== netRoutesFingerprint) {
2288
+ netRoutesFingerprint = payload.fingerprint
2289
+ applyNetRouteFragment(payload)
2290
+ }
2291
+ } catch (error) {
2292
+ console.warn("net route refresh failed", error)
2293
+ } finally {
2294
+ netRoutesInFlight = false
2295
+ }
2296
+ }
2297
+ if (netRoutesActive()) {
2298
+ refreshNetRouteFragment()
2299
+ setInterval(refreshNetRouteFragment, 2000)
2300
+ }
1508
2301
  document.querySelector("#reset-label").addEventListener("click", async (e) => {
1509
2302
  e.preventDefault()
1510
2303
  e.stopPropagation()
@@ -1538,7 +2331,7 @@ document.querySelector("#reset-label").addEventListener("click", async (e) => {
1538
2331
  clearInterval(i2)
1539
2332
  if (res.peer_name) {
1540
2333
  console.log("> 1")
1541
- location.href = "/net/" + res.peer_name
2334
+ location.href = "/network"
1542
2335
  } else {
1543
2336
  console.log("> 2")
1544
2337
  location.reload()
@@ -1702,7 +2495,7 @@ document.querySelector("main").addEventListener("click", async (e) => {
1702
2495
  clearInterval(i3)
1703
2496
  if (res.peer_name) {
1704
2497
  console.log("> 1")
1705
- location.href = "/net/" + res.peer_name
2498
+ location.href = "/network"
1706
2499
  } else {
1707
2500
  console.log("> 2")
1708
2501
  location.reload()