pinokiod 3.180.0 → 3.181.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/favicon.js +91 -34
- package/kernel/peer.js +73 -0
- package/kernel/util.js +13 -2
- package/package.json +1 -1
- package/server/index.js +126 -23
- package/server/public/common.js +244 -0
- package/server/public/layout.js +114 -0
- package/server/public/nav.js +227 -64
- package/server/public/style.css +22 -2
- package/server/public/tab-idle-notifier.js +3 -0
- package/server/socket.js +71 -4
- package/server/views/app.ejs +226 -56
- package/server/views/connect.ejs +9 -0
- package/server/views/index.ejs +9 -0
- package/server/views/init/index.ejs +9 -2
- package/server/views/layout.ejs +2 -0
- package/server/views/net.ejs +9 -0
- package/server/views/network.ejs +9 -0
- package/server/views/screenshots.ejs +9 -0
- package/server/views/settings.ejs +9 -0
- package/server/views/terminals.ejs +12 -3
- package/server/views/tools.ejs +10 -1
package/kernel/favicon.js
CHANGED
|
@@ -1,55 +1,112 @@
|
|
|
1
1
|
const axios = require('axios')
|
|
2
|
-
const { URL } = require(
|
|
3
|
-
const { JSDOM } = require(
|
|
2
|
+
const { URL } = require('url')
|
|
3
|
+
const { JSDOM } = require('jsdom')
|
|
4
4
|
|
|
5
5
|
class Favicon {
|
|
6
|
+
constructor(opts = {}) {
|
|
7
|
+
this.cache = new Map() // origin -> { url: string|null, ts: number }
|
|
8
|
+
this.ttlMs = opts.ttlMs || (10 * 60 * 1000) // 10 minutes
|
|
9
|
+
this.headTimeoutMs = opts.headTimeoutMs || 800
|
|
10
|
+
this.getTimeoutMs = opts.getTimeoutMs || 700
|
|
11
|
+
this.totalBudgetMs = opts.totalBudgetMs || 900
|
|
12
|
+
this.commonPaths = Array.isArray(opts.commonPaths) && opts.commonPaths.length > 0
|
|
13
|
+
? opts.commonPaths
|
|
14
|
+
: ['/favicon.ico', '/favicon.png']
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_now() {
|
|
18
|
+
return Date.now()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_getCache(origin) {
|
|
22
|
+
const entry = this.cache.get(origin)
|
|
23
|
+
if (!entry) return null
|
|
24
|
+
if (this._now() - entry.ts > this.ttlMs) {
|
|
25
|
+
this.cache.delete(origin)
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
return entry.url
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_setCache(origin, url) {
|
|
32
|
+
this.cache.set(origin, { url: url || null, ts: this._now() })
|
|
33
|
+
}
|
|
34
|
+
|
|
6
35
|
async get(pageUrl) {
|
|
7
|
-
let origin
|
|
36
|
+
let origin
|
|
8
37
|
try {
|
|
9
|
-
origin = new URL(pageUrl).origin
|
|
38
|
+
origin = new URL(pageUrl).origin
|
|
10
39
|
} catch {
|
|
11
|
-
throw new Error(
|
|
40
|
+
throw new Error('Invalid URL: ' + pageUrl)
|
|
12
41
|
}
|
|
13
42
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
// 1. Try common favicon paths first
|
|
23
|
-
for (const path of candidates) {
|
|
24
|
-
const fullUrl = origin + path;
|
|
25
|
-
if (await this.checkImageUrl(fullUrl)) return fullUrl;
|
|
43
|
+
const cached = this._getCache(origin)
|
|
44
|
+
if (typeof cached !== 'undefined' && cached !== null) {
|
|
45
|
+
return cached
|
|
46
|
+
} else if (cached === null) {
|
|
47
|
+
// we cached a miss previously
|
|
48
|
+
return null
|
|
26
49
|
}
|
|
27
50
|
|
|
28
|
-
|
|
51
|
+
const start = this._now()
|
|
52
|
+
const withinBudget = () => (this._now() - start) < this.totalBudgetMs
|
|
53
|
+
|
|
54
|
+
// 1) Try common paths (limited set) in parallel, with short HEAD timeouts
|
|
29
55
|
try {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (await this.checkImageUrl(resolvedUrl)) return resolvedUrl;
|
|
56
|
+
const attempts = this.commonPaths.map((p) => this.checkImageUrl(origin + p, this.headTimeoutMs))
|
|
57
|
+
const results = await Promise.all(attempts)
|
|
58
|
+
for (let i = 0; i < results.length; i++) {
|
|
59
|
+
if (results[i]) {
|
|
60
|
+
const url = origin + this.commonPaths[i]
|
|
61
|
+
this._setCache(origin, url)
|
|
62
|
+
return url
|
|
63
|
+
}
|
|
39
64
|
}
|
|
40
|
-
} catch {
|
|
41
|
-
|
|
65
|
+
} catch (_) {}
|
|
66
|
+
|
|
67
|
+
if (!withinBudget()) {
|
|
68
|
+
this._setCache(origin, null)
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2) Fallback: fetch the page quickly, parse <link rel="icon">, then HEAD the first 1-2 candidates
|
|
73
|
+
try {
|
|
74
|
+
const res = await axios.get(pageUrl, { timeout: this.getTimeoutMs })
|
|
75
|
+
const dom = new JSDOM(res.data)
|
|
76
|
+
const nodes = Array.from(dom.window.document.querySelectorAll("link[rel~='icon'], link[rel='apple-touch-icon']"))
|
|
77
|
+
const hrefs = []
|
|
78
|
+
for (const node of nodes) {
|
|
79
|
+
const href = node.getAttribute('href')
|
|
80
|
+
if (href && typeof href === 'string') {
|
|
81
|
+
try {
|
|
82
|
+
const resolved = new URL(href, origin).href
|
|
83
|
+
hrefs.push(resolved)
|
|
84
|
+
} catch (_) {}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Try at most 2 parsed icons within budget
|
|
88
|
+
for (let i = 0; i < Math.min(2, hrefs.length); i++) {
|
|
89
|
+
if (!withinBudget()) break
|
|
90
|
+
const ok = await this.checkImageUrl(hrefs[i], this.headTimeoutMs)
|
|
91
|
+
if (ok) {
|
|
92
|
+
this._setCache(origin, hrefs[i])
|
|
93
|
+
return hrefs[i]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (_) {
|
|
97
|
+
// ignore parse failures
|
|
42
98
|
}
|
|
43
99
|
|
|
44
|
-
|
|
100
|
+
this._setCache(origin, null)
|
|
101
|
+
return null
|
|
45
102
|
}
|
|
46
103
|
|
|
47
|
-
async checkImageUrl(url) {
|
|
104
|
+
async checkImageUrl(url, timeoutOverride) {
|
|
48
105
|
try {
|
|
49
|
-
const res = await axios.head(url, { timeout:
|
|
50
|
-
return res.status === 200 && res.headers[
|
|
106
|
+
const res = await axios.head(url, { timeout: timeoutOverride || this.headTimeoutMs })
|
|
107
|
+
return res.status === 200 && (res.headers['content-type'] || '').startsWith('image/')
|
|
51
108
|
} catch {
|
|
52
|
-
return false
|
|
109
|
+
return false
|
|
53
110
|
}
|
|
54
111
|
}
|
|
55
112
|
}
|
package/kernel/peer.js
CHANGED
|
@@ -337,6 +337,79 @@ class PeerDiscovery {
|
|
|
337
337
|
return []
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
|
+
async router_info_lite() {
|
|
341
|
+
try {
|
|
342
|
+
let processes = []
|
|
343
|
+
if (this.info && this.info[this.host]) {
|
|
344
|
+
let procs = this.info[this.host].proc
|
|
345
|
+
let router = this.info[this.host].router
|
|
346
|
+
let port_mapping = this.info[this.host].port_mapping
|
|
347
|
+
for (let proc of procs) {
|
|
348
|
+
let chunks = (proc.ip || '').split(":")
|
|
349
|
+
let internal_port = chunks[chunks.length - 1]
|
|
350
|
+
let internal_host = chunks.slice(0, chunks.length - 1).join(":")
|
|
351
|
+
let external_port = port_mapping ? port_mapping[internal_port] : undefined
|
|
352
|
+
let external_ip = external_port ? `${this.host}:${external_port}` : undefined
|
|
353
|
+
|
|
354
|
+
let internal_router = []
|
|
355
|
+
// Check common local keys
|
|
356
|
+
const keys = [
|
|
357
|
+
`127.0.0.1:${proc.port}`,
|
|
358
|
+
`0.0.0.0:${proc.port}`,
|
|
359
|
+
`localhost:${proc.port}`,
|
|
360
|
+
]
|
|
361
|
+
for (const key of keys) {
|
|
362
|
+
if (router && router[key]) {
|
|
363
|
+
internal_router = internal_router.concat(router[key])
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const info = {
|
|
368
|
+
external_router: (router && external_ip && router[external_ip]) ? router[external_ip] : [],
|
|
369
|
+
internal_router,
|
|
370
|
+
external_ip,
|
|
371
|
+
external_port: external_port ? parseInt(external_port) : undefined,
|
|
372
|
+
internal_port: internal_port ? parseInt(internal_port) : undefined,
|
|
373
|
+
...proc,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// In custom-domain mode, ensure external_router has something meaningful
|
|
377
|
+
const usingCustomDomain = this.kernel.router_kind === 'custom-domain'
|
|
378
|
+
if (usingCustomDomain) {
|
|
379
|
+
if (!info.external_router || info.external_router.length === 0) {
|
|
380
|
+
const fallbackKeys = new Set([
|
|
381
|
+
proc.ip,
|
|
382
|
+
`${internal_host}:${proc.port}`,
|
|
383
|
+
`127.0.0.1:${proc.port}`,
|
|
384
|
+
`0.0.0.0:${proc.port}`,
|
|
385
|
+
`localhost:${proc.port}`
|
|
386
|
+
])
|
|
387
|
+
for (const key of fallbackKeys) {
|
|
388
|
+
if (key && router && router[key] && router[key].length > 0) {
|
|
389
|
+
info.external_router = router[key]
|
|
390
|
+
break
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (info.external_router && info.external_router.length > 0) {
|
|
395
|
+
info.external_router = Array.from(new Set(info.external_router))
|
|
396
|
+
} else if (internal_router.length > 0) {
|
|
397
|
+
info.external_router = Array.from(new Set(internal_router))
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
processes.push(info)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
processes.sort((a, b) => {
|
|
405
|
+
return (b.external_port || 0) - (a.external_port || 0)
|
|
406
|
+
})
|
|
407
|
+
return processes
|
|
408
|
+
} catch (e) {
|
|
409
|
+
console.log('router_info_lite ERROR', e)
|
|
410
|
+
return []
|
|
411
|
+
}
|
|
412
|
+
}
|
|
340
413
|
async installed() {
|
|
341
414
|
let folders = await fs.promises.readdir(this.kernel.path("api"))
|
|
342
415
|
let installed = []
|
package/kernel/util.js
CHANGED
|
@@ -698,6 +698,8 @@ function push(params) {
|
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
700
|
const clientImage = resolvePublicAssetUrl(notifyParams.contentImage) || resolvePublicAssetUrl(notifyParams.image)
|
|
701
|
+
const deviceId = (typeof notifyParams.device_id === 'string' && notifyParams.device_id.trim()) ? notifyParams.device_id.trim() : null
|
|
702
|
+
const audience = (typeof notifyParams.audience === 'string' && notifyParams.audience.trim()) ? notifyParams.audience.trim() : null
|
|
701
703
|
const eventPayload = {
|
|
702
704
|
id: randomUUID(),
|
|
703
705
|
title: notifyParams.title,
|
|
@@ -707,11 +709,20 @@ function push(params) {
|
|
|
707
709
|
sound: clientSoundUrl || null,
|
|
708
710
|
timestamp: Date.now(),
|
|
709
711
|
platform,
|
|
712
|
+
device_id: deviceId,
|
|
713
|
+
audience,
|
|
710
714
|
}
|
|
711
715
|
|
|
712
716
|
emitPushEvent(eventPayload)
|
|
713
|
-
|
|
714
|
-
|
|
717
|
+
// Suppress host OS notification when explicitly disabled (e.g., device-scoped pushes)
|
|
718
|
+
const shouldNotifyHost = notifyParams.host !== false
|
|
719
|
+
if (shouldNotifyHost) {
|
|
720
|
+
const notifyCopy = { ...notifyParams }
|
|
721
|
+
delete notifyCopy.host
|
|
722
|
+
delete notifyCopy.device_id
|
|
723
|
+
delete notifyCopy.audience
|
|
724
|
+
notifier.notify(notifyCopy)
|
|
725
|
+
}
|
|
715
726
|
}
|
|
716
727
|
function p2u(localPath) {
|
|
717
728
|
/*
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1509,10 +1509,15 @@ class Server {
|
|
|
1509
1509
|
drive: path.resolve(this.kernel.homedir, "drive"),
|
|
1510
1510
|
}
|
|
1511
1511
|
}
|
|
1512
|
+
const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
|
|
1513
|
+
let peer_qr = null
|
|
1514
|
+
try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
|
|
1512
1515
|
let list = this.getPeers()
|
|
1513
1516
|
res.render("settings", {
|
|
1514
1517
|
list,
|
|
1515
1518
|
current_host: this.kernel.peer.host,
|
|
1519
|
+
peer_url,
|
|
1520
|
+
peer_qr,
|
|
1516
1521
|
platform,
|
|
1517
1522
|
version: this.version,
|
|
1518
1523
|
logo: this.logo,
|
|
@@ -2234,6 +2239,10 @@ class Server {
|
|
|
2234
2239
|
qr_cloudflare = await QRCode.toDataURL(this.cloudflare_pub)
|
|
2235
2240
|
}
|
|
2236
2241
|
|
|
2242
|
+
const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
|
|
2243
|
+
let peer_qr = null
|
|
2244
|
+
try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
|
|
2245
|
+
|
|
2237
2246
|
// custom theme
|
|
2238
2247
|
let exists = await fse.pathExists(this.kernel.path("web"))
|
|
2239
2248
|
if (exists) {
|
|
@@ -2263,6 +2272,8 @@ class Server {
|
|
|
2263
2272
|
res.render("index", {
|
|
2264
2273
|
list,
|
|
2265
2274
|
current_host: this.kernel.peer.host,
|
|
2275
|
+
peer_url,
|
|
2276
|
+
peer_qr,
|
|
2266
2277
|
current_urls,
|
|
2267
2278
|
portal: this.portal,
|
|
2268
2279
|
install: this.install,
|
|
@@ -3924,6 +3935,9 @@ class Server {
|
|
|
3924
3935
|
}))
|
|
3925
3936
|
*/
|
|
3926
3937
|
this.app.get("/tools", ex(async (req, res) => {
|
|
3938
|
+
const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
|
|
3939
|
+
let peer_qr = null
|
|
3940
|
+
try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
|
|
3927
3941
|
let list = this.getPeers()
|
|
3928
3942
|
let installs = []
|
|
3929
3943
|
for(let key in this.kernel.bin.installed) {
|
|
@@ -3955,6 +3969,8 @@ class Server {
|
|
|
3955
3969
|
}
|
|
3956
3970
|
res.render("tools", {
|
|
3957
3971
|
current_host: this.kernel.peer.host,
|
|
3972
|
+
peer_url,
|
|
3973
|
+
peer_qr,
|
|
3958
3974
|
pending,
|
|
3959
3975
|
installs,
|
|
3960
3976
|
bundles,
|
|
@@ -4039,9 +4055,14 @@ class Server {
|
|
|
4039
4055
|
return (a.name || '').localeCompare(b.name || '')
|
|
4040
4056
|
})
|
|
4041
4057
|
|
|
4058
|
+
const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
|
|
4059
|
+
let peer_qr = null
|
|
4060
|
+
try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
|
|
4042
4061
|
const list = this.getPeers()
|
|
4043
4062
|
res.render("terminals", {
|
|
4044
4063
|
current_host: this.kernel.peer.host,
|
|
4064
|
+
peer_url,
|
|
4065
|
+
peer_qr,
|
|
4045
4066
|
pluginMenu,
|
|
4046
4067
|
apps,
|
|
4047
4068
|
portal: this.portal,
|
|
@@ -4055,9 +4076,14 @@ class Server {
|
|
|
4055
4076
|
res.redirect(301, "/terminals")
|
|
4056
4077
|
})
|
|
4057
4078
|
this.app.get("/screenshots", ex(async (req, res) => {
|
|
4079
|
+
const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
|
|
4080
|
+
let peer_qr = null
|
|
4081
|
+
try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
|
|
4058
4082
|
let list = this.getPeers()
|
|
4059
4083
|
res.render("screenshots", {
|
|
4060
4084
|
current_host: this.kernel.peer.host,
|
|
4085
|
+
peer_url,
|
|
4086
|
+
peer_qr,
|
|
4061
4087
|
version: this.version,
|
|
4062
4088
|
portal: this.portal,
|
|
4063
4089
|
logo: this.logo,
|
|
@@ -4144,6 +4170,7 @@ class Server {
|
|
|
4144
4170
|
// if <app_name>.localhost
|
|
4145
4171
|
// otherwise => redirect
|
|
4146
4172
|
|
|
4173
|
+
console.log("Chunks", chunks)
|
|
4147
4174
|
|
|
4148
4175
|
if (chunks.length >= 2) {
|
|
4149
4176
|
|
|
@@ -4190,29 +4217,31 @@ class Server {
|
|
|
4190
4217
|
} else {
|
|
4191
4218
|
nameChunks = chunks
|
|
4192
4219
|
}
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4220
|
+
if (nameChunks) {
|
|
4221
|
+
let name = nameChunks.join(".")
|
|
4222
|
+
let api_path = this.kernel.path("api", name)
|
|
4223
|
+
let exists = await this.exists(api_path)
|
|
4224
|
+
if (exists) {
|
|
4225
|
+
let meta = await this.kernel.api.meta(name)
|
|
4226
|
+
let launcher = await this.kernel.api.launcher(name)
|
|
4227
|
+
let pinokio = launcher.script
|
|
4228
|
+
let launchable = false
|
|
4229
|
+
if (pinokio && pinokio.menu && pinokio.menu.length > 0) {
|
|
4230
|
+
launchable = true
|
|
4231
|
+
}
|
|
4232
|
+
res.render("start", {
|
|
4233
|
+
url,
|
|
4234
|
+
launchable,
|
|
4235
|
+
autolaunch,
|
|
4236
|
+
logo: this.logo,
|
|
4237
|
+
theme: this.theme,
|
|
4238
|
+
agent: req.agent,
|
|
4239
|
+
name: meta.title,
|
|
4240
|
+
image: meta.icon,
|
|
4241
|
+
link: `/p/${name}?autolaunch=${autolaunch ? "1" : "0"}`,
|
|
4242
|
+
})
|
|
4243
|
+
return
|
|
4244
|
+
}
|
|
4216
4245
|
}
|
|
4217
4246
|
}
|
|
4218
4247
|
res.render("start", {
|
|
@@ -4325,9 +4354,14 @@ class Server {
|
|
|
4325
4354
|
drive: path.resolve(this.kernel.homedir, "drive"),
|
|
4326
4355
|
}
|
|
4327
4356
|
}
|
|
4357
|
+
const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
|
|
4358
|
+
let peer_qr = null
|
|
4359
|
+
try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
|
|
4328
4360
|
let list = this.getPeers()
|
|
4329
4361
|
res.render("settings", {
|
|
4330
4362
|
current_host: this.kernel.peer.host,
|
|
4363
|
+
peer_url,
|
|
4364
|
+
peer_qr,
|
|
4331
4365
|
list,
|
|
4332
4366
|
platform,
|
|
4333
4367
|
version: this.version,
|
|
@@ -4443,6 +4477,9 @@ class Server {
|
|
|
4443
4477
|
return
|
|
4444
4478
|
}
|
|
4445
4479
|
|
|
4480
|
+
const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
|
|
4481
|
+
let peer_qr = null
|
|
4482
|
+
try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
|
|
4446
4483
|
let list = this.getPeers()
|
|
4447
4484
|
let ai = await this.kernel.proto.ai()
|
|
4448
4485
|
ai = [{
|
|
@@ -4456,6 +4493,8 @@ class Server {
|
|
|
4456
4493
|
list,
|
|
4457
4494
|
ai,
|
|
4458
4495
|
current_host: this.kernel.peer.host,
|
|
4496
|
+
peer_url,
|
|
4497
|
+
peer_qr,
|
|
4459
4498
|
cwd: this.kernel.path("api"),
|
|
4460
4499
|
name: null,
|
|
4461
4500
|
// name: req.params.name,
|
|
@@ -4556,9 +4595,14 @@ class Server {
|
|
|
4556
4595
|
} catch (e) {
|
|
4557
4596
|
}
|
|
4558
4597
|
}
|
|
4598
|
+
const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
|
|
4599
|
+
let peer_qr = null
|
|
4600
|
+
try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
|
|
4559
4601
|
res.render(`connect`, {
|
|
4560
4602
|
current_urls,
|
|
4561
4603
|
current_host: this.kernel.peer.host,
|
|
4604
|
+
peer_url,
|
|
4605
|
+
peer_qr,
|
|
4562
4606
|
list,
|
|
4563
4607
|
portal: this.portal,
|
|
4564
4608
|
logo: this.logo,
|
|
@@ -4700,6 +4744,13 @@ class Server {
|
|
|
4700
4744
|
this.app.post("/push", ex(async (req, res) => {
|
|
4701
4745
|
try {
|
|
4702
4746
|
const payload = { ...(req.body || {}) }
|
|
4747
|
+
// Normalise audience and device targeting
|
|
4748
|
+
if (typeof payload.audience === 'string') {
|
|
4749
|
+
payload.audience = payload.audience.trim() || undefined
|
|
4750
|
+
}
|
|
4751
|
+
if (typeof payload.device_id === 'string') {
|
|
4752
|
+
payload.device_id = payload.device_id.trim() || undefined
|
|
4753
|
+
}
|
|
4703
4754
|
const resolveAssetPath = (raw) => {
|
|
4704
4755
|
if (typeof raw !== 'string') {
|
|
4705
4756
|
return null
|
|
@@ -4782,6 +4833,19 @@ class Server {
|
|
|
4782
4833
|
}
|
|
4783
4834
|
delete payload.soundUrl
|
|
4784
4835
|
delete payload.soundPath
|
|
4836
|
+
// For device-scoped notifications, suppress host OS notifier for remote origins,
|
|
4837
|
+
// but allow it when the request originates from the local machine
|
|
4838
|
+
if (payload.audience === 'device' && typeof payload.device_id === 'string' && payload.device_id) {
|
|
4839
|
+
try {
|
|
4840
|
+
if (this.socket && typeof this.socket.isLocalDevice === 'function') {
|
|
4841
|
+
payload.host = !!this.socket.isLocalDevice(payload.device_id)
|
|
4842
|
+
} else {
|
|
4843
|
+
payload.host = false
|
|
4844
|
+
}
|
|
4845
|
+
} catch (_) {
|
|
4846
|
+
payload.host = false
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4785
4849
|
Util.push(payload)
|
|
4786
4850
|
res.json({ success: true })
|
|
4787
4851
|
} catch (e) {
|
|
@@ -5343,6 +5407,9 @@ class Server {
|
|
|
5343
5407
|
let static_routes = Object.keys(this.kernel.router.rewrite_mapping).map((key) => {
|
|
5344
5408
|
return this.kernel.router.rewrite_mapping[key]
|
|
5345
5409
|
})
|
|
5410
|
+
const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
|
|
5411
|
+
let peer_qr = null
|
|
5412
|
+
try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
|
|
5346
5413
|
res.render("net", {
|
|
5347
5414
|
static_routes,
|
|
5348
5415
|
selected_name: req.params.name,
|
|
@@ -5361,6 +5428,8 @@ class Server {
|
|
|
5361
5428
|
peer,
|
|
5362
5429
|
protocol,
|
|
5363
5430
|
current_host: this.kernel.peer.host,
|
|
5431
|
+
peer_url,
|
|
5432
|
+
peer_qr,
|
|
5364
5433
|
})
|
|
5365
5434
|
}))
|
|
5366
5435
|
this.app.get("/network", ex(async (req, res) => {
|
|
@@ -5502,6 +5571,9 @@ class Server {
|
|
|
5502
5571
|
let static_routes = Object.keys(this.kernel.router.rewrite_mapping).map((key) => {
|
|
5503
5572
|
return this.kernel.router.rewrite_mapping[key]
|
|
5504
5573
|
})
|
|
5574
|
+
const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
|
|
5575
|
+
let peer_qr = null
|
|
5576
|
+
try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
|
|
5505
5577
|
res.render("network", {
|
|
5506
5578
|
static_routes,
|
|
5507
5579
|
host,
|
|
@@ -5527,6 +5599,8 @@ class Server {
|
|
|
5527
5599
|
peer_active: this.kernel.peer.active,
|
|
5528
5600
|
port_mapping: this.kernel.router.port_mapping,
|
|
5529
5601
|
// port_mapping: this.kernel.caddy.port_mapping,
|
|
5602
|
+
peer_url,
|
|
5603
|
+
peer_qr,
|
|
5530
5604
|
// ip_mapping: this.kernel.caddy.ip_mapping,
|
|
5531
5605
|
lan: this.kernel.router.local_network_mapping,
|
|
5532
5606
|
agent: req.agent,
|
|
@@ -7059,6 +7133,35 @@ class Server {
|
|
|
7059
7133
|
let current_peer_info = await this.kernel.peer.current_host()
|
|
7060
7134
|
res.json(current_peer_info)
|
|
7061
7135
|
}))
|
|
7136
|
+
this.app.get("/info/router", ex(async (req, res) => {
|
|
7137
|
+
try {
|
|
7138
|
+
// Lightweight router mapping without favicon or installed scans
|
|
7139
|
+
const https_active = this.kernel.peer.https_active
|
|
7140
|
+
const router_info = await this.kernel.peer.router_info_lite()
|
|
7141
|
+
const rewrite_mapping = this.kernel.router.rewrite_mapping
|
|
7142
|
+
const router = this.kernel.router.published()
|
|
7143
|
+
res.json({ https_active, router_info, rewrite_mapping, router })
|
|
7144
|
+
} catch (err) {
|
|
7145
|
+
res.json({ https_active: false, router_info: [], rewrite_mapping: {}, router: {} })
|
|
7146
|
+
}
|
|
7147
|
+
}))
|
|
7148
|
+
this.app.get("/qr", ex(async (req, res) => {
|
|
7149
|
+
try {
|
|
7150
|
+
const data = typeof req.query.data === 'string' ? req.query.data : ''
|
|
7151
|
+
if (!data) {
|
|
7152
|
+
res.status(400).json({ error: 'Missing data parameter' })
|
|
7153
|
+
return
|
|
7154
|
+
}
|
|
7155
|
+
const scale = Math.max(2, Math.min(10, parseInt(req.query.s || '4', 10) || 4))
|
|
7156
|
+
const margin = Math.max(0, Math.min(4, parseInt(req.query.m || '0', 10) || 0))
|
|
7157
|
+
const buf = await QRCode.toBuffer(data, { type: 'png', scale, margin })
|
|
7158
|
+
res.setHeader('Content-Type', 'image/png')
|
|
7159
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
7160
|
+
res.send(buf)
|
|
7161
|
+
} catch (err) {
|
|
7162
|
+
res.status(500).json({ error: 'Failed to generate QR' })
|
|
7163
|
+
}
|
|
7164
|
+
}))
|
|
7062
7165
|
this.app.get("/info/api", ex(async (req,res) => {
|
|
7063
7166
|
// api related info
|
|
7064
7167
|
let repo = this.kernel.git.find(req.query.git)
|