pinokiod 3.20.18 → 3.20.20

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.
@@ -0,0 +1,56 @@
1
+ const axios = require('axios')
2
+ const { URL } = require("url");
3
+ const { JSDOM } = require("jsdom");
4
+
5
+ class Favicon {
6
+ async get(pageUrl) {
7
+ let origin;
8
+ try {
9
+ origin = new URL(pageUrl).origin;
10
+ } catch {
11
+ throw new Error("Invalid URL: " + pageUrl);
12
+ }
13
+
14
+ const candidates = [
15
+ "/favicon.ico",
16
+ "/favicon.png",
17
+ "/assets/favicon.ico",
18
+ "/static/favicon.ico",
19
+ "/favicon.svg",
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;
26
+ }
27
+
28
+ // 2. Fallback to parsing HTML
29
+ try {
30
+ const res = await axios.get(pageUrl, { timeout: 1000 });
31
+ const dom = new JSDOM(res.data);
32
+ const icons = Array.from(dom.window.document.querySelectorAll("link[rel~='icon'], link[rel='apple-touch-icon']"));
33
+
34
+ for (const icon of icons) {
35
+ const href = icon.getAttribute("href");
36
+ if (!href) continue;
37
+ const resolvedUrl = new URL(href, origin).href;
38
+ if (await this.checkImageUrl(resolvedUrl)) return resolvedUrl;
39
+ }
40
+ } catch {
41
+ // Ignore HTML errors
42
+ }
43
+
44
+ return null;
45
+ }
46
+
47
+ async checkImageUrl(url) {
48
+ try {
49
+ const res = await axios.head(url, { timeout: 3000 });
50
+ return res.status === 200 && res.headers["content-type"]?.startsWith("image/");
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+ }
56
+ module.exports = Favicon
package/kernel/index.js CHANGED
@@ -37,6 +37,7 @@ const Procs = require('./procs')
37
37
  const Peer = require('./peer')
38
38
  const Git = require('./git')
39
39
  const Connect = require('./connect')
40
+ const Favicon = require('./favicon')
40
41
  const { DownloaderHelper } = require('node-downloader-helper');
41
42
  const { ProxyAgent } = require('proxy-agent');
42
43
  const fakeUa = require('fake-useragent');
@@ -83,6 +84,7 @@ class Kernel {
83
84
  this.exposed = {}
84
85
  this.envs = {}
85
86
  this.shellpath = shellPath.sync()
87
+ this.favicon = new Favicon()
86
88
 
87
89
 
88
90
  }
@@ -76,7 +76,6 @@ class Proto {
76
76
  let mod = await this.kernel.require(mod_path)
77
77
 
78
78
  console.log({ mod_path, projectType, startType })
79
- console.log("payload", payload)
80
79
 
81
80
  await mod(payload, ondata, this.kernel)
82
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.20.18",
3
+ "version": "3.20.20",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -659,6 +659,12 @@ class Server {
659
659
  }
660
660
  return shell_id
661
661
  }
662
+ is_subpath(parent, child) {
663
+ const relative = path.relative(parent, child);
664
+ let check = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
665
+ console.log({ relative, parent, child, check })
666
+ return check
667
+ }
662
668
  async render(req, res, pathComponents, meta) {
663
669
  let base_path = req.base || this.kernel.path("api")
664
670
  let full_filepath = path.resolve(base_path, ...pathComponents)
@@ -1326,35 +1332,6 @@ class Server {
1326
1332
  let p = path.resolve(uri, item.name, "pinokio.js")
1327
1333
  let config = (await this.kernel.loader.load(p)).resolved
1328
1334
  if (config) {
1329
- /*
1330
- if (config.version) {
1331
- let coerced = semver.coerce(config.version)
1332
- console.log("version", { coerced, v: config.version })
1333
- if (semver.satisfies(coerced, this.kernel.schema)) {
1334
- console.log("semver satisfied", config.version, this.kernel.schema)
1335
- } else {
1336
- console.log("semver NOT satisfied", config.version, this.kernel.schema)
1337
- error = `Please update Pinokio to the latest version (current script version: ${config.version}, supported: ${this.kernel.schema}`
1338
- }
1339
- }
1340
- */
1341
- // if (config.run) {
1342
- // items[i].run = config.run
1343
- // }
1344
- // if (config.menu) {
1345
- // if (typeof config.menu === "function") {
1346
- // if (config.menu.constructor.name === "AsyncFunction") {
1347
- // config.menu = await config.menu(this.kernel, this.kernel.info)
1348
- // } else {
1349
- // config.menu = config.menu(this.kernel, this.kernel.info)
1350
- // }
1351
- // }
1352
- //
1353
- // await this.renderMenu(uri, item.name, config, pathComponents)
1354
- //
1355
- // items[i].menu = config.menu
1356
- // }
1357
-
1358
1335
  if (config.shortcuts) {
1359
1336
  if (typeof config.shortcuts === "function") {
1360
1337
  if (config.shortcuts.constructor.name === "AsyncFunction") {
@@ -1372,36 +1349,35 @@ class Server {
1372
1349
  if (config && config.type === "lib") {
1373
1350
  continue
1374
1351
  }
1375
-
1376
- // // if there's a run clause, do not display on the home page
1377
- // if (config && config.run && Array.isArray(config.run)) {
1378
- // continue
1379
- // }
1380
-
1381
-
1382
1352
  // check if there is a running process with this folder name
1383
1353
  let runningApps = new Set()
1384
1354
  for(let key in this.kernel.api.running) {
1385
1355
  //let p = this.kernel.path("api", items[i].name) + path.sep
1386
1356
  let p = this.kernel.path("api", items[i].name)
1387
1357
 
1388
- //let re = new RegExp(items[i].name)
1389
- //if (re.test(key)) {
1390
- //if (p === key) {
1391
-
1392
-
1393
1358
  // not only should include the pattern, but also end with it (otherwise can include similar patterns such as /api/qqqa, /api/qqqaaa, etc.
1394
1359
 
1395
1360
  let is_running
1396
1361
  let api_path = this.kernel.path("api")
1397
- if (key.startsWith(api_path)) {
1398
- // api script
1399
- if (key.startsWith(p)) {
1362
+ if (this.is_subpath(api_path, key)) {
1363
+ if (this.is_subpath(p, key)) {
1400
1364
  is_running = true
1401
1365
  }
1402
1366
  } else {
1403
1367
  if (key.endsWith(p)) {
1404
1368
  is_running = true
1369
+ } else {
1370
+ if (!path.isAbsolute(key)) {
1371
+ let chunks = key.split("_")
1372
+ if (chunks.length > 1) {
1373
+ let folder = chunks[0]
1374
+ /// if the folder name matches, it's running
1375
+ if (folder === items[i].name) {
1376
+ is_running = true
1377
+ }
1378
+ }
1379
+
1380
+ }
1405
1381
  }
1406
1382
  }
1407
1383
  // 1. if the script path starts with api path => api script
@@ -1412,34 +1388,46 @@ class Server {
1412
1388
 
1413
1389
  //if (key.includes(p) && key.endsWith(p)) {
1414
1390
  if (is_running) {
1415
- items[i].running = true
1416
- items[i].index = index
1391
+ // add to running
1392
+ running.push(items[i])
1417
1393
  if (!items[i].running_scripts) {
1418
1394
  items[i].running_scripts = []
1419
1395
  }
1420
- if (key.startsWith(p)) {
1421
- items[i].running_scripts.push({ path: path.relative(this.kernel.homedir, key), name: path.relative(p, key) })
1422
- } else {
1423
- let chunks = key.split("?")
1424
- let dev = chunks[0]
1425
- let name_chunks = dev.split(path.sep)
1426
- let name = name_chunks[name_chunks.length-1]
1427
- items[i].running_scripts.push({ id: key, name })
1428
- }
1429
- index++;
1430
- running.push(items[i])
1431
- // break
1432
- }
1433
- let shell = this.kernel.shell.find({
1434
- filter: (shell) => {
1435
- return shell.id.startsWith(items[i].name + "_")
1436
- }
1437
- })
1438
- if (shell.length > 0) {
1439
1396
  items[i].running = true
1440
1397
  items[i].index = index
1398
+
1399
+ // add the running script to running_scripts array
1400
+ // 1. normal api script
1401
+ if (path.isAbsolute(key)) {
1402
+ // script
1403
+ if (this.is_subpath(api_path, key)) {
1404
+ // scripts inside api folder
1405
+ if (this.is_subpath(p, key)) {
1406
+ items[i].running_scripts.push({ path: path.relative(this.kernel.homedir, key), name: path.relative(p, key) })
1407
+ }
1408
+ } else {
1409
+ // other global scripts
1410
+ let chunks = key.split("?")
1411
+ let dev = chunks[0]
1412
+ let name_chunks = dev.split(path.sep)
1413
+ let name = name_chunks[name_chunks.length-1]
1414
+ items[i].running_scripts.push({ id: key, name })
1415
+ }
1416
+ } else {
1417
+ let shell = this.kernel.shell.find({
1418
+ filter: (shell) => {
1419
+ return shell.id.startsWith(items[i].name + "_")
1420
+ }
1421
+ })
1422
+ if (shell.length > 0) {
1423
+ items[i].running = true
1424
+ items[i].index = index
1425
+ for(let sh of shell) {
1426
+ items[i].running_scripts.push({ id: sh.id, name: "Terminal", type: "shell" })
1427
+ }
1428
+ }
1429
+ }
1441
1430
  index++;
1442
- running.push(items[i])
1443
1431
  }
1444
1432
  }
1445
1433
  if (!items[i].running) {
@@ -1447,8 +1435,6 @@ class Server {
1447
1435
  index++;
1448
1436
  notRunning.push(items[i])
1449
1437
  }
1450
-
1451
-
1452
1438
  }
1453
1439
  }
1454
1440
 
@@ -1567,9 +1553,13 @@ class Server {
1567
1553
 
1568
1554
  let current_urls = await this.current_urls()
1569
1555
 
1556
+ let list = this.getPeerInfo()
1557
+
1570
1558
  if (meta) {
1571
1559
  items = running.concat(notRunning)
1572
1560
  res.render("index", {
1561
+ list,
1562
+ current_host: this.kernel.peer.host,
1573
1563
  current_urls,
1574
1564
  portal: this.portal,
1575
1565
  install: this.install,
@@ -3715,6 +3705,98 @@ class Server {
3715
3705
  requirements_pending,
3716
3706
  })
3717
3707
  }))
3708
+ this.app.get("/net/:name", ex(async (req, res) => {
3709
+ let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
3710
+ bin: this.kernel.bin.preset("network"),
3711
+ })
3712
+
3713
+ if (!requirements_pending && install_required) {
3714
+ console.log("redirect to /setup/network")
3715
+ res.redirect("/setup/network?callback=/network")
3716
+ return
3717
+ }
3718
+
3719
+ let list = this.getPeerInfo()
3720
+ let processes
3721
+ let host
3722
+ let peer
3723
+ for(let item of list) {
3724
+ if (item.name === req.params.name) {
3725
+ processes = item.processes
3726
+ host = item.host
3727
+ console.log("matched", processes)
3728
+ peer = item
3729
+ }
3730
+ }
3731
+ let favicons = {}
3732
+ let titles = {}
3733
+ let descriptions = {}
3734
+ console.time("Favicon")
3735
+ //await Promise.all(peer.processes.map((proc) => {
3736
+ // console.log("Proc", proc)
3737
+ // return new Promise(async (resolve, reject) => {
3738
+ // let favicon = await this.kernel.favicon.get("http://" + proc.ip)
3739
+ // console.log("Got favicon", { favicon, ip: proc.ip })
3740
+ // if (favicon) {
3741
+ // favicons[proc.host] = favicon
3742
+ // }
3743
+ // })
3744
+ //}))
3745
+ for(let proc of peer.processes) {
3746
+ if (proc.external_router) {
3747
+ // try to get icons from pinokio
3748
+ for(let router of proc.external_router) {
3749
+ // replace the root domain: facefusion-pinokio.git.x.localhost => facefusion-pinokio.git
3750
+ let pattern = `.${req.params.name}.localhost`
3751
+ if (router.endsWith(pattern)) {
3752
+ let name = router.replace(pattern, "")
3753
+ let api_path = this.kernel.path("api", name)
3754
+ let exists = await this.exists(api_path)
3755
+ if (exists) {
3756
+ let meta = await this.kernel.api.meta(name)
3757
+ if (meta.icon) {
3758
+ favicons[proc.ip] = meta.icon
3759
+ }
3760
+ if (meta.title) {
3761
+ titles[proc.ip] = meta.title
3762
+ }
3763
+ if (meta.description) {
3764
+ descriptions[proc.ip] = meta.description
3765
+ }
3766
+ }
3767
+ }
3768
+ }
3769
+ }
3770
+ // if not running from pinokio, try to fetch and infer the favicon
3771
+ if (!favicons[proc.ip]) {
3772
+ let favicon = await this.kernel.favicon.get("http://" + proc.ip)
3773
+ if (favicon) {
3774
+ favicons[proc.ip] = favicon
3775
+ }
3776
+ }
3777
+ }
3778
+ console.timeEnd("Favicon")
3779
+
3780
+ let current_urls = await this.current_urls(req.originalUrl.slice(1))
3781
+ console.log("LIST", JSON.stringify(list, null, 2))
3782
+ res.render("net", {
3783
+ favicons,
3784
+ titles,
3785
+ descriptions,
3786
+ current_urls,
3787
+ docs: this.docs,
3788
+ portal: this.portal,
3789
+ install: this.install,
3790
+ agent: this.agent,
3791
+ theme: this.theme,
3792
+ processes,
3793
+ error: null,
3794
+ list,
3795
+ host,
3796
+ running: [],
3797
+ current_host: this.kernel.peer.host,
3798
+ })
3799
+ }))
3718
3800
  this.app.get("/network", ex(async (req, res) => {
3719
3801
  let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
3720
3802
  bin: this.kernel.bin.preset("network"),
@@ -3812,8 +3894,10 @@ class Server {
3812
3894
  }
3813
3895
 
3814
3896
 
3897
+ let current_urls = await this.current_urls(req.originalUrl.slice(1))
3815
3898
 
3816
3899
  res.render("network", {
3900
+ current_urls,
3817
3901
  requirements_pending,
3818
3902
  install_required,
3819
3903
  docs: this.docs,
@@ -111,6 +111,9 @@ body.dark *::-webkit-scrollbar-thumb {
111
111
  border: 2px dotted silver !important;
112
112
  }
113
113
  .app-btns {
114
+ display: flex;
115
+ gap: 5px;
116
+ align-items: center;
114
117
  }
115
118
  .drawer {
116
119
  max-height: 100000px;
@@ -151,6 +154,10 @@ body.dark .dynamic.selected {
151
154
  background: rgba(0,0,0,0.05);
152
155
  */
153
156
  }
157
+ .submenu {
158
+ margin-left: 10px;
159
+ box-sizing: border-box;
160
+ }
154
161
 
155
162
  .url-bar {
156
163
  font-family: verdana;
@@ -160,12 +167,12 @@ body.dark .dynamic.selected {
160
167
  flex-grow: 1;
161
168
  }
162
169
  .url-bar .http-url:hover {
163
- color: firebrick;
164
- border-color: firebrick;
170
+ color: royalblue;
171
+ border-color: royalblue;
165
172
  }
166
173
  .url-bar .https-url:hover {
167
- color: firebrick;
168
- border-color: firebrick;
174
+ color: royalblue;
175
+ border-color: royalblue;
169
176
  }
170
177
  .url-bar .https-url {
171
178
  cursor: pointer;
@@ -652,7 +659,13 @@ form.search {
652
659
  margin: 0;
653
660
  display: flex;
654
661
  flex-grow: 1;
662
+ /*
655
663
  padding: 0 10px 20px;
664
+ */
665
+ padding: 0 10px 10px;
666
+ /*
667
+ padding: 10px 20px 10px;
668
+ */
656
669
  }
657
670
  .input-form {
658
671
  padding: 10px;
@@ -720,7 +733,7 @@ body.dark form.search input[type=search] {
720
733
  }
721
734
  form.search input[type=search] {
722
735
  flex-grow: 1;
723
- padding: 10px;
736
+ padding: 8px 10px;
724
737
  /*
725
738
  margin: 0 0 0 10px;
726
739
  */
@@ -733,8 +746,9 @@ form.search input[type=search] {
733
746
  border: 1px solid rgba(0,0,0,0.1);
734
747
  border-right: none;
735
748
  background: rgba(0,0,0,0.05);
736
- */
737
749
  background: white;
750
+ */
751
+ background: rgba(0,0,0,0.05);
738
752
  border: none;
739
753
  outline: none;
740
754
  }
@@ -996,7 +1010,7 @@ header {
996
1010
  background: white;
997
1011
  */
998
1012
  position: sticky;
999
- padding: 10px;
1013
+ padding: 10px 10px 0;
1000
1014
  top: 0;
1001
1015
  box-sizing: border-box;
1002
1016
  z-index: 1000;
@@ -1121,6 +1135,9 @@ main {
1121
1135
  main h1 {
1122
1136
  margin: 0;
1123
1137
  }
1138
+ body.dark .offline .indicator {
1139
+ background: rgba(255,255,255,0.1);
1140
+ }
1124
1141
  .hidden {
1125
1142
  display: none !important;
1126
1143
  }
@@ -1137,6 +1154,9 @@ main h1 {
1137
1154
  box-sizing: border-box;
1138
1155
  }
1139
1156
  .container {
1157
+ flex-grow: 1;
1158
+ box-sizing: border-box;
1159
+ padding: 0 20px;
1140
1160
  /*
1141
1161
  max-width: var(--content-width);
1142
1162
  margin: 20px auto;
@@ -1227,9 +1247,11 @@ body.dark .readme a {
1227
1247
  .line:last-child {
1228
1248
  border: none;
1229
1249
  }
1250
+ /*
1230
1251
  body.dark .line {
1231
1252
  border-bottom: 1px solid var(--dark-thin);
1232
1253
  }
1254
+ */
1233
1255
  body.dark .line.selected, body.dark .line:hover {
1234
1256
  /*
1235
1257
  border-left: 6px solid white;
@@ -1257,7 +1279,9 @@ body.dark .line:hover {
1257
1279
  padding: 15px 15px;
1258
1280
  */
1259
1281
  padding: 7px 5px;
1282
+ /*
1260
1283
  border-bottom: 1px solid var(--light-thin);
1284
+ */
1261
1285
  }
1262
1286
  .thick.line {
1263
1287
  padding: 15px;
@@ -1345,22 +1369,34 @@ body.dark .line.align-top h3 .col .uri {
1345
1369
  .line.align-top h3 .col .uri {
1346
1370
  opacity: 0.9;
1347
1371
  padding-bottom: 2px;
1348
- color: var(--light-fade);
1372
+ color: rgba(0,0,0,0.4);
1349
1373
  font-size: 12px;
1350
1374
  font-family: verdana;
1351
1375
  }
1352
1376
  body.dark .line.align-top h3 .col .title a {
1353
1377
  color: var(--dark-link-color);
1354
1378
  }
1355
- .line.align-top h3 .col .title i {
1356
- font-size: 15px;
1379
+ .line.align-top h3 .col .title span {
1357
1380
  margin-left: 5px;
1358
1381
  }
1382
+
1383
+ .running-apps .line.align-top h3 .col .title {
1384
+ /*
1385
+ color: yellowgreen;
1386
+ */
1387
+ }
1388
+ .running-apps .line.align-top h3 .col .title i {
1389
+ color: yellowgreen;
1390
+ }
1391
+ .line.align-top h3 .col .title i {
1392
+ font-size: 12px;
1393
+ }
1394
+ body.dark .line.align-top h3 .col .title i {
1395
+ }
1359
1396
  .line.align-top h3 .col .title a {
1360
1397
  text-decoration: none;
1361
1398
  display: flex;
1362
1399
  align-items: center;
1363
- color: var(--light-link-color);
1364
1400
  }
1365
1401
  .line.align-top h3 .col .title .btn {
1366
1402
  letter-spacing: 0;
@@ -1376,10 +1412,7 @@ body.dark .line.align-top h3 .col .title a {
1376
1412
  */
1377
1413
  display: flex;
1378
1414
  align-items: center;
1379
- /*
1380
- font-size: 20px;
1381
- */
1382
- font-size: 16px;
1415
+ font-size: 14px;
1383
1416
  font-family: arial;
1384
1417
  font-weight: bold;
1385
1418
  }
@@ -1942,30 +1975,11 @@ nav.error-message {
1942
1975
  font-weight: bold;
1943
1976
  }
1944
1977
  /*
1945
- .not-running-apps, .running-apps {
1946
- background: rgba(0,0,100,0.04);
1947
- }
1948
- body.dark .not-running-apps, body.dark .running-apps {
1949
- background: rgba(255,255,255,0.03);
1950
- }
1951
- */
1952
- /*
1953
- .running-apps .line .title {
1954
- color: #82bc09;
1955
- }
1956
- */
1957
1978
  .running-apps .line {
1958
1979
  border-left: 10px solid #82bc09;
1959
1980
  }
1960
1981
  .running-apps .line img {
1961
1982
  padding-left: 3px;
1962
- /*
1963
- padding-left: 10px;
1964
- border: 3px solid #8fd400;
1965
- */
1966
- /*
1967
- border-radius: 5px;
1968
- */
1969
1983
  }
1970
1984
  body.dark .not-running-apps .line {
1971
1985
  border-left: 10px solid rgba(255,255,255,0.1);
@@ -1976,21 +1990,14 @@ body.dark .not-running-apps .line {
1976
1990
  .not-running-apps .line img {
1977
1991
  padding-left: 3px;
1978
1992
  }
1979
- /*
1980
- .running-apps .line.align-top h3 .col .title {
1981
- color: #8fd400;
1982
- }
1983
- */
1984
1993
  .running-apps .spinner, .not-running-apps .spinner {
1985
1994
  width: 50px;
1986
1995
  text-align: center;
1987
1996
  font-size: 20px;
1988
1997
  }
1998
+ */
1989
1999
  body.dark header .home {
1990
2000
  color: var(--dark-btn-color);
1991
- /*
1992
- color: white;
1993
- */
1994
2001
  }
1995
2002
  header .home {
1996
2003
  color: var(--light-color);
@@ -760,7 +760,7 @@ body.dark .top-menu .btn2.selected {
760
760
  color: white;
761
761
  }
762
762
  body.minimized #collapse {
763
- background: none;
763
+ background: none !important;
764
764
  }
765
765
  body.dark #collapse {
766
766
  background: rgba(255,255,255,0.07);
@@ -776,8 +776,13 @@ body.dark .mode-selector .btn2.selected {
776
776
  }
777
777
  .mode-selector {
778
778
  display: flex;
779
+ gap: 3px;
780
+ /*
781
+ gap: 5px;
782
+ */
779
783
  }
780
784
  .mode-selector .btn2 {
785
+ border-radius: 0;
781
786
  padding: 10px 0;
782
787
  min-width: 40px;
783
788
  }
@@ -900,7 +905,6 @@ body.dark .mode-selector .btn2.selected {
900
905
  <a class='http-url' target="_blank" href="<%=current_urls.http%>"><i class="fa-solid fa-square-arrow-up-right"></i> <%=current_urls.http%></a>
901
906
  <% } %>
902
907
  </div>
903
- <a href="/network" class='btn2'><div><i class="fa-solid fa-wifi"></i></div><div>Network</div></a>
904
908
  <a href="/connect" class='btn2'><div><i class="fa-solid fa-circle-user"></i></div><div>Connect</div></a>
905
909
  <!--
906
910
  <input type='url' id='location' readonly value="<%=execUrl%>">
@@ -196,7 +196,6 @@ pre {
196
196
  <button class='btn2' id='forward'><div><i class="fa-solid fa-chevron-right"></i></div><div>Next</div></button>
197
197
  <button class='btn2' id='refresh-page'><div><i class="fa-solid fa-rotate-right"></i></div><div>Refresh</div></button>
198
198
  <div class='flexible'></div>
199
- <a href="/network" class='btn2'><div><i class="fa-solid fa-wifi"></i></div><div>Network</div></a>
200
199
  <a href="/connect" class='btn2'><div><i class="fa-solid fa-circle-user"></i></div><div>Connect</div></a>
201
200
  <div class='nav-btns'>
202
201
  <a class='btn2' href="<%=portal%>" target="_blank"><div><i class="fa-solid fa-question"></i></div><div>Help</div></a>
@@ -232,7 +232,6 @@ body.dark .config {
232
232
  <button class='btn2' id='forward'><div><i class="fa-solid fa-chevron-right"></i></div><div>Next</div></button>
233
233
  <button class='btn2' id='refresh-page'><div><i class="fa-solid fa-rotate-right"></i></div><div>Refresh</div></button>
234
234
  <div class='flexible'></div>
235
- <a href="/network" class='btn2'><div><i class="fa-solid fa-wifi"></i></div><div>Network</div></a>
236
235
  <a href="/connect" class='btn2'><div><i class="fa-solid fa-circle-user"></i></div><div>Connect</div></a>
237
236
  <div class='nav-btns'>
238
237
  <a class='btn2' href="<%=portal%>" target="_blank"><div><i class="fa-solid fa-question"></i></div><div>Help</div></a>
@@ -115,7 +115,6 @@ body.frozen {
115
115
  <button class='btn2' id='back'><div><i class="fa-solid fa-chevron-left"></i></div><div>Prev</div></button>
116
116
  <button class='btn2' id='forward'><div><i class="fa-solid fa-chevron-right"></i></div><div>Next</div></button>
117
117
  <button class='btn2' id='refresh-page'><div><i class="fa-solid fa-rotate-right"></i></div><div>Refresh</div></button>
118
- <a href="/network" class='btn2'><div><i class="fa-solid fa-wifi"></i></div><div>Network</div></a>
119
118
  <a href="/connect" class='btn2'><div><i class="fa-solid fa-circle-user"></i></div><div>Connect</div></a>
120
119
  <div class='flexible'></div>
121
120
  <div class='nav-btns'>
@@ -125,7 +125,6 @@ body main iframe {
125
125
  <button class='btn2' id='forward'><div><i class="fa-solid fa-chevron-right"></i></div><div>Next</div></button>
126
126
  <button class='btn2' id='refresh-page'><div><i class="fa-solid fa-rotate-right"></i></div><div>Refresh</div></button>
127
127
  <div class='flexible'></div>
128
- <a href="/network" class='btn2'><div><i class="fa-solid fa-wifi"></i></div><div>Network</div></a>
129
128
  <a href="/connect" class='btn2'><div><i class="fa-solid fa-circle-user"></i></div><div>Connect</div></a>
130
129
  <div class='nav-btns'>
131
130
  <a class='btn2' href="<%=portal%>" target="_blank"><div><i class="fa-solid fa-question"></i></div><div>Help</div></a>
@@ -234,7 +234,6 @@ body.dark .browser-options-row {
234
234
  </div>
235
235
  <% } %>
236
236
  <div class='browser-options-row'>
237
- <a href="/network" class='btn'><div><i class="fa-solid fa-gauge"></i> Configure</div></a>
238
237
  <div class='flexible'>
239
238
  <h3><i class="fa-solid fa-wifi"></i> Local Sharing</h3>
240
239
  <div>Serve Pinokio, installed apps, and even external apps (example: Ollama) to any device on the same network.</div>