pinokiod 3.230.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.230.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,12 +705,11 @@ 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
715
  res.redirect(`/setup/dev?callback=${req.originalUrl}`)
@@ -849,7 +850,20 @@ class Server {
849
850
  dev_link = "/d/" + posix_path
850
851
  }
851
852
 
852
- 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
+ }
853
867
  let dev_tab = "/p/" + name + "/dev"
854
868
  let review_tab = "/p/" + name + "/review"
855
869
  let files_tab = "/p/" + name + "/files"
@@ -888,6 +902,7 @@ class Server {
888
902
  port: this.port,
889
903
  // mem,
890
904
  type,
905
+ autoselect,
891
906
  platform,
892
907
  running:this.kernel.api.running,
893
908
  memory: this.kernel.memory,
@@ -1676,12 +1691,29 @@ class Server {
1676
1691
  template = "editor"
1677
1692
  }
1678
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")
1679
1703
  let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
1680
- //bin: this.kernel.bin.preset("ai"),
1681
- bin: this.kernel.bin.preset("dev"),
1704
+ bin: preset,
1682
1705
  script: resolved
1683
1706
  })
1684
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
+
1685
1717
  //let requirements = this.kernel.bin.requirements(resolved)
1686
1718
  //let requirements_pending = !this.kernel.bin.installed_initialized
1687
1719
  //let install_required = true
@@ -3367,7 +3399,7 @@ class Server {
3367
3399
  ]
3368
3400
  pushEntry({
3369
3401
  host: hostMeta,
3370
- name: `[Files] ${rewrite.name || key}`,
3402
+ name: `[Website] ${rewrite.name || key}`,
3371
3403
  ip: externalIp || null,
3372
3404
  httpUrl: externalIp,
3373
3405
  httpsUrls: Array.from(new Set(httpsSources))
@@ -3400,26 +3432,26 @@ class Server {
3400
3432
  })
3401
3433
  }
3402
3434
 
3403
- const installedApps = Array.isArray(hostInfo.installed) ? hostInfo.installed : []
3404
- for (const app of installedApps) {
3405
- if (!app) {
3406
- continue
3407
- }
3408
- const httpHref = Array.isArray(app.http_href) ? app.http_href[0] : app.http_href
3409
- const httpsCandidates = Array.from(new Set([
3410
- ...ensureArray(app.app_href),
3411
- ...ensureArray(app.https_href)
3412
- ]))
3413
- pushEntry({
3414
- host: hostMeta,
3415
- name: app.title || app.name || app.folder,
3416
- ip: httpHref ? httpHref.replace(/^https?:\/\//i, '') : null,
3417
- httpUrl: httpHref || null,
3418
- httpsUrls: httpsCandidates,
3419
- description: app.description,
3420
- icon: app.https_icon || app.http_icon || app.icon
3421
- })
3422
- }
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
+ // }
3423
3455
  }
3424
3456
  }
3425
3457
  async terminals(filepath) {
@@ -3727,6 +3759,7 @@ class Server {
3727
3759
 
3728
3760
 
3729
3761
  await this.kernel.init({ port: this.port})
3762
+ this.kernel.server_port = this.port
3730
3763
  this.kernel.peer.start(this.kernel)
3731
3764
 
3732
3765
 
@@ -5493,6 +5526,18 @@ class Server {
5493
5526
  requirements_pending,
5494
5527
  })
5495
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
+ }))
5496
5541
  this.app.get("/net/:name", ex(async (req, res) => {
5497
5542
  let protocol = req.get('X-Forwarded-Proto') || "http"
5498
5543
  let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
@@ -5546,6 +5591,7 @@ class Server {
5546
5591
  const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
5547
5592
  let peer_qr = null
5548
5593
  try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
5594
+ const allow_dns_creation = req.params.name === this.kernel.peer.name
5549
5595
  res.render("net", {
5550
5596
  static_routes,
5551
5597
  selected_name: req.params.name,
@@ -5566,6 +5612,8 @@ class Server {
5566
5612
  current_host: this.kernel.peer.host,
5567
5613
  peer_url,
5568
5614
  peer_qr,
5615
+ cwd: this.kernel.path("api"),
5616
+ allow_dns_creation,
5569
5617
  })
5570
5618
  }))
5571
5619
  this.app.get("/network", ex(async (req, res) => {
@@ -7102,7 +7150,13 @@ class Server {
7102
7150
  return hosts[0]
7103
7151
  }
7104
7152
 
7105
- 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) => {
7106
7160
  const httpUrl = item.ip ? `http://${item.ip}` : null
7107
7161
  let httpsHosts = []
7108
7162
  if (preferHttps) {
@@ -7148,6 +7202,12 @@ class Server {
7148
7202
 
7149
7203
  this.add_extra_urls(info)
7150
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
+
7151
7211
  const toArray = (value) => {
7152
7212
  if (!value) return []
7153
7213
  return Array.isArray(value) ? value.filter(Boolean) : [value].filter(Boolean)
@@ -7428,7 +7488,9 @@ class Server {
7428
7488
  this.app.get("/pinokio/browser/:name", ex(async (req, res) => {
7429
7489
  await this.chrome(req, res, "run")
7430
7490
  }))
7431
-
7491
+ this.app.get("/v/:name", ex(async (req, res) => {
7492
+ await this.chrome(req, res, "run", { no_autoselect: true })
7493
+ }))
7432
7494
 
7433
7495
  this.app.get("/p/:name/review", ex(async (req, res) => {
7434
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
+ }