pinokiod 3.152.0 → 3.155.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/router/common.js +4 -0
- package/kernel/router/connector.js +8 -0
- package/kernel/router/localhost_home_router.js +4 -0
- package/package.json +1 -1
- package/server/index.js +154 -2
- package/server/public/style.css +2 -1
- package/server/public/urldropdown.css +12 -46
- package/server/public/urldropdown.js +125 -215
package/kernel/router/common.js
CHANGED
|
@@ -12,6 +12,10 @@ class Connector extends Processor {
|
|
|
12
12
|
|
|
13
13
|
let handler = [{
|
|
14
14
|
"handler": "reverse_proxy",
|
|
15
|
+
"transport": {
|
|
16
|
+
"protocol": "http",
|
|
17
|
+
"versions": ["1.1"]
|
|
18
|
+
},
|
|
15
19
|
"upstreams": [{ "dial": dial }],
|
|
16
20
|
"headers": {
|
|
17
21
|
"request": {
|
|
@@ -52,6 +56,10 @@ class Connector extends Processor {
|
|
|
52
56
|
"handle": [
|
|
53
57
|
{
|
|
54
58
|
"handler": "reverse_proxy",
|
|
59
|
+
"transport": {
|
|
60
|
+
"protocol": "http",
|
|
61
|
+
"versions": ["1.1"]
|
|
62
|
+
},
|
|
55
63
|
"upstreams": [{ "dial": listener_pointer }],
|
|
56
64
|
"headers": {
|
|
57
65
|
"request": {
|
|
@@ -51,6 +51,10 @@ class LocalhostHomeRouter {
|
|
|
51
51
|
"handle": [
|
|
52
52
|
{
|
|
53
53
|
"handler": "reverse_proxy",
|
|
54
|
+
"transport": {
|
|
55
|
+
"protocol": "http",
|
|
56
|
+
"versions": ["1.1"]
|
|
57
|
+
},
|
|
54
58
|
"upstreams": [{ "dial": this.router.default_host + ":" + this.router.default_port }],
|
|
55
59
|
"headers": {
|
|
56
60
|
"request": {
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -6742,7 +6742,6 @@ class Server {
|
|
|
6742
6742
|
if (preferHttps) {
|
|
6743
6743
|
httpsHosts = resolveHttpsHosts(item)
|
|
6744
6744
|
}
|
|
6745
|
-
console.log("httpsHosts", httpsHosts)
|
|
6746
6745
|
const httpsUrls = httpsHosts.map((host) => {
|
|
6747
6746
|
if (!host) {
|
|
6748
6747
|
return null
|
|
@@ -6753,7 +6752,6 @@ class Server {
|
|
|
6753
6752
|
}
|
|
6754
6753
|
return `https://${trimmed}`
|
|
6755
6754
|
}).filter(Boolean)
|
|
6756
|
-
console.log("httpsUrls", httpsUrls)
|
|
6757
6755
|
|
|
6758
6756
|
const preferredHttpsUrl = preferFriendlyHost(httpsHosts)
|
|
6759
6757
|
const displayHttpsUrl = preferredHttpsUrl
|
|
@@ -6783,6 +6781,160 @@ class Server {
|
|
|
6783
6781
|
})
|
|
6784
6782
|
|
|
6785
6783
|
this.add_extra_urls(info)
|
|
6784
|
+
|
|
6785
|
+
const toArray = (value) => {
|
|
6786
|
+
if (!value) return []
|
|
6787
|
+
return Array.isArray(value) ? value.filter(Boolean) : [value].filter(Boolean)
|
|
6788
|
+
}
|
|
6789
|
+
|
|
6790
|
+
const uniqueUrls = (urls) => {
|
|
6791
|
+
const seen = new Set()
|
|
6792
|
+
const result = []
|
|
6793
|
+
for (const url of urls) {
|
|
6794
|
+
if (typeof url !== 'string') continue
|
|
6795
|
+
const trimmed = url.trim()
|
|
6796
|
+
if (!trimmed || seen.has(trimmed)) {
|
|
6797
|
+
continue
|
|
6798
|
+
}
|
|
6799
|
+
seen.add(trimmed)
|
|
6800
|
+
result.push(trimmed)
|
|
6801
|
+
}
|
|
6802
|
+
return result
|
|
6803
|
+
}
|
|
6804
|
+
|
|
6805
|
+
const isLoopbackHost = (url) => {
|
|
6806
|
+
try {
|
|
6807
|
+
const hostname = new URL(url).hostname.toLowerCase()
|
|
6808
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
|
|
6809
|
+
} catch (error) {
|
|
6810
|
+
return false
|
|
6811
|
+
}
|
|
6812
|
+
}
|
|
6813
|
+
|
|
6814
|
+
const stripPeerFromHostname = (url, peerName) => {
|
|
6815
|
+
if (!url || !peerName) {
|
|
6816
|
+
return url
|
|
6817
|
+
}
|
|
6818
|
+
try {
|
|
6819
|
+
const parsed = new URL(url)
|
|
6820
|
+
const suffix = `.${peerName}.localhost`
|
|
6821
|
+
if (parsed.hostname.toLowerCase().endsWith(suffix.toLowerCase())) {
|
|
6822
|
+
const prefix = parsed.hostname.slice(0, -suffix.length)
|
|
6823
|
+
if (!prefix) {
|
|
6824
|
+
return url
|
|
6825
|
+
}
|
|
6826
|
+
const sanitizedHostname = `${prefix}.localhost`
|
|
6827
|
+
const portPart = parsed.port ? `:${parsed.port}` : ''
|
|
6828
|
+
return `${parsed.protocol}//${sanitizedHostname}${portPart}${parsed.pathname}${parsed.search}${parsed.hash}`
|
|
6829
|
+
}
|
|
6830
|
+
return url
|
|
6831
|
+
} catch (error) {
|
|
6832
|
+
return url
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
|
|
6836
|
+
const urlContainsPeerName = (url, peerName) => {
|
|
6837
|
+
if (!url || !peerName) {
|
|
6838
|
+
return false
|
|
6839
|
+
}
|
|
6840
|
+
try {
|
|
6841
|
+
const hostname = new URL(url).hostname.toLowerCase()
|
|
6842
|
+
const needle = `.${peerName.toLowerCase()}.`
|
|
6843
|
+
return hostname.includes(needle)
|
|
6844
|
+
} catch (error) {
|
|
6845
|
+
return false
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
|
|
6849
|
+
const selectPrimaryUrl = (entry) => {
|
|
6850
|
+
const hostMeta = entry.host || {}
|
|
6851
|
+
const isLocalHost = hostMeta.local === true
|
|
6852
|
+
const peerName = typeof hostMeta.name === 'string' ? hostMeta.name : ''
|
|
6853
|
+
|
|
6854
|
+
const httpCandidates = toArray(entry.urls && entry.urls.http)
|
|
6855
|
+
const httpsCandidates = toArray(entry.urls && entry.urls.https)
|
|
6856
|
+
|
|
6857
|
+
let sanitizedHttpsCandidates = httpsCandidates.slice()
|
|
6858
|
+
if (preferHttps && isLocalHost) {
|
|
6859
|
+
sanitizedHttpsCandidates = uniqueUrls(sanitizedHttpsCandidates.map((url) => {
|
|
6860
|
+
return stripPeerFromHostname(url, peerName)
|
|
6861
|
+
}))
|
|
6862
|
+
}
|
|
6863
|
+
|
|
6864
|
+
let primaryUrl
|
|
6865
|
+
|
|
6866
|
+
if (preferHttps) {
|
|
6867
|
+
let candidates = sanitizedHttpsCandidates.slice()
|
|
6868
|
+
|
|
6869
|
+
if (!isLocalHost && peerName) {
|
|
6870
|
+
const withPeer = candidates.filter((url) => urlContainsPeerName(url, peerName))
|
|
6871
|
+
if (withPeer.length > 0) {
|
|
6872
|
+
candidates = withPeer
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
|
|
6876
|
+
primaryUrl = candidates[0]
|
|
6877
|
+
|
|
6878
|
+
if (!primaryUrl && httpCandidates.length > 0) {
|
|
6879
|
+
primaryUrl = httpCandidates[0]
|
|
6880
|
+
}
|
|
6881
|
+
} else {
|
|
6882
|
+
let candidates = httpCandidates.slice()
|
|
6883
|
+
|
|
6884
|
+
if (isLocalHost) {
|
|
6885
|
+
const loopbackCandidate = candidates.find((url) => isLoopbackHost(url))
|
|
6886
|
+
if (loopbackCandidate) {
|
|
6887
|
+
candidates = [loopbackCandidate]
|
|
6888
|
+
}
|
|
6889
|
+
} else {
|
|
6890
|
+
const nonLoopback = candidates.filter((url) => !isLoopbackHost(url))
|
|
6891
|
+
if (nonLoopback.length > 0) {
|
|
6892
|
+
candidates = nonLoopback
|
|
6893
|
+
}
|
|
6894
|
+
}
|
|
6895
|
+
|
|
6896
|
+
primaryUrl = candidates[0]
|
|
6897
|
+
|
|
6898
|
+
if (!primaryUrl) {
|
|
6899
|
+
let httpsFallback = sanitizedHttpsCandidates.slice()
|
|
6900
|
+
|
|
6901
|
+
if (isLocalHost) {
|
|
6902
|
+
httpsFallback = uniqueUrls(httpsFallback.map((url) => stripPeerFromHostname(url, peerName)))
|
|
6903
|
+
} else if (peerName) {
|
|
6904
|
+
const withPeer = httpsFallback.filter((url) => urlContainsPeerName(url, peerName))
|
|
6905
|
+
if (withPeer.length > 0) {
|
|
6906
|
+
httpsFallback = withPeer
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
|
|
6910
|
+
primaryUrl = httpsFallback[0]
|
|
6911
|
+
}
|
|
6912
|
+
}
|
|
6913
|
+
|
|
6914
|
+
if (!primaryUrl) {
|
|
6915
|
+
primaryUrl = entry.url || httpCandidates[0] || sanitizedHttpsCandidates[0] || httpsCandidates[0] || null
|
|
6916
|
+
}
|
|
6917
|
+
|
|
6918
|
+
if (primaryUrl) {
|
|
6919
|
+
entry.url = primaryUrl
|
|
6920
|
+
entry.protocol = primaryUrl.startsWith('https://') ? 'https' : 'http'
|
|
6921
|
+
|
|
6922
|
+
entry.urls = {
|
|
6923
|
+
http: primaryUrl.startsWith('http://') ? [primaryUrl] : [],
|
|
6924
|
+
https: primaryUrl.startsWith('https://') ? [primaryUrl] : []
|
|
6925
|
+
}
|
|
6926
|
+
} else {
|
|
6927
|
+
entry.urls = {
|
|
6928
|
+
http: [],
|
|
6929
|
+
https: []
|
|
6930
|
+
}
|
|
6931
|
+
}
|
|
6932
|
+
}
|
|
6933
|
+
|
|
6934
|
+
for (const entry of info) {
|
|
6935
|
+
selectPrimaryUrl(entry)
|
|
6936
|
+
}
|
|
6937
|
+
|
|
6786
6938
|
res.json({
|
|
6787
6939
|
info
|
|
6788
6940
|
})
|
package/server/public/style.css
CHANGED
|
@@ -93,68 +93,34 @@ body.dark .url-dropdown-name {
|
|
|
93
93
|
.url-dropdown-url {
|
|
94
94
|
color: #718096;
|
|
95
95
|
font-size: 12px;
|
|
96
|
-
}
|
|
97
|
-
body.dark .url-dropdown-url {
|
|
98
|
-
color: #a0aec0;
|
|
99
|
-
}
|
|
100
|
-
.url-dropdown-url-list {
|
|
101
96
|
display: flex;
|
|
102
|
-
flex-wrap: wrap;
|
|
103
|
-
gap: 6px;
|
|
104
|
-
}
|
|
105
|
-
.url-chip {
|
|
106
|
-
display: inline-flex;
|
|
107
97
|
align-items: center;
|
|
108
|
-
gap:
|
|
109
|
-
padding: 4px 8px;
|
|
110
|
-
border-radius: 6px;
|
|
111
|
-
border: 1px solid #e2e8f0;
|
|
112
|
-
background: #f8fafc;
|
|
113
|
-
color: #2d3748;
|
|
114
|
-
font-size: 12px;
|
|
115
|
-
font-weight: 500;
|
|
116
|
-
cursor: pointer;
|
|
117
|
-
transition: background 0.15s ease, border-color 0.15s ease;
|
|
118
|
-
}
|
|
119
|
-
.url-chip:hover {
|
|
120
|
-
background: #edf2f7;
|
|
121
|
-
border-color: #cbd5f5;
|
|
122
|
-
}
|
|
123
|
-
body.dark .url-chip {
|
|
124
|
-
border-color: rgba(255,255,255,0.15);
|
|
125
|
-
background: rgba(255,255,255,0.06);
|
|
126
|
-
color: #e2e8f0;
|
|
98
|
+
gap: 8px;
|
|
127
99
|
}
|
|
128
|
-
body.dark .url-
|
|
129
|
-
|
|
130
|
-
border-color: rgba(255,255,255,0.22);
|
|
100
|
+
body.dark .url-dropdown-url {
|
|
101
|
+
color: #a0aec0;
|
|
131
102
|
}
|
|
132
|
-
.url-
|
|
103
|
+
.url-scheme {
|
|
133
104
|
font-size: 10px;
|
|
134
105
|
font-weight: 600;
|
|
135
106
|
letter-spacing: 0.05em;
|
|
136
107
|
text-transform: uppercase;
|
|
137
|
-
padding: 2px
|
|
108
|
+
padding: 2px 6px;
|
|
138
109
|
border-radius: 4px;
|
|
110
|
+
background: rgba(237, 137, 54, 0.18);
|
|
111
|
+
color: #9c4221;
|
|
139
112
|
}
|
|
140
|
-
.url-
|
|
113
|
+
.url-scheme.https {
|
|
141
114
|
background: rgba(72, 187, 120, 0.18);
|
|
142
115
|
color: #276749;
|
|
143
116
|
}
|
|
144
|
-
body.dark .url-
|
|
145
|
-
background: rgba(72, 187, 120, 0.25);
|
|
146
|
-
color: #9ae6b4;
|
|
147
|
-
}
|
|
148
|
-
.url-chip-http .chip-mark {
|
|
149
|
-
background: rgba(237, 137, 54, 0.18);
|
|
150
|
-
color: #9c4221;
|
|
151
|
-
}
|
|
152
|
-
body.dark .url-chip-http .chip-mark {
|
|
117
|
+
body.dark .url-scheme {
|
|
153
118
|
background: rgba(237, 137, 54, 0.25);
|
|
154
119
|
color: #fbd38d;
|
|
155
120
|
}
|
|
156
|
-
.url-
|
|
157
|
-
|
|
121
|
+
body.dark .url-scheme.https {
|
|
122
|
+
background: rgba(72, 187, 120, 0.25);
|
|
123
|
+
color: #9ae6b4;
|
|
158
124
|
}
|
|
159
125
|
.url-dropdown-loading {
|
|
160
126
|
padding: 12px 16px;
|
|
@@ -82,8 +82,9 @@ function initUrlDropdown(config = {}) {
|
|
|
82
82
|
|
|
83
83
|
const getProcessUrls = (process) => {
|
|
84
84
|
const urls = (process && process.urls) || {};
|
|
85
|
-
const
|
|
86
|
-
const
|
|
85
|
+
const httpCandidates = toArray(urls.http);
|
|
86
|
+
const httpsCandidates = toArray(urls.https);
|
|
87
|
+
const normalizedHttps = httpsCandidates
|
|
87
88
|
.map((value) => {
|
|
88
89
|
if (typeof value !== 'string') return null;
|
|
89
90
|
const trimmed = value.trim();
|
|
@@ -94,12 +95,19 @@ function initUrlDropdown(config = {}) {
|
|
|
94
95
|
return `https://${trimmed}`;
|
|
95
96
|
})
|
|
96
97
|
.filter(Boolean);
|
|
97
|
-
|
|
98
|
+
const httpUrl = httpCandidates.length > 0
|
|
99
|
+
? httpCandidates[0]
|
|
100
|
+
: (process && process.ip ? `http://${process.ip}` : null);
|
|
101
|
+
return {
|
|
102
|
+
httpUrl,
|
|
103
|
+
httpUrls: httpCandidates.filter((value) => typeof value === 'string' && value.trim().length > 0),
|
|
104
|
+
httpsUrls: normalizedHttps
|
|
105
|
+
};
|
|
98
106
|
};
|
|
99
107
|
|
|
100
108
|
const getProcessDisplayUrl = (process) => {
|
|
101
109
|
if (process && typeof process.url === 'string' && process.url.trim().length > 0) {
|
|
102
|
-
return process.url;
|
|
110
|
+
return process.url.trim();
|
|
103
111
|
}
|
|
104
112
|
const { httpUrl, httpsUrls } = getProcessUrls(process);
|
|
105
113
|
if (httpsUrls.length > 0) {
|
|
@@ -114,11 +122,9 @@ function initUrlDropdown(config = {}) {
|
|
|
114
122
|
if (display) {
|
|
115
123
|
urls.add(display);
|
|
116
124
|
}
|
|
117
|
-
const {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
httpsUrls.forEach((httpsUrl) => urls.add(httpsUrl));
|
|
125
|
+
const { httpUrls, httpsUrls } = getProcessUrls(process);
|
|
126
|
+
httpUrls.forEach((value) => urls.add(value));
|
|
127
|
+
httpsUrls.forEach((value) => urls.add(value));
|
|
122
128
|
return Array.from(urls);
|
|
123
129
|
};
|
|
124
130
|
|
|
@@ -163,43 +169,6 @@ function initUrlDropdown(config = {}) {
|
|
|
163
169
|
return url.replace(/^https?:\/\//i, '');
|
|
164
170
|
};
|
|
165
171
|
|
|
166
|
-
const createUrlChip = (url, type, scheme) => {
|
|
167
|
-
if (!url) {
|
|
168
|
-
return '';
|
|
169
|
-
}
|
|
170
|
-
const normalizedType = type || 'remote';
|
|
171
|
-
const classSuffix = scheme === 'HTTPS' ? 'https' : 'http';
|
|
172
|
-
const label = formatUrlLabel(url);
|
|
173
|
-
return `
|
|
174
|
-
<button type="button" class="url-chip url-chip-${classSuffix}" data-url="${escapeAttribute(url)}" data-host-type="${escapeAttribute(normalizedType)}">
|
|
175
|
-
<span class="chip-mark">${scheme}</span>
|
|
176
|
-
<span class="chip-label">${escapeHtml(label)}</span>
|
|
177
|
-
</button>
|
|
178
|
-
`;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const buildProcessUrlList = (process) => {
|
|
182
|
-
const { httpUrl, httpsUrls } = getProcessUrls(process);
|
|
183
|
-
const hostType = (process && process.host && process.host.local) ? 'local' : 'remote';
|
|
184
|
-
const parts = [];
|
|
185
|
-
const seen = new Set();
|
|
186
|
-
httpsUrls.forEach((candidate) => {
|
|
187
|
-
if (!candidate || seen.has(candidate)) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
parts.push(createUrlChip(candidate, hostType, 'HTTPS'));
|
|
191
|
-
seen.add(candidate);
|
|
192
|
-
});
|
|
193
|
-
if (httpUrl && !seen.has(httpUrl)) {
|
|
194
|
-
parts.push(createUrlChip(httpUrl, hostType, 'HTTP'));
|
|
195
|
-
seen.add(httpUrl);
|
|
196
|
-
}
|
|
197
|
-
if (parts.length === 0) {
|
|
198
|
-
return '';
|
|
199
|
-
}
|
|
200
|
-
return `<div class="url-dropdown-url-list">${parts.join('')}</div>`;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
172
|
let isDropdownVisible = false;
|
|
204
173
|
let allProcesses = []; // Store all processes for filtering
|
|
205
174
|
let filteredProcesses = []; // Store currently filtered processes
|
|
@@ -449,89 +418,103 @@ function initUrlDropdown(config = {}) {
|
|
|
449
418
|
`;
|
|
450
419
|
}
|
|
451
420
|
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
421
|
+
const buildProcessItemHtml = (process) => {
|
|
422
|
+
const onlineIndicator = process.online ?
|
|
423
|
+
'<div class="status-circle online"></div>' :
|
|
424
|
+
'<div class="status-circle offline"></div>';
|
|
455
425
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
<div class="url-dropdown-
|
|
460
|
-
<span class="host-name">Current tab</span>
|
|
461
|
-
</div>
|
|
462
|
-
<div class="url-dropdown-item" data-url="${escapeAttribute(currentUrl)}" data-host-type="current">
|
|
426
|
+
if (process.ip === null || process.ip === undefined) {
|
|
427
|
+
const networkUrl = `http://${process.host.ip}:42000/network`;
|
|
428
|
+
return `
|
|
429
|
+
<div class="url-dropdown-item non-selectable">
|
|
463
430
|
<div class="url-dropdown-name">
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
</span>
|
|
431
|
+
${onlineIndicator}
|
|
432
|
+
<button class="peer-network-button" data-network-url="${escapeAttribute(networkUrl)}"><i class="fa-solid fa-toggle-on"></i> Turn on peer network</button>
|
|
433
|
+
${escapeHtml(process.name)}
|
|
468
434
|
</div>
|
|
469
|
-
<div class="url-dropdown-url">${escapeHtml(currentUrl)}</div>
|
|
470
435
|
</div>
|
|
471
436
|
`;
|
|
472
437
|
}
|
|
473
438
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
attachCreateButtonHandler(dropdown, urlInput);
|
|
478
|
-
attachUrlItemHandlers(dropdown);
|
|
479
|
-
return;
|
|
439
|
+
const displayUrl = getProcessDisplayUrl(process);
|
|
440
|
+
if (!displayUrl) {
|
|
441
|
+
return '';
|
|
480
442
|
}
|
|
443
|
+
const selectionType = (process.host && process.host.local) ? 'local' : 'remote';
|
|
444
|
+
const schemeLabel = displayUrl.startsWith('https://') ? 'HTTPS' : 'HTTP';
|
|
445
|
+
const formattedUrl = formatUrlLabel(displayUrl);
|
|
446
|
+
return `
|
|
447
|
+
<div class="url-dropdown-item" data-url="${escapeAttribute(displayUrl)}" data-host-type="${escapeAttribute(selectionType)}">
|
|
448
|
+
<div class="url-dropdown-name">
|
|
449
|
+
${onlineIndicator}
|
|
450
|
+
${escapeHtml(process.name)}
|
|
451
|
+
</div>
|
|
452
|
+
<div class="url-dropdown-url">
|
|
453
|
+
<span class="url-scheme ${schemeLabel === 'HTTPS' ? 'https' : 'http'}">${schemeLabel}</span>
|
|
454
|
+
<span class="url-address">${escapeHtml(formattedUrl || displayUrl)}</span>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
`;
|
|
458
|
+
};
|
|
481
459
|
|
|
482
|
-
|
|
460
|
+
const buildHostsHtml = (processes) => {
|
|
483
461
|
const groupedProcesses = groupProcessesByHost(processes);
|
|
462
|
+
let html = '';
|
|
484
463
|
|
|
485
464
|
Object.keys(groupedProcesses).forEach(hostKey => {
|
|
486
465
|
const hostData = groupedProcesses[hostKey];
|
|
487
466
|
const hostInfo = hostData.host;
|
|
488
467
|
const hostProcesses = hostData.processes;
|
|
489
468
|
const isLocal = hostData.isLocal;
|
|
490
|
-
|
|
491
|
-
// Add host header
|
|
469
|
+
|
|
492
470
|
html += createHostHeader(hostInfo, isLocal);
|
|
493
|
-
|
|
494
|
-
// Add processes for this host
|
|
495
|
-
hostProcesses.forEach(process => {
|
|
496
|
-
const onlineIndicator = process.online ?
|
|
497
|
-
'<div class="status-circle online"></div>' :
|
|
498
|
-
'<div class="status-circle offline"></div>';
|
|
499
|
-
|
|
500
|
-
if (process.ip === null || process.ip === undefined) {
|
|
501
|
-
// Non-selectable item with "turn on peer network" button
|
|
502
|
-
const networkUrl = `http://${process.host.ip}:42000/network`;
|
|
503
|
-
html += `
|
|
504
|
-
<div class="url-dropdown-item non-selectable">
|
|
505
|
-
<div class="url-dropdown-name">
|
|
506
|
-
${onlineIndicator}
|
|
507
|
-
<button class="peer-network-button" data-network-url="${escapeAttribute(networkUrl)}"><i class="fa-solid fa-toggle-on"></i> Turn on peer network</button>
|
|
508
|
-
${escapeHtml(process.name)}
|
|
509
|
-
</div>
|
|
510
|
-
</div>
|
|
511
|
-
`;
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
471
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
const selectionType = (process.host && process.host.local) ? 'local' : 'remote';
|
|
520
|
-
const urlListHtml = buildProcessUrlList(process);
|
|
521
|
-
html += `
|
|
522
|
-
<div class="url-dropdown-item" data-url="${escapeAttribute(displayUrl)}" data-host-type="${escapeAttribute(selectionType)}">
|
|
523
|
-
<div class="url-dropdown-name">
|
|
524
|
-
${onlineIndicator}
|
|
525
|
-
${escapeHtml(process.name)}
|
|
526
|
-
</div>
|
|
527
|
-
<div class="url-dropdown-url">${escapeHtml(displayUrl)}</div>
|
|
528
|
-
${urlListHtml}
|
|
529
|
-
</div>
|
|
530
|
-
`;
|
|
472
|
+
hostProcesses.forEach(process => {
|
|
473
|
+
html += buildProcessItemHtml(process);
|
|
531
474
|
});
|
|
532
475
|
});
|
|
533
476
|
|
|
534
|
-
|
|
477
|
+
return html;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const buildDropdownHtml = (processes, { includeCurrentTab = true, inputElement } = {}) => {
|
|
481
|
+
const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
|
|
482
|
+
const currentTitle = typeof document !== 'undefined' ? (document.title || 'Current tab') : 'Current tab';
|
|
483
|
+
|
|
484
|
+
let html = '';
|
|
485
|
+
if (includeCurrentTab && currentUrl) {
|
|
486
|
+
const schemeLabel = currentUrl.startsWith('https://') ? 'HTTPS' : 'HTTP';
|
|
487
|
+
html += `
|
|
488
|
+
<div class="url-dropdown-host-header current-tab">
|
|
489
|
+
<span class="host-name">Current tab</span>
|
|
490
|
+
</div>
|
|
491
|
+
<div class="url-dropdown-item" data-url="${escapeAttribute(currentUrl)}" data-host-type="current">
|
|
492
|
+
<div class="url-dropdown-name">
|
|
493
|
+
<span>
|
|
494
|
+
<i class="fa-solid fa-clone"></i>
|
|
495
|
+
${escapeHtml(currentTitle)}
|
|
496
|
+
</span>
|
|
497
|
+
</div>
|
|
498
|
+
<div class="url-dropdown-url">
|
|
499
|
+
<span class="url-scheme ${schemeLabel === 'HTTPS' ? 'https' : 'http'}">${schemeLabel}</span>
|
|
500
|
+
<span class="url-address">${escapeHtml(formatUrlLabel(currentUrl) || currentUrl)}</span>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!processes || processes.length === 0) {
|
|
507
|
+
html += createEmptyStateHtml(getEmptyStateMessage(inputElement));
|
|
508
|
+
return html;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
html += buildHostsHtml(processes);
|
|
512
|
+
return html;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
function populateDropdown(processes) {
|
|
516
|
+
dropdown.innerHTML = buildDropdownHtml(processes, { includeCurrentTab: true, inputElement: urlInput });
|
|
517
|
+
attachCreateButtonHandler(dropdown, urlInput);
|
|
535
518
|
attachUrlItemHandlers(dropdown);
|
|
536
519
|
}
|
|
537
520
|
|
|
@@ -548,21 +531,19 @@ function initUrlDropdown(config = {}) {
|
|
|
548
531
|
openUrlWithType(url, type);
|
|
549
532
|
}
|
|
550
533
|
|
|
551
|
-
function attachUrlItemHandlers(container) {
|
|
534
|
+
function attachUrlItemHandlers(container, options = {}) {
|
|
552
535
|
if (!container) return;
|
|
536
|
+
const onSelect = typeof options.onSelect === 'function' ? options.onSelect : handleSelection;
|
|
537
|
+
const selectionOptions = options.selectionOptions || {};
|
|
553
538
|
container.querySelectorAll('.url-dropdown-item:not(.non-selectable)').forEach(item => {
|
|
554
539
|
item.addEventListener('click', function() {
|
|
555
540
|
const url = this.getAttribute('data-url');
|
|
556
541
|
const type = this.getAttribute('data-host-type');
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
event.stopPropagation();
|
|
563
|
-
const url = this.getAttribute('data-url');
|
|
564
|
-
const type = this.getAttribute('data-host-type');
|
|
565
|
-
handleSelection(url, type);
|
|
542
|
+
if (onSelect === handleSelection) {
|
|
543
|
+
onSelect(url, type, selectionOptions);
|
|
544
|
+
} else {
|
|
545
|
+
onSelect(url, type);
|
|
546
|
+
}
|
|
566
547
|
});
|
|
567
548
|
});
|
|
568
549
|
container.querySelectorAll('.peer-network-button').forEach(button => {
|
|
@@ -829,6 +810,9 @@ function initUrlDropdown(config = {}) {
|
|
|
829
810
|
refs.context = customOptions.context || 'default';
|
|
830
811
|
refs.returnSelection = Boolean(customOptions.awaitSelection);
|
|
831
812
|
refs.resolve = null;
|
|
813
|
+
if (refs.dropdown) {
|
|
814
|
+
refs.dropdown._includeCurrent = includeCurrent;
|
|
815
|
+
}
|
|
832
816
|
|
|
833
817
|
modalInput.value = initialValue;
|
|
834
818
|
updateConfirmState();
|
|
@@ -923,15 +907,28 @@ function initUrlDropdown(config = {}) {
|
|
|
923
907
|
}
|
|
924
908
|
|
|
925
909
|
function showModalDropdown(modalDropdown) {
|
|
910
|
+
if (!modalDropdown || !modalDropdown.parentElement) return;
|
|
926
911
|
modalDropdown.style.display = 'block';
|
|
927
|
-
|
|
912
|
+
|
|
913
|
+
const includeCurrent = modalDropdown._includeCurrent !== false;
|
|
914
|
+
modalDropdown._includeCurrent = includeCurrent;
|
|
915
|
+
|
|
916
|
+
const render = (processes) => {
|
|
917
|
+
modalDropdown.innerHTML = buildDropdownHtml(processes, {
|
|
918
|
+
includeCurrentTab: includeCurrent,
|
|
919
|
+
inputElement: modalDropdown.parentElement.querySelector('.url-modal-input')
|
|
920
|
+
});
|
|
921
|
+
attachCreateButtonHandler(modalDropdown, modalDropdown.parentElement.querySelector('.url-modal-input'));
|
|
922
|
+
attachUrlItemHandlers(modalDropdown, { onSelect: handleModalSelection });
|
|
923
|
+
};
|
|
924
|
+
|
|
928
925
|
if (allProcesses.length > 0) {
|
|
929
|
-
|
|
926
|
+
render(allProcesses);
|
|
930
927
|
return;
|
|
931
928
|
}
|
|
932
|
-
|
|
929
|
+
|
|
933
930
|
modalDropdown.innerHTML = '<div class="url-dropdown-loading">Loading running processes...</div>';
|
|
934
|
-
|
|
931
|
+
|
|
935
932
|
fetch(options.apiEndpoint)
|
|
936
933
|
.then(response => {
|
|
937
934
|
if (!response.ok) {
|
|
@@ -941,7 +938,7 @@ function initUrlDropdown(config = {}) {
|
|
|
941
938
|
})
|
|
942
939
|
.then(data => {
|
|
943
940
|
allProcesses = data.info || [];
|
|
944
|
-
|
|
941
|
+
render(allProcesses);
|
|
945
942
|
})
|
|
946
943
|
.catch(error => {
|
|
947
944
|
console.error('Failed to fetch processes:', error);
|
|
@@ -971,103 +968,16 @@ function initUrlDropdown(config = {}) {
|
|
|
971
968
|
|
|
972
969
|
function populateModalDropdown(processes, modalDropdown) {
|
|
973
970
|
const modalInput = modalDropdown.parentElement.querySelector('.url-modal-input');
|
|
974
|
-
const currentUrl = window.location.href;
|
|
975
|
-
const currentTitle = document.title || 'Current tab';
|
|
976
971
|
const overlayRefs = getModalRefs();
|
|
977
972
|
const includeCurrent = overlayRefs?.includeCurrent !== false;
|
|
978
973
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
html += `
|
|
983
|
-
<div class="url-dropdown-host-header current-tab">
|
|
984
|
-
<span class="host-name">Current tab</span>
|
|
985
|
-
</div>
|
|
986
|
-
<div class="url-dropdown-item" data-url="${escapeHtml(currentUrl)}" data-host-type="current">
|
|
987
|
-
<div class="url-dropdown-name">
|
|
988
|
-
<i class="fa-solid fa-clone"></i>
|
|
989
|
-
<span>${escapeHtml(currentTitle)}</span>
|
|
990
|
-
</div>
|
|
991
|
-
<div class="url-dropdown-url">${escapeHtml(currentUrl)}</div>
|
|
992
|
-
</div>
|
|
993
|
-
`;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (processes.length === 0) {
|
|
997
|
-
html += createEmptyStateHtml(getEmptyStateMessage(modalInput));
|
|
998
|
-
modalDropdown.innerHTML = html;
|
|
999
|
-
attachCreateButtonHandler(modalDropdown, modalInput);
|
|
1000
|
-
|
|
1001
|
-
modalDropdown.querySelectorAll('.url-dropdown-item:not(.non-selectable)').forEach(item => {
|
|
1002
|
-
item.addEventListener('click', function() {
|
|
1003
|
-
const url = this.getAttribute('data-url');
|
|
1004
|
-
const type = this.getAttribute('data-host-type');
|
|
1005
|
-
handleModalSelection(url, type);
|
|
1006
|
-
});
|
|
1007
|
-
});
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
const groupedProcesses = groupProcessesByHost(processes);
|
|
1012
|
-
Object.keys(groupedProcesses).forEach(hostKey => {
|
|
1013
|
-
const hostData = groupedProcesses[hostKey];
|
|
1014
|
-
const hostInfo = hostData.host;
|
|
1015
|
-
const hostProcesses = hostData.processes;
|
|
1016
|
-
const isLocal = hostData.isLocal;
|
|
1017
|
-
|
|
1018
|
-
html += createHostHeader(hostInfo, isLocal);
|
|
1019
|
-
|
|
1020
|
-
hostProcesses.forEach(process => {
|
|
1021
|
-
const onlineIndicator = process.online ?
|
|
1022
|
-
'<div class="status-circle online"></div>' :
|
|
1023
|
-
'<div class="status-circle offline"></div>';
|
|
1024
|
-
|
|
1025
|
-
if (process.ip === null || process.ip === undefined) {
|
|
1026
|
-
const networkUrl = `http://${process.host.ip}:42000/network`;
|
|
1027
|
-
html += `
|
|
1028
|
-
<div class="url-dropdown-item non-selectable">
|
|
1029
|
-
<div class="url-dropdown-name">
|
|
1030
|
-
${onlineIndicator}
|
|
1031
|
-
${escapeHtml(process.name)}
|
|
1032
|
-
</div>
|
|
1033
|
-
<button class="peer-network-button" data-network-url="${networkUrl}"><i class="fa-solid fa-toggle-on"></i> Turn on peer network</button>
|
|
1034
|
-
</div>
|
|
1035
|
-
`;
|
|
1036
|
-
} else {
|
|
1037
|
-
const url = getProcessDisplayUrl(process);
|
|
1038
|
-
if (!url) {
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
html += `
|
|
1042
|
-
<div class="url-dropdown-item" data-url="${url}" data-host-type="${process.host.local ? "local" : "remote"}">
|
|
1043
|
-
<div class="url-dropdown-name">
|
|
1044
|
-
${onlineIndicator}
|
|
1045
|
-
${escapeHtml(process.name)}
|
|
1046
|
-
</div>
|
|
1047
|
-
<div class="url-dropdown-url">${escapeHtml(url)}</div>
|
|
1048
|
-
</div>
|
|
1049
|
-
`;
|
|
1050
|
-
}
|
|
1051
|
-
});
|
|
1052
|
-
});
|
|
1053
|
-
|
|
1054
|
-
modalDropdown.innerHTML = html;
|
|
1055
|
-
|
|
1056
|
-
modalDropdown.querySelectorAll('.url-dropdown-item:not(.non-selectable)').forEach(item => {
|
|
1057
|
-
item.addEventListener('click', function() {
|
|
1058
|
-
const url = this.getAttribute('data-url');
|
|
1059
|
-
const type = this.getAttribute('data-host-type');
|
|
1060
|
-
handleModalSelection(url, type);
|
|
1061
|
-
});
|
|
974
|
+
modalDropdown.innerHTML = buildDropdownHtml(processes, {
|
|
975
|
+
includeCurrentTab: includeCurrent,
|
|
976
|
+
inputElement: modalInput
|
|
1062
977
|
});
|
|
1063
978
|
|
|
1064
|
-
modalDropdown
|
|
1065
|
-
|
|
1066
|
-
e.stopPropagation();
|
|
1067
|
-
const networkUrl = this.getAttribute('data-network-url');
|
|
1068
|
-
window.open(networkUrl, '_blank', 'self');
|
|
1069
|
-
});
|
|
1070
|
-
});
|
|
979
|
+
attachCreateButtonHandler(modalDropdown, modalInput);
|
|
980
|
+
attachUrlItemHandlers(modalDropdown, { onSelect: handleModalSelection });
|
|
1071
981
|
}
|
|
1072
982
|
|
|
1073
983
|
// Set up mobile button click handler
|