pinokiod 3.265.0 → 3.271.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/kernel/index.js CHANGED
@@ -803,6 +803,7 @@ class Kernel {
803
803
  async init(options) {
804
804
 
805
805
  let home = this.store.get("home") || process.env.PINOKIO_HOME
806
+ this.homedir = home
806
807
 
807
808
  // reset shells if they exist
808
809
  if (this.shell) {
@@ -820,10 +821,9 @@ class Kernel {
820
821
  this.kv = new KV(this)
821
822
  this.cloudflare = new Cloudflare()
822
823
  this.peer = new Peer(this)
824
+ await this.peer.initialize(this)
823
825
  this.git = new Git(this)
824
826
 
825
- this.homedir = home
826
-
827
827
  // if (home) {
828
828
  // this.homedir = home
829
829
  // } else {
package/kernel/peer.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const dgram = require('dgram');
2
2
  const axios = require('axios');
3
3
  const os = require('os')
4
+ const systeminformation = require('systeminformation')
4
5
  const Environment = require("./environment")
5
6
  class PeerDiscovery {
6
7
  constructor(kernel, port = 41234, message = 'ping', interval = 1000) {
@@ -12,9 +13,8 @@ class PeerDiscovery {
12
13
  this.peers = new Set();
13
14
  this.interface_addresses = []
14
15
  this.host_candidates = []
15
- this.host = this._getLocalIPAddress()
16
+ this.host = null
16
17
  this.default_port = 42000
17
- this.peers.add(this.host)
18
18
  this.router_info_cache = {}
19
19
  // this.start();
20
20
  }
@@ -85,6 +85,13 @@ class PeerDiscovery {
85
85
  this.active = false
86
86
  }
87
87
  }
88
+ // Prepare host/peer state before the rest of the kernel bootstraps
89
+ async initialize(kernel) {
90
+ await this.refreshLocalAddress()
91
+ if (kernel) {
92
+ await this.check(kernel)
93
+ }
94
+ }
88
95
  async start(kernel) {
89
96
  let env = await Environment.get(kernel.homedir, kernel)
90
97
 
@@ -576,23 +583,70 @@ class PeerDiscovery {
576
583
  _isLocalLAN(ip) {
577
584
  return this.isRFC1918(ip)
578
585
  }
579
- _getLocalIPAddress() {
580
- this.interface_addresses = this._collectInterfaceAddresses()
581
- const shareable = this.interface_addresses.filter((entry) => entry.shareable)
582
- this.host_candidates = shareable
583
- const lanCandidate = shareable.find((entry) => entry.scope === 'lan')
584
- if (lanCandidate) {
585
- return lanCandidate.address
586
+ // Refresh LAN/IP selection; keeps peers set in sync with the active address
587
+ async refreshLocalAddress() {
588
+ try {
589
+ const { host, host_candidates, interface_addresses } = await this._getLocalIPAddress()
590
+ this.interface_addresses = interface_addresses
591
+ this.host_candidates = host_candidates
592
+ if (host && this.host !== host) {
593
+ if (this.host) {
594
+ this.peers.delete(this.host)
595
+ }
596
+ this.host = host
597
+ } else if (!this.host) {
598
+ this.host = host
599
+ }
600
+ if (this.host) {
601
+ this.peers.add(this.host)
602
+ }
603
+ return this.host
604
+ } catch (err) {
605
+ console.error('peer refreshLocalAddress error', err)
606
+ if (!this.host) {
607
+ this.host = null
608
+ }
609
+ return this.host
610
+ }
611
+ }
612
+ async _getLocalIPAddress() {
613
+ const interface_addresses = await this._collectInterfaceAddresses()
614
+ const shareable = interface_addresses.filter((entry) => entry.shareable)
615
+ const host_candidates = shareable.map((entry) => ({
616
+ address: entry.address,
617
+ netmask: entry.netmask,
618
+ interface: entry.interface,
619
+ scope: entry.scope,
620
+ shareable: entry.shareable,
621
+ type: entry.type || null,
622
+ operstate: entry.operstate || null,
623
+ virtual: entry.virtual || false,
624
+ default: entry.default || false,
625
+ prefixLength: entry.prefixLength,
626
+ mac: entry.mac || null,
627
+ score: this._scoreCandidate(entry)
628
+ }))
629
+ let selectedHost = null
630
+ let bestScore = -Infinity
631
+ host_candidates.forEach((candidate, index) => {
632
+ const score = typeof candidate.score === 'number' ? candidate.score : -Infinity
633
+ if (score > bestScore) {
634
+ bestScore = score
635
+ selectedHost = candidate.address
636
+ } else if (score === bestScore && selectedHost === null) {
637
+ selectedHost = candidate.address
638
+ }
639
+ })
640
+ if (!selectedHost && shareable.length > 0) {
641
+ selectedHost = shareable[0].address
586
642
  }
587
- const cgnatCandidate = shareable.find((entry) => entry.scope === 'cgnat')
588
- if (cgnatCandidate) {
589
- return cgnatCandidate.address
643
+ if (!selectedHost && interface_addresses.length > 0) {
644
+ selectedHost = interface_addresses[0].address
590
645
  }
591
- const publicCandidate = shareable.find((entry) => entry.scope === 'public')
592
- if (publicCandidate) {
593
- return publicCandidate.address
646
+ if (!selectedHost) {
647
+ selectedHost = '127.0.0.1'
594
648
  }
595
- return shareable.length > 0 ? shareable[0].address : null
649
+ return { host: selectedHost, host_candidates, interface_addresses }
596
650
  }
597
651
  isPrivateOrCGNAT(ip) {
598
652
  return this.isRFC1918(ip) || this.isCGNAT(ip)
@@ -626,7 +680,7 @@ class PeerDiscovery {
626
680
  }
627
681
  return secondOctet >= 16 && secondOctet <= 31
628
682
  }
629
- _collectInterfaceAddresses() {
683
+ _collectInterfaceAddressesSync() {
630
684
  const interfaces = os.networkInterfaces()
631
685
  const results = []
632
686
  const seen = new Set()
@@ -653,12 +707,51 @@ class PeerDiscovery {
653
707
  interface: ifaceName,
654
708
  internal: Boolean(iface.internal),
655
709
  scope: classification.scope,
656
- shareable: classification.shareable
710
+ shareable: classification.shareable,
711
+ mac: typeof iface.mac === 'string' ? iface.mac : null
657
712
  })
658
713
  }
659
714
  }
660
715
  return results
661
716
  }
717
+ async _collectInterfaceAddresses() {
718
+ const baseEntries = this._collectInterfaceAddressesSync()
719
+ let metadata = []
720
+ try {
721
+ metadata = await systeminformation.networkInterfaces()
722
+ } catch (err) {
723
+ metadata = []
724
+ }
725
+ const metadataMap = new Map()
726
+ if (Array.isArray(metadata)) {
727
+ metadata.forEach((entry) => {
728
+ if (entry && typeof entry.iface === 'string') {
729
+ metadataMap.set(this._normalizeInterfaceName(entry.iface), entry)
730
+ }
731
+ })
732
+ }
733
+ return baseEntries.map((entry) => {
734
+ const key = this._normalizeInterfaceName(entry.interface)
735
+ const meta = key ? metadataMap.get(key) : null
736
+ const prefixLength = this._prefixLengthFromNetmask(entry.netmask)
737
+ return {
738
+ ...entry,
739
+ prefixLength,
740
+ type: meta && meta.type ? meta.type : null,
741
+ operstate: meta && meta.operstate ? meta.operstate : null,
742
+ speed: typeof meta?.speed === 'number' ? meta.speed : null,
743
+ virtual: Boolean(meta && meta.virtual),
744
+ default: Boolean(meta && meta.default),
745
+ mac: entry.mac || (meta && meta.mac) || null
746
+ }
747
+ })
748
+ }
749
+ _normalizeInterfaceName(name) {
750
+ if (!name || typeof name !== 'string') {
751
+ return ''
752
+ }
753
+ return name.trim().toLowerCase()
754
+ }
662
755
  classifyAddress(address, isInternal = false) {
663
756
  if (!address || typeof address !== 'string') {
664
757
  return { scope: 'unknown', shareable: false }
@@ -684,6 +777,80 @@ class PeerDiscovery {
684
777
  }
685
778
  return { scope: 'public', shareable: true }
686
779
  }
780
+ _prefixLengthFromNetmask(netmask) {
781
+ if (!netmask || typeof netmask !== 'string') {
782
+ return null
783
+ }
784
+ const octets = this._parseIPv4(netmask)
785
+ if (!octets) {
786
+ return null
787
+ }
788
+ let bits = 0
789
+ for (const octet of octets) {
790
+ bits += this._countBits(octet)
791
+ }
792
+ return bits
793
+ }
794
+ _countBits(value) {
795
+ let count = 0
796
+ let v = value & 255
797
+ while (v) {
798
+ count += v & 1
799
+ v >>= 1
800
+ }
801
+ return count
802
+ }
803
+ // Heuristically rank interface candidates so physical LAN adapters win over VPN/tunnels
804
+ _scoreCandidate(entry) {
805
+ if (!entry || !entry.shareable) {
806
+ return -Infinity
807
+ }
808
+ let score = 0
809
+ switch (entry.scope) {
810
+ case 'lan':
811
+ score += 100
812
+ break
813
+ case 'cgnat':
814
+ score += 60
815
+ break
816
+ case 'public':
817
+ score += 40
818
+ break
819
+ default:
820
+ score -= 50
821
+ break
822
+ }
823
+ if (entry.default) {
824
+ score += 20
825
+ }
826
+ const type = entry.type ? entry.type.toLowerCase() : ''
827
+ if (type === 'wired') {
828
+ score += 25
829
+ } else if (type === 'wireless') {
830
+ score += 18
831
+ } else if (type === 'vpn') {
832
+ score -= 40
833
+ } else if (type === 'cellular') {
834
+ score += 5
835
+ }
836
+ if (entry.virtual) {
837
+ score -= 25
838
+ }
839
+ if (entry.operstate && entry.operstate.toLowerCase() === 'up') {
840
+ score += 5
841
+ } else if (entry.operstate) {
842
+ score -= 10
843
+ }
844
+ if (typeof entry.prefixLength === 'number') {
845
+ if (entry.prefixLength <= 24) {
846
+ score += 5
847
+ }
848
+ if (entry.prefixLength >= 30) {
849
+ score -= 20
850
+ }
851
+ }
852
+ return score
853
+ }
687
854
  _buildExternalHostEntries(externalPort) {
688
855
  if (!externalPort && externalPort !== 0) {
689
856
  return []
@@ -741,7 +908,7 @@ class PeerDiscovery {
741
908
  return entries
742
909
  }
743
910
  _broadcastTargets() {
744
- const addresses = this._collectInterfaceAddresses()
911
+ const addresses = this._collectInterfaceAddressesSync()
745
912
  this.interface_addresses = addresses
746
913
  const targets = new Set()
747
914
  for (const entry of addresses) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.265.0",
3
+ "version": "3.271.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -100,8 +100,6 @@
100
100
  }
101
101
 
102
102
  if (mobileButton) {
103
- mobileButton.addEventListener('mouseover', () => showPopoverForAnchor(mobileButton, urlInput))
104
- mobileButton.addEventListener('mouseout', (event) => handleMouseLeave(mobileButton, event))
105
103
  mobileButton.addEventListener('focus', () => showPopoverForAnchor(mobileButton, urlInput))
106
104
  mobileButton.addEventListener('blur', (event) => handleMouseLeave(mobileButton, event))
107
105
  }
@@ -1796,8 +1796,9 @@ body.dark .btn {
1796
1796
  background: rgba(0,0,0,0.8);
1797
1797
  }
1798
1798
  body.dark .swal2-popup {
1799
- background: var(--dark-btn-bg);
1799
+ background: #090909;
1800
1800
  color: var(--dark-btn-color);
1801
+ border: 5px solid rgba(255,255,255,0.2);
1801
1802
  }
1802
1803
  .swal2-popup {
1803
1804
  border-radius: 0px !important;
@@ -2586,6 +2587,18 @@ aside .qr {
2586
2587
  padding: 15px;
2587
2588
  }
2588
2589
 
2590
+ @media only screen and (max-width: 600px) {
2591
+ aside .tab, aside .tab.submenu {
2592
+ flex-direction: column !important;
2593
+ padding: 10px 0 !important;
2594
+ }
2595
+ aside .caption {
2596
+ width: 50px !important;
2597
+ text-align: center !important;
2598
+ word-wrap: break-word !important;
2599
+ display: block !important;
2600
+ }
2601
+ }
2589
2602
 
2590
2603
  @media only screen and (max-width: 768px) {
2591
2604
  /* Hide QR block on small screens */
@@ -2723,6 +2736,11 @@ body[data-agent='electron'] {
2723
2736
  padding-top: 26px;
2724
2737
  }
2725
2738
  */
2739
+ /*
2740
+ body:not([data-agent='electron']) #inspector {
2741
+ display: none !important;
2742
+ }
2743
+ */
2726
2744
  body.dark #dropdown-portal .dropdown-content {
2727
2745
  background: rgb(34, 34, 34);
2728
2746
  }
@@ -806,7 +806,9 @@ body.dark .urlbar input[type=url] {
806
806
  .mobile-link-button {
807
807
  background: rgba(0,0,0,0.05);
808
808
  padding: 5px;
809
+ /*
809
810
  flex-grow: 1;
811
+ */
810
812
  margin: 5px 0;
811
813
  border-radius: 10px;
812
814
  }
@@ -827,6 +829,9 @@ body.dark .urlbar input[type=url] {
827
829
  .mobile-link-button {
828
830
  display: block;
829
831
  }
832
+ .mobile-flexible {
833
+ flex-grow: 1;
834
+ }
830
835
  }
831
836
  .mobile-link-button {
832
837
  display: none;
@@ -41,7 +41,6 @@ body.plugin-page aside .tab.selected {
41
41
  opacity: 1;
42
42
  }
43
43
  body.plugin-page aside .tab i {
44
- width: 20px;
45
44
  text-align: center;
46
45
  }
47
46
  body.plugin-page .plugin-container {
@@ -52,7 +51,7 @@ body.plugin-page .plugin-container {
52
51
  body.plugin-page .btn-tab {
53
52
  display: flex;
54
53
  align-items: center;
55
- gap: 10px;
54
+ gap: 5px;
56
55
  }
57
56
  body.plugin-page .btn-tab .btn {
58
57
  display: flex;
@@ -339,20 +338,29 @@ body.dark .plugin-option:hover {
339
338
  }
340
339
  }
341
340
  @media (max-width: 600px) {
341
+ /*
342
342
  body.plugin-page main {
343
343
  flex-direction: column;
344
344
  }
345
+ */
345
346
  body.plugin-page aside {
346
347
  width: auto;
347
348
  display: flex;
348
349
  flex-wrap: wrap;
349
- gap: 6px;
350
- padding: 0 20px 20px;
350
+ gap: 0px;
351
+ padding: 0 10px;
352
+ flex-direction: column;
351
353
  }
352
- body.plugin-page aside .tab,
353
354
  body.plugin-page aside .btn-tab {
355
+ flex-direction: column;
356
+ padding: 10px 0;
357
+ }
358
+ body.plugin-page aside .tab {
354
359
  border-left: none;
355
360
  opacity: 1;
361
+ flex-direction: column;
362
+ padding: 10px;
363
+ margin: 0;
356
364
  }
357
365
  body.plugin-page aside .caption {
358
366
  display: none;
@@ -400,7 +408,7 @@ body.dark .plugin-option:hover {
400
408
  }) %>
401
409
  </head>
402
410
  <body class='<%=theme%> plugin-page' data-agent="<%=agent%>">
403
- <header class='grabbable'>
411
+ <header class='navheader grabbable'>
404
412
  <h1>
405
413
  <a class='home' href="/home"><img class='icon' src="/pinokio-black.png" alt="Pinokio"></a>
406
414
  <button class='btn2' id='minimize-header' data-tippy-content="fullscreen" title='fullscreen'>
@@ -427,7 +435,7 @@ body.dark .plugin-option:hover {
427
435
  </h1>
428
436
  </header>
429
437
  <main class='plugin-main'>
430
- <div class='plugin-container'>
438
+ <div class='plugin-container container'>
431
439
  <h1><i class="fa-solid fa-robot"></i>Agents</h1>
432
440
  <% const pluginCategories = pluginMenu.reduce((acc, pluginItem, index) => {
433
441
  const runs = Array.isArray(pluginItem.run) ? pluginItem.run : []
@@ -477,13 +485,6 @@ body.dark .plugin-option:hover {
477
485
  <div class='subtitle'><%=pluginItem.description%></div>
478
486
  <% } %>
479
487
  </div>
480
- <div class='flexible'></div>
481
- <% if (pluginItem.link) { %>
482
- <button type='button' class='plugin-info' data-link="<%=pluginItem.link.replace(/"/g, '&quot;')%>" aria-label="Open info"><i class="fa-solid fa-circle-info"></i></button>
483
- <% } %>
484
- <div class='disclosure-indicator' aria-hidden="true">
485
- <i class="fa-solid fa-chevron-right"></i>
486
- </div>
487
488
  </div>
488
489
  <% }) %>
489
490
  </div>
@@ -3017,10 +3017,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3017
3017
  <%if (type==='browse') { %>
3018
3018
  <a id='devtab' data-mode="refresh" target="<%=dev_link%>" href="<%=dev_link%>" class="btn frame-link selected" data-index="10">
3019
3019
  <div class="tab">
3020
- <% if (config.icon) { %>
3021
- <img src="<%=config.icon%>" onerror="this.src='/pinokio-black.png'"/>
3022
- <% } %>
3023
- <div><%=config.title%></div>
3020
+ <i class="fa-solid fa-bars"></i> Launchers
3024
3021
  </div>
3025
3022
  </a>
3026
3023
 
@@ -481,10 +481,13 @@ body.dark .open-menu, body.dark .browse {
481
481
  }
482
482
  .grid-3 {
483
483
  display: grid;
484
- grid-template-columns: repeat(3, 1fr); /* Three equal columns */
484
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); /* Responsive columns that wrap */
485
485
  gap: 1rem; /* Optional spacing between columns */
486
486
  padding: 10px;
487
487
  }
488
+ .grid-3 > * {
489
+ min-width: 0;
490
+ }
488
491
  .section-header .grid-1, .section-header .grid-2, .section-header .grid-3 {
489
492
  background: none !important;
490
493
  padding: 0 !important;
@@ -546,7 +549,6 @@ aside .selected {
546
549
  */
547
550
  .tab-content {
548
551
  width: 100%;
549
- display: flex;
550
552
  flex-direction: column;
551
553
  flex-wrap: wrap;
552
554
  gap: 15px;
@@ -729,6 +731,7 @@ body.dark .profile td {
729
731
  padding:6px 10px;
730
732
  white-space: pre-wrap;
731
733
  background: rgba(0,0,0,0.04);
734
+ word-break: break-word;
732
735
  }
733
736
  .app-icon {
734
737
  width: 25px;
@@ -824,6 +827,7 @@ document.addEventListener('DOMContentLoaded', function() {
824
827
  <button class='btn2' id='screenshot' data-tippy-content="screen capture"><i class="fa-solid fa-camera"></i></button>
825
828
  <button class='btn2' id='inspector' data-tippy-content="X-ray mode"><i class="fa-solid fa-eye"></i></button>
826
829
  <button class='btn2 mobile-link-button' id='mobile-link-button' data-tippy-content="enter url"><i class="fa-solid fa-link"></i></button>
830
+ <div class='mobile-flexible'></div>
827
831
  <form class='urlbar'>
828
832
  <div class='url-input-container'>
829
833
  <input type='url' placeholder='enter any local url to open in pinokio'>
@@ -338,6 +338,7 @@ iframe {
338
338
  <button class='btn2' id='screenshot' data-tippy-content="screen capture"><i class="fa-solid fa-camera"></i></button>
339
339
  <button class='btn2' id='inspector' data-tippy-content="X-ray mode"><i class="fa-solid fa-eye"></i></button>
340
340
  <button class='btn2 mobile-link-button' id='mobile-link-button' data-tippy-content="enter url"><i class="fa-solid fa-link"></i></button>
341
+ <div class='mobile-flexible'></div>
341
342
  <form class='urlbar'>
342
343
  <div class='url-input-container'>
343
344
  <input type='url' placeholder='enter any local url to open in pinokio' value="<%=src%>">
@@ -437,6 +437,7 @@ body.dark aside .current.selected {
437
437
  <button class='btn2' id='screenshot' data-tippy-content="screen capture"><i class="fa-solid fa-camera"></i></button>
438
438
  <button class='btn2' id='inspector' data-tippy-content="X-ray mode"><i class="fa-solid fa-eye"></i></button>
439
439
  <button class='btn2 mobile-link-button' id='mobile-link-button' data-tippy-content="enter url"><i class="fa-solid fa-link"></i></button>
440
+ <div class='mobile-flexible'></div>
440
441
  <form class='urlbar'>
441
442
  <div class='url-input-container'>
442
443
  <input type='url' placeholder='enter any local url to open in pinokio'>
@@ -378,10 +378,20 @@ body.dark .open-menu, body.dark .browse {
378
378
  }
379
379
  .grid-3 {
380
380
  display: grid;
381
- grid-template-columns: repeat(3, 1fr); /* Three equal columns */
381
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); /* Responsive columns that wrap */
382
382
  gap: 1rem; /* Optional spacing between columns */
383
383
  padding: 10px;
384
384
  }
385
+ .grid-3 .section > h2 {
386
+ display: flex;
387
+ align-items: center;
388
+ gap: 6px;
389
+ margin: 0 0 8px;
390
+ font-size: 12px;
391
+ }
392
+ .grid-3 > * {
393
+ min-width: 0;
394
+ }
385
395
  .section-header .grid-1, .section-header .grid-2, .section-header .grid-3 {
386
396
  background: none !important;
387
397
  padding: 0 !important;
@@ -653,6 +663,7 @@ document.addEventListener('DOMContentLoaded', function() {
653
663
  <button class='btn2' id='screenshot' data-tippy-content="screen capture"><i class="fa-solid fa-camera"></i></button>
654
664
  <button class='btn2' id='inspector' data-tippy-content="X-ray mode"><i class="fa-solid fa-eye"></i></button>
655
665
  <button class='btn2 mobile-link-button' id='mobile-link-button' data-tippy-content="enter url"><i class="fa-solid fa-link"></i></button>
666
+ <div class='mobile-flexible'></div>
656
667
  <form class='urlbar'>
657
668
  <div class='url-input-container'>
658
669
  <input type='url' placeholder='enter any local url to open in pinokio'>
@@ -686,25 +697,6 @@ document.addEventListener('DOMContentLoaded', function() {
686
697
  </form>
687
698
  <div class='running-apps'>
688
699
  <% if (current_host === host) { %>
689
- <div class='section-header'>
690
- <h3>
691
- <div class='blank'></div>
692
- <div class='col'>
693
- <div class='grid-3'>
694
- <div class='section'>
695
- <h2><i class='fas fa-laptop-code'></i> This machine</h2>
696
- <div>accessible from this machine</div>
697
- </div>
698
- <div class='section'>
699
- <h2><i class="fa-solid fa-wifi"></i> Local network</h2><div>accessible from any machine on the local network</div>
700
- </div>
701
- <div class='section'>
702
- <h2><i class="fa-solid fa-podcast"></i> Peer</h2><div>accessible from any pinokio peer on the local network</a></div>
703
- </div>
704
- </div>
705
- </div>
706
- </h3>
707
- </div>
708
700
  <% processes.forEach((item, index) => { %>
709
701
  <div class='index line align-top' data-index="<%=index%>" data-name="<%=item.name%>" data-title="<%=item.name%>" data-description="<%=item.internal_router.join(' ')%> <%=item.port%>">
710
702
  <h3>
@@ -723,6 +715,7 @@ document.addEventListener('DOMContentLoaded', function() {
723
715
  <div class='title'><i class="fa-solid fa-circle"></i><span><%=item.title || item.name%></span></div>
724
716
  <div class='grid-3'>
725
717
  <div class='section'>
718
+ <h2><i class='fas fa-laptop-code'></i> This machine</h2>
726
719
  <%
727
720
  let default_dns_name = ''
728
721
  if (item.internal_router && item.internal_router.length > 0) {
@@ -755,6 +748,7 @@ document.addEventListener('DOMContentLoaded', function() {
755
748
  <% } %>
756
749
  </div>
757
750
  <div class='section'>
751
+ <h2><i class="fa-solid fa-wifi"></i> Local network</h2>
758
752
  <%
759
753
  const directHosts = []
760
754
  if (Array.isArray(item.external_hosts)) {
@@ -801,6 +795,7 @@ document.addEventListener('DOMContentLoaded', function() {
801
795
  <% } %>
802
796
  </div>
803
797
  <div class='section'>
798
+ <h2><i class="fa-solid fa-podcast"></i> Peer</h2>
804
799
  <% item.external_router.forEach((domain) => { %>
805
800
  <a class='net' target="_blank" href="https://<%=domain%>">https://<%=domain%></a>
806
801
  <% }) %>
@@ -822,6 +817,7 @@ document.addEventListener('DOMContentLoaded', function() {
822
817
  <div class='title'><i class="fa-solid fa-circle"></i><span><%=route.name%></span></div>
823
818
  <div class='grid-3'>
824
819
  <div class='section'>
820
+ <h2><i class='fas fa-laptop-code'></i> This machine</h2>
825
821
  <% route.internal_router.forEach((domain) => { %>
826
822
  <% if (domain.endsWith(".localhost")) { %>
827
823
  <a class='net' target="_blank" href="https://<%=domain%>">https://<%=domain%></a>
@@ -831,11 +827,13 @@ document.addEventListener('DOMContentLoaded', function() {
831
827
  <% }) %>
832
828
  </div>
833
829
  <div class='section'>
830
+ <h2><i class="fa-solid fa-wifi"></i> Local network</h2>
834
831
  <% if (route.external_ip) { %>
835
832
  <a class='net' target="_blank" href="http://<%=route.external_ip%>">http://<%=route.external_ip%></a>
836
833
  <% } %>
837
834
  </div>
838
835
  <div class='section'>
836
+ <h2><i class="fa-solid fa-podcast"></i> Peer</h2>
839
837
  <% route.external_router.forEach((domain) => { %>
840
838
  <a class='net' target="_blank" href="https://<%=domain%>">https://<%=domain%></a>
841
839
  <% }) %>
@@ -444,10 +444,13 @@ body.dark .desc {
444
444
  }
445
445
  .grid-3 {
446
446
  display: grid;
447
- grid-template-columns: repeat(3, 1fr); /* Three equal columns */
447
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); /* Responsive columns that wrap */
448
448
  gap: 1rem; /* Optional spacing between columns */
449
449
  padding: 10px;
450
450
  }
451
+ .grid-3 > * {
452
+ min-width: 0;
453
+ }
451
454
  .section-header .grid-1, .section-header .grid-2, .section-header .grid-3 {
452
455
  background: none !important;
453
456
  padding: 0 !important;
@@ -1015,6 +1018,25 @@ body.dark .troubleshoot {
1015
1018
  header .flexible {
1016
1019
  min-width: unset;
1017
1020
  }
1021
+ .config-body {
1022
+ flex-direction: column;
1023
+ gap: 20px;
1024
+ }
1025
+ .config-row {
1026
+ width: 100%;
1027
+ padding-right: 0;
1028
+ }
1029
+ .network-name {
1030
+ flex-direction: column;
1031
+ align-items: stretch;
1032
+ gap: 10px;
1033
+ }
1034
+ .network-name input[type=text] {
1035
+ width: 100%;
1036
+ }
1037
+ .network-name .btn {
1038
+ width: 100%;
1039
+ }
1018
1040
  }
1019
1041
 
1020
1042
  @media only screen and (max-width: 480px) {
@@ -1060,6 +1082,7 @@ document.addEventListener('DOMContentLoaded', function() {
1060
1082
  <button class='btn2' id='screenshot' data-tippy-content="screen capture"><i class="fa-solid fa-camera"></i></button>
1061
1083
  <button class='btn2' id='inspector' data-tippy-content="X-ray mode"><i class="fa-solid fa-eye"></i></button>
1062
1084
  <button class='btn2 mobile-link-button' id='mobile-link-button' data-tippy-content="enter url"><i class="fa-solid fa-link"></i></button>
1085
+ <div class='mobile-flexible'></div>
1063
1086
  <form class='urlbar'>
1064
1087
  <div class='url-input-container'>
1065
1088
  <input type='url' placeholder='enter any local url to open in pinokio'>
@@ -717,6 +717,7 @@ document.addEventListener('DOMContentLoaded', function() {
717
717
  <button class='btn2' id='screenshot' data-tippy-content="screen capture"><i class="fa-solid fa-camera"></i></button>
718
718
  <button class='btn2' id='inspector' data-tippy-content="X-ray mode"><i class="fa-solid fa-eye"></i></button>
719
719
  <button class='btn2 mobile-link-button' id='mobile-link-button' data-tippy-content="enter url"><i class="fa-solid fa-link"></i></button>
720
+ <div class='mobile-flexible'></div>
720
721
  <form class='urlbar'>
721
722
  <div class='url-input-container'>
722
723
  <input type='url' placeholder='enter any local url to open in pinokio'>
@@ -389,6 +389,7 @@ document.addEventListener('DOMContentLoaded', function() {
389
389
  <button class='btn2' id='screenshot' data-tippy-content="screen capture"><i class="fa-solid fa-camera"></i></button>
390
390
  <button class='btn2' id='inspector' data-tippy-content="X-ray mode"><i class="fa-solid fa-eye"></i></button>
391
391
  <button class='btn2 mobile-link-button' id='mobile-link-button' data-tippy-content="enter url"><i class="fa-solid fa-link"></i></button>
392
+ <div class='mobile-flexible'></div>
392
393
  <form class='urlbar'>
393
394
  <div class='url-input-container'>
394
395
  <input type='url' placeholder='enter any local url to open in pinokio'>
@@ -971,6 +971,15 @@ aside .selected {
971
971
  opacity: 1;
972
972
  }
973
973
  @media only screen and (max-width: 600px) {
974
+ .bundle-header {
975
+ display: block;
976
+ }
977
+ .bundle-status {
978
+ margin-top: 10px;
979
+ }
980
+ .bundles-grid {
981
+ grid-template-columns: minmax(0, 1fr);
982
+ }
974
983
  aside {
975
984
  width: unset;
976
985
  flex-shrink: unset;
@@ -1107,6 +1116,7 @@ document.addEventListener('DOMContentLoaded', function() {
1107
1116
  <button class='btn2' id='screenshot' data-tippy-content="screen capture"><i class="fa-solid fa-camera"></i></button>
1108
1117
  <button class='btn2' id='inspector' data-tippy-content="X-ray mode"><i class="fa-solid fa-eye"></i></button>
1109
1118
  <button class='btn2 mobile-link-button' id='mobile-link-button' data-tippy-content="enter url"><i class="fa-solid fa-link"></i></button>
1119
+ <div class='mobile-flexible'></div>
1110
1120
  <form class='urlbar'>
1111
1121
  <div class='url-input-container'>
1112
1122
  <input type='url' placeholder='enter any local url to open in pinokio'>