pinokiod 3.231.0 → 3.232.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.
@@ -3,100 +3,107 @@ const fs = require('fs')
3
3
  const path = require('path')
4
4
  class Cuda {
5
5
  async install(req, ondata) {
6
- if (this.kernel.platform === "win32") {
7
- await this.kernel.bin.exec({
8
- message: [
9
- "conda clean -y --all",
10
- "conda install -y cudnn libzlib-wapi -c conda-forge",
11
- ]
12
- }, ondata)
13
- await this.kernel.bin.exec({
14
- message: [
15
- "conda clean -y --all",
16
- "conda install -y nvidia/label/cuda-12.8.1::cuda"
17
- ]
18
- }, ondata)
19
- const folder = this.kernel.bin.path("miniconda/etc/conda/activate.d")
20
- let deactivate_list = [
21
- "~cuda-nvcc_activate.bat",
22
- "vs2019_compiler_vars.bat",
23
- "vs2022_compiler_vars.bat",
24
- ]
25
- for(let item of deactivate_list) {
26
- const old_name = path.resolve(folder, item)
27
- const new_name = path.resolve(folder, item + ".disabled")
28
- console.log("rename", { old_name, new_name })
29
- await fs.promises.rename(old_name, new_name)
30
- }
31
- } else {
32
- await this.kernel.bin.exec({
33
- message: [
34
- "conda clean -y --all",
35
- "conda install -y cudnn -c conda-forge",
36
- ]
37
- }, ondata)
38
- await this.kernel.bin.exec({
39
- message: [
40
- "conda clean -y --all",
41
- "conda install -y nvidia/label/cuda-12.8.1::cuda"
6
+ if (this.kernel.gpu === "nvidia") {
7
+ if (this.kernel.platform === "win32") {
8
+ await this.kernel.bin.exec({
9
+ message: [
10
+ "conda clean -y --all",
11
+ "conda install -y cudnn libzlib-wapi -c conda-forge",
12
+ ]
13
+ }, ondata)
14
+ await this.kernel.bin.exec({
15
+ message: [
16
+ "conda clean -y --all",
17
+ "conda install -y nvidia/label/cuda-12.8.1::cuda"
18
+ ]
19
+ }, ondata)
20
+ const folder = this.kernel.bin.path("miniconda/etc/conda/activate.d")
21
+ let deactivate_list = [
22
+ "~cuda-nvcc_activate.bat",
23
+ "vs2019_compiler_vars.bat",
24
+ "vs2022_compiler_vars.bat",
42
25
  ]
43
- }, ondata)
44
- if (this.kernel.platform === "linux") {
26
+ for(let item of deactivate_list) {
27
+ const old_name = path.resolve(folder, item)
28
+ const new_name = path.resolve(folder, item + ".disabled")
29
+ console.log("rename", { old_name, new_name })
30
+ await fs.promises.rename(old_name, new_name)
31
+ }
32
+ } else {
45
33
  await this.kernel.bin.exec({
46
34
  message: [
47
- "conda install -y -c conda-forge nccl"
35
+ "conda clean -y --all",
36
+ "conda install -y cudnn -c conda-forge",
48
37
  ]
49
38
  }, ondata)
39
+ await this.kernel.bin.exec({
40
+ message: [
41
+ "conda clean -y --all",
42
+ "conda install -y nvidia/label/cuda-12.8.1::cuda"
43
+ ]
44
+ }, ondata)
45
+ if (this.kernel.platform === "linux") {
46
+ await this.kernel.bin.exec({
47
+ message: [
48
+ "conda install -y -c conda-forge nccl"
49
+ ]
50
+ }, ondata)
51
+ }
50
52
  }
51
53
  }
52
54
  }
53
55
  async installed() {
54
- if (this.kernel.platform === 'win32') {
55
- if (this.kernel.bin.installed.conda.has("cudnn") && this.kernel.bin.installed.conda.has("cuda") && this.kernel.bin.installed.conda.has("libzlib-wapi")) {
56
- let version = this.kernel.bin.installed.conda_versions.cuda
57
- if (version) {
58
- let coerced = semver.coerce(version)
59
- console.log("cuda version", coerced)
60
- if (semver.satisfies(coerced, ">=12.8.1")) {
61
- console.log("cuda satisfied")
62
- let deactivate_list = [
63
- "~cuda-nvcc_activate.bat",
64
- "vs2019_compiler_vars.bat",
65
- "vs2022_compiler_vars.bat",
66
- ]
67
- const folder = this.kernel.bin.path("miniconda/etc/conda/activate.d")
68
- let at_least_one_exists = false
69
- for(let item of deactivate_list) {
70
- let exists = await this.kernel.exists("bin/miniconda/etc/conda/activate.d/" + item)
71
- if (exists) {
72
- // break if at least one exists
73
- at_least_one_exists = true
74
- break
56
+ if (this.kernel.gpu === "nvidia") {
57
+ if (this.kernel.platform === 'win32') {
58
+ if (this.kernel.bin.installed.conda.has("cudnn") && this.kernel.bin.installed.conda.has("cuda") && this.kernel.bin.installed.conda.has("libzlib-wapi")) {
59
+ let version = this.kernel.bin.installed.conda_versions.cuda
60
+ if (version) {
61
+ let coerced = semver.coerce(version)
62
+ console.log("cuda version", coerced)
63
+ if (semver.satisfies(coerced, ">=12.8.1")) {
64
+ console.log("cuda satisfied")
65
+ let deactivate_list = [
66
+ "~cuda-nvcc_activate.bat",
67
+ "vs2019_compiler_vars.bat",
68
+ "vs2022_compiler_vars.bat",
69
+ ]
70
+ const folder = this.kernel.bin.path("miniconda/etc/conda/activate.d")
71
+ let at_least_one_exists = false
72
+ for(let item of deactivate_list) {
73
+ let exists = await this.kernel.exists("bin/miniconda/etc/conda/activate.d/" + item)
74
+ if (exists) {
75
+ // break if at least one exists
76
+ at_least_one_exists = true
77
+ break
78
+ }
79
+ }
80
+ console.log("nvcc_activate exists?", at_least_one_exists)
81
+ if (at_least_one_exists) {
82
+ return false
83
+ } else {
84
+ return true
75
85
  }
76
86
  }
77
- console.log("nvcc_activate exists?", at_least_one_exists)
78
- if (at_least_one_exists) {
79
- return false
80
- } else {
87
+ }
88
+ }
89
+ } else {
90
+ if (this.kernel.bin.installed.conda.has("cudnn") && this.kernel.bin.installed.conda.has("cuda")) {
91
+ let version = this.kernel.bin.installed.conda_versions.cuda
92
+ if (version) {
93
+ let coerced = semver.coerce(version)
94
+ console.log("cuda version", coerced)
95
+ if (semver.satisfies(coerced, ">=12.8.1")) {
96
+ console.log("satisfied")
81
97
  return true
82
98
  }
83
99
  }
84
100
  }
85
101
  }
102
+ return false
86
103
  } else {
87
- if (this.kernel.bin.installed.conda.has("cudnn") && this.kernel.bin.installed.conda.has("cuda")) {
88
- let version = this.kernel.bin.installed.conda_versions.cuda
89
- if (version) {
90
- let coerced = semver.coerce(version)
91
- console.log("cuda version", coerced)
92
- if (semver.satisfies(coerced, ">=12.8.1")) {
93
- console.log("satisfied")
94
- return true
95
- }
96
- }
97
- }
104
+ // just return true for all other gpus so they can be avoided
105
+ return true
98
106
  }
99
- return false
100
107
  }
101
108
  env() {
102
109
  return {
@@ -157,12 +157,14 @@ module.exports = {
157
157
  { name: "cli", },
158
158
  { name: "uv", },
159
159
  { name: "py", },
160
+ { name: "huggingface" },
160
161
  { name: "browserless" },
161
162
  ])
162
163
  let conda_requirements = [
163
164
  zip_cmd,
164
165
  "uv",
165
166
  "node",
167
+ "huggingface",
166
168
  "git",
167
169
  ]
168
170
  return {
@@ -109,6 +109,19 @@ class Proto {
109
109
  let mod = await this.kernel.require(mod_path)
110
110
  let response = await mod(payload, ondata, this.kernel)
111
111
 
112
+ if (projectType === 'dns') {
113
+ try {
114
+ await this.kernel.dns({ path: payload.cwd })
115
+ } catch (dnsError) {
116
+ console.log('[proto] dns update failed', dnsError)
117
+ }
118
+ try {
119
+ await this.kernel.refresh(true)
120
+ } catch (refreshError) {
121
+ console.log('[proto] refresh failed after dns create', refreshError)
122
+ }
123
+ }
124
+
112
125
  // // copy readme
113
126
  // let readme_path = this.kernel.path("prototype/PINOKIO.md")
114
127
  // await fs.promises.cp(readme_path, path.resolve(cwd, name, "PINOKIO.md"))
@@ -38,6 +38,7 @@ class Router {
38
38
  this.local_network_mapping = {}
39
39
  this.custom_routers = {}
40
40
  this.rewrite_mapping = {}
41
+ this.stream_close_delay = '10m'
41
42
  }
42
43
  async init() {
43
44
  // if ~/pinokio/network doesn't exist, clone
@@ -258,6 +259,14 @@ class Router {
258
259
  }
259
260
  }
260
261
  this.mapping = this._mapping
262
+
263
+ // set self origins => used for detecting all IPs resembling pinokiod itself
264
+ const basePort = Number(this.kernel.server_port || this.default_port)
265
+ const mappedPort = this.port_mapping && basePort ? Number(this.port_mapping[String(basePort)]) : null
266
+ const lanHost = (this.kernel.peer && this.kernel.peer.host) ? String(this.kernel.peer.host).trim() : ''
267
+ const hosts = ['127.0.0.1', 'localhost', lanHost].filter(Boolean)
268
+ const ports = [basePort, mappedPort].filter((value) => Number.isFinite(value))
269
+ this.kernel.selfOrigins = hosts.flatMap((host) => ports.map((port) => `${host}:${port}`))
261
270
  }
262
271
 
263
272
  fallback() {
@@ -299,8 +308,40 @@ class Router {
299
308
  this.mapping = this._mapping
300
309
  }
301
310
 
311
+ ensureStreamCloseDelay(target) {
312
+ const delay = this.stream_close_delay
313
+ if (!delay || !target) {
314
+ return
315
+ }
316
+ const seen = new WeakSet()
317
+ const visit = (node) => {
318
+ if (!node || (typeof node === 'object' && seen.has(node))) {
319
+ return
320
+ }
321
+ if (typeof node === 'object') {
322
+ seen.add(node)
323
+ }
324
+ if (Array.isArray(node)) {
325
+ for (const item of node) {
326
+ visit(item)
327
+ }
328
+ return
329
+ }
330
+ if (typeof node === 'object') {
331
+ if (node.handler === 'reverse_proxy' && typeof node.stream_close_delay === 'undefined') {
332
+ node.stream_close_delay = delay
333
+ }
334
+ for (const key of Object.keys(node)) {
335
+ visit(node[key])
336
+ }
337
+ }
338
+ }
339
+ visit(target)
340
+ }
341
+
302
342
  // update caddy config
303
343
  async update() {
344
+ this.ensureStreamCloseDelay(this.config)
304
345
  if (JSON.stringify(this.config) === JSON.stringify(this.old_config)) {
305
346
  // console.log("######### config hasn't updated")
306
347
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.231.0",
3
+ "version": "3.232.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -441,6 +441,7 @@ class Server {
441
441
  //browser_url = "/pinokio/browser/" + x.name
442
442
  browser_url = "/p/" + x.name
443
443
  }
444
+ let view_url = "/v/" + x.name
444
445
  let dev_url = browser_url + "/dev"
445
446
  let review_url = browser_url + "/review"
446
447
  let files_url = "/asset/api/" + x.name
@@ -469,6 +470,7 @@ class Server {
469
470
  url: browser_url,
470
471
  path: uri,
471
472
  dev_url,
473
+ view_url,
472
474
  review_url,
473
475
  files_url,
474
476
  }
@@ -703,16 +705,14 @@ class Server {
703
705
  // return current_urls
704
706
  }
705
707
 
706
- async chrome(req, res, type) {
708
+ async chrome(req, res, type, options) {
707
709
 
708
710
  let d = Date.now()
709
711
  let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
710
- //bin: this.kernel.bin.preset("dev"),
711
- bin: this.kernel.bin.preset("ai"),
712
+ bin: this.kernel.bin.preset("dev"),
712
713
  })
713
714
  if (!requirements_pending && install_required) {
714
- //res.redirect(`/setup/dev?callback=${req.originalUrl}`)
715
- res.redirect(`/setup/ai?callback=${req.originalUrl}`)
715
+ res.redirect(`/setup/dev?callback=${req.originalUrl}`)
716
716
  return
717
717
  }
718
718
 
@@ -850,7 +850,20 @@ class Server {
850
850
  dev_link = "/d/" + posix_path
851
851
  }
852
852
 
853
- let run_tab = "/p/" + name
853
+ let autoselect
854
+ let run_tab
855
+ if (type === "run") {
856
+ if (options && options.no_autoselect) {
857
+ run_tab = "/v/" + name
858
+ autoselect = false
859
+ } else {
860
+ run_tab = "/p/" + name
861
+ autoselect = true
862
+ }
863
+ } else {
864
+ run_tab = "/p/" + name
865
+ autoselect = false
866
+ }
854
867
  let dev_tab = "/p/" + name + "/dev"
855
868
  let review_tab = "/p/" + name + "/review"
856
869
  let files_tab = "/p/" + name + "/files"
@@ -889,6 +902,7 @@ class Server {
889
902
  port: this.port,
890
903
  // mem,
891
904
  type,
905
+ autoselect,
892
906
  platform,
893
907
  running:this.kernel.api.running,
894
908
  memory: this.kernel.memory,
@@ -1677,12 +1691,29 @@ class Server {
1677
1691
  template = "editor"
1678
1692
  }
1679
1693
 
1694
+ let requires_bundle = null
1695
+ if (resolved && resolved.requires && !Array.isArray(resolved.requires)) {
1696
+ const bundle = resolved.requires.bundle
1697
+ if (typeof bundle === "string" && typeof Setup[bundle] === "function") {
1698
+ requires_bundle = bundle
1699
+ }
1700
+ }
1701
+
1702
+ const preset = requires_bundle ? this.kernel.bin.preset(requires_bundle) : this.kernel.bin.preset("dev")
1680
1703
  let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
1681
- //bin: this.kernel.bin.preset("ai"),
1682
- bin: this.kernel.bin.preset("dev"),
1704
+ bin: preset,
1683
1705
  script: resolved
1684
1706
  })
1685
1707
 
1708
+ if (requires_bundle) {
1709
+ console.log({ requires_bundle, requirements_pending, install_required, })
1710
+ }
1711
+
1712
+ if (requires_bundle && !requirements_pending && install_required) {
1713
+ res.redirect(`/setup/${requires_bundle}?callback=${req.originalUrl}`)
1714
+ return
1715
+ }
1716
+
1686
1717
  //let requirements = this.kernel.bin.requirements(resolved)
1687
1718
  //let requirements_pending = !this.kernel.bin.installed_initialized
1688
1719
  //let install_required = true
@@ -3368,7 +3399,7 @@ class Server {
3368
3399
  ]
3369
3400
  pushEntry({
3370
3401
  host: hostMeta,
3371
- name: `[Files] ${rewrite.name || key}`,
3402
+ name: `[Website] ${rewrite.name || key}`,
3372
3403
  ip: externalIp || null,
3373
3404
  httpUrl: externalIp,
3374
3405
  httpsUrls: Array.from(new Set(httpsSources))
@@ -3401,26 +3432,26 @@ class Server {
3401
3432
  })
3402
3433
  }
3403
3434
 
3404
- const installedApps = Array.isArray(hostInfo.installed) ? hostInfo.installed : []
3405
- for (const app of installedApps) {
3406
- if (!app) {
3407
- continue
3408
- }
3409
- const httpHref = Array.isArray(app.http_href) ? app.http_href[0] : app.http_href
3410
- const httpsCandidates = Array.from(new Set([
3411
- ...ensureArray(app.app_href),
3412
- ...ensureArray(app.https_href)
3413
- ]))
3414
- pushEntry({
3415
- host: hostMeta,
3416
- name: app.title || app.name || app.folder,
3417
- ip: httpHref ? httpHref.replace(/^https?:\/\//i, '') : null,
3418
- httpUrl: httpHref || null,
3419
- httpsUrls: httpsCandidates,
3420
- description: app.description,
3421
- icon: app.https_icon || app.http_icon || app.icon
3422
- })
3423
- }
3435
+ // const installedApps = Array.isArray(hostInfo.installed) ? hostInfo.installed : []
3436
+ // for (const app of installedApps) {
3437
+ // if (!app) {
3438
+ // continue
3439
+ // }
3440
+ // const httpHref = Array.isArray(app.http_href) ? app.http_href[0] : app.http_href
3441
+ // const httpsCandidates = Array.from(new Set([
3442
+ // ...ensureArray(app.app_href),
3443
+ // ...ensureArray(app.https_href)
3444
+ // ]))
3445
+ // pushEntry({
3446
+ // host: hostMeta,
3447
+ // name: app.title || app.name || app.folder,
3448
+ // ip: httpHref ? httpHref.replace(/^https?:\/\//i, '') : null,
3449
+ // httpUrl: httpHref || null,
3450
+ // httpsUrls: httpsCandidates,
3451
+ // description: app.description,
3452
+ // icon: app.https_icon || app.http_icon || app.icon
3453
+ // })
3454
+ // }
3424
3455
  }
3425
3456
  }
3426
3457
  async terminals(filepath) {
@@ -3728,6 +3759,7 @@ class Server {
3728
3759
 
3729
3760
 
3730
3761
  await this.kernel.init({ port: this.port})
3762
+ this.kernel.server_port = this.port
3731
3763
  this.kernel.peer.start(this.kernel)
3732
3764
 
3733
3765
 
@@ -5494,6 +5526,18 @@ class Server {
5494
5526
  requirements_pending,
5495
5527
  })
5496
5528
  }))
5529
+ this.app.get("/net/:name/diff", ex(async (req, res) => {
5530
+ try {
5531
+ let processes = this.kernel.peer.info[this.kernel.peer.host].router_info
5532
+ let last_proc = JSON.stringify(this.kernel.last_processes)
5533
+ let current_proc = JSON.stringify(processes)
5534
+ this.kernel.last_processes = processes
5535
+ res.json({ diff: last_proc !== current_proc })
5536
+ } catch (e) {
5537
+ console.log("ERROR", e)
5538
+ res.json({ diff: true })
5539
+ }
5540
+ }))
5497
5541
  this.app.get("/net/:name", ex(async (req, res) => {
5498
5542
  let protocol = req.get('X-Forwarded-Proto') || "http"
5499
5543
  let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
@@ -5547,6 +5591,7 @@ class Server {
5547
5591
  const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
5548
5592
  let peer_qr = null
5549
5593
  try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
5594
+ const allow_dns_creation = req.params.name === this.kernel.peer.name
5550
5595
  res.render("net", {
5551
5596
  static_routes,
5552
5597
  selected_name: req.params.name,
@@ -5567,6 +5612,8 @@ class Server {
5567
5612
  current_host: this.kernel.peer.host,
5568
5613
  peer_url,
5569
5614
  peer_qr,
5615
+ cwd: this.kernel.path("api"),
5616
+ allow_dns_creation,
5570
5617
  })
5571
5618
  }))
5572
5619
  this.app.get("/network", ex(async (req, res) => {
@@ -7103,7 +7150,13 @@ class Server {
7103
7150
  return hosts[0]
7104
7151
  }
7105
7152
 
7106
- const info = this.kernel.processes.info.map((item) => {
7153
+ const processes = Array.isArray(this.kernel.processes.info) ? this.kernel.processes.info : []
7154
+ const serverPid = Number(process.pid)
7155
+ const filteredProcesses = Number.isFinite(serverPid)
7156
+ ? processes.filter((item) => Number(item && item.pid) !== serverPid)
7157
+ : processes.slice()
7158
+
7159
+ let info = filteredProcesses.map((item) => {
7107
7160
  const httpUrl = item.ip ? `http://${item.ip}` : null
7108
7161
  let httpsHosts = []
7109
7162
  if (preferHttps) {
@@ -7149,6 +7202,12 @@ class Server {
7149
7202
 
7150
7203
  this.add_extra_urls(info)
7151
7204
 
7205
+ if (Array.isArray(this.kernel.selfOrigins) && this.kernel.selfOrigins.length > 0) {
7206
+ info = info.filter((entry) => {
7207
+ return !this.kernel.selfOrigins.includes(entry.ip)
7208
+ })
7209
+ }
7210
+
7152
7211
  const toArray = (value) => {
7153
7212
  if (!value) return []
7154
7213
  return Array.isArray(value) ? value.filter(Boolean) : [value].filter(Boolean)
@@ -7429,7 +7488,9 @@ class Server {
7429
7488
  this.app.get("/pinokio/browser/:name", ex(async (req, res) => {
7430
7489
  await this.chrome(req, res, "run")
7431
7490
  }))
7432
-
7491
+ this.app.get("/v/:name", ex(async (req, res) => {
7492
+ await this.chrome(req, res, "run", { no_autoselect: true })
7493
+ }))
7433
7494
 
7434
7495
  this.app.get("/p/:name/review", ex(async (req, res) => {
7435
7496
  let gitRemote = null
@@ -0,0 +1,115 @@
1
+ (function () {
2
+ const api = window.PinokioTabLinkPopover
3
+ if (!api) {
4
+ return
5
+ }
6
+ const { renderTabLinkPopover, hideTabLinkPopover, isLocalHostLike } = api
7
+
8
+ const getPopoverEl = () => document.getElementById('tab-link-popover')
9
+
10
+ const ensureHttpUrl = (value) => {
11
+ if (typeof value !== 'string') {
12
+ return ''
13
+ }
14
+ let trimmed = value.trim()
15
+ if (!trimmed) {
16
+ return ''
17
+ }
18
+ if (!/^https?:\/\//i.test(trimmed)) {
19
+ if (/^[a-z]+:\/\//i.test(trimmed)) {
20
+ return ''
21
+ }
22
+ trimmed = `http://${trimmed}`
23
+ }
24
+ try {
25
+ const parsed = new URL(trimmed)
26
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
27
+ return ''
28
+ }
29
+ return parsed.toString()
30
+ } catch (_) {
31
+ return ''
32
+ }
33
+ }
34
+
35
+ const isLocalUrl = (value) => {
36
+ if (!value) {
37
+ return false
38
+ }
39
+ try {
40
+ const parsed = new URL(value)
41
+ return isLocalHostLike(parsed.hostname)
42
+ } catch (_) {
43
+ return false
44
+ }
45
+ }
46
+
47
+ const resolveCurrentUrl = (input) => {
48
+ if (input && typeof input.value === 'string' && input.value.trim().length > 0) {
49
+ const normalized = ensureHttpUrl(input.value)
50
+ if (normalized) {
51
+ return normalized
52
+ }
53
+ }
54
+ const fallback = input?.getAttribute('value')
55
+ return ensureHttpUrl(fallback || '')
56
+ }
57
+
58
+ const showPopoverForAnchor = (anchor, urlInput) => {
59
+ if (!anchor) {
60
+ return
61
+ }
62
+ const currentUrl = resolveCurrentUrl(urlInput)
63
+ if (!currentUrl || !isLocalUrl(currentUrl)) {
64
+ hideTabLinkPopover({ immediate: true })
65
+ return
66
+ }
67
+ renderTabLinkPopover(anchor, {
68
+ hrefOverride: currentUrl,
69
+ requireAlternate: false,
70
+ restrictToBase: true,
71
+ forceCanonicalQr: true,
72
+ allowQrPortMismatch: true,
73
+ skipPeerFallback: true
74
+ })
75
+ }
76
+
77
+ const handleMouseLeave = (anchor, event) => {
78
+ const related = event.relatedTarget
79
+ const popover = getPopoverEl()
80
+ if (related && (anchor.contains(related) || (popover && popover.contains(related)))) {
81
+ return
82
+ }
83
+ hideTabLinkPopover()
84
+ }
85
+
86
+ const init = () => {
87
+ const container = document.querySelector('.url-input-container')
88
+ const urlInput = container ? container.querySelector('input[type="url"]') : null
89
+ const mobileButton = document.getElementById('mobile-link-button')
90
+
91
+ if (container) {
92
+ container.addEventListener('mouseover', () => showPopoverForAnchor(container, urlInput))
93
+ container.addEventListener('mouseout', (event) => handleMouseLeave(container, event))
94
+ const inputFocus = () => showPopoverForAnchor(container, urlInput)
95
+ const inputBlur = (event) => handleMouseLeave(container, event)
96
+ if (urlInput) {
97
+ urlInput.addEventListener('focus', inputFocus)
98
+ urlInput.addEventListener('blur', inputBlur)
99
+ }
100
+ }
101
+
102
+ if (mobileButton) {
103
+ mobileButton.addEventListener('mouseover', () => showPopoverForAnchor(mobileButton, urlInput))
104
+ mobileButton.addEventListener('mouseout', (event) => handleMouseLeave(mobileButton, event))
105
+ mobileButton.addEventListener('focus', () => showPopoverForAnchor(mobileButton, urlInput))
106
+ mobileButton.addEventListener('blur', (event) => handleMouseLeave(mobileButton, event))
107
+ }
108
+ }
109
+
110
+ if (document.readyState === 'loading') {
111
+ document.addEventListener('DOMContentLoaded', init)
112
+ } else {
113
+ init()
114
+ }
115
+ })()
@@ -2816,3 +2816,7 @@ header.navheader.minimized .home .icon {
2816
2816
  header.navheader.transitioning {
2817
2817
  pointer-events: none;
2818
2818
  }
2819
+
2820
+ .shutdown i.fa-stop:hover {
2821
+ color: crimson;
2822
+ }