pinokiod 1.3.96 → 1.3.98
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/api/cloudflare/index.js +1 -0
- package/kernel/api/local/index.js +1 -0
- package/kernel/environment.js +3 -1
- package/kernel/shell.js +2 -1
- package/package.json +1 -1
- package/pipe/index.js +30 -7
- package/pipe/views/login.ejs +6 -4
- package/server/views/env_editor.ejs +2 -0
- package/server/views/fullscreen_editor.ejs +30 -106
- package/server/views/share_editor.ejs +107 -12
|
@@ -64,6 +64,7 @@ class Local {
|
|
|
64
64
|
*/
|
|
65
65
|
|
|
66
66
|
let current_env = await Environment.get2(req.parent.path, kernel)
|
|
67
|
+
console.log({ current_env })
|
|
67
68
|
|
|
68
69
|
// if the key at current_env.PINOKIO_SHARE_VAR (by default it's 'url') is the variable being set, trigger the share logic
|
|
69
70
|
if (current_env.PINOKIO_SHARE_VAR && current_env.PINOKIO_SHARE_VAR in req.params) {
|
package/kernel/environment.js
CHANGED
|
@@ -293,10 +293,12 @@ const init_folders = async (homedir) => {
|
|
|
293
293
|
|
|
294
294
|
// Get the actual environment variable at specific path
|
|
295
295
|
const get2 = async (filepath, kernel) => {
|
|
296
|
+
console.log("Get2")
|
|
296
297
|
let api_path = Util.api_path(filepath, kernel)
|
|
297
298
|
let default_env = await get(kernel.homedir)
|
|
298
299
|
let api_env = await get(api_path)
|
|
299
|
-
|
|
300
|
+
console.log({ api_path, default_env, api_env, process_env: process.env })
|
|
301
|
+
let current_env = Object.assign({}, process.env, default_env, api_env)
|
|
300
302
|
return current_env
|
|
301
303
|
}
|
|
302
304
|
const get = async (homedir) => {
|
package/kernel/shell.js
CHANGED
package/package.json
CHANGED
package/pipe/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
2
|
+
const { yellow, green, blue } = require('kleur');
|
|
2
3
|
|
|
3
4
|
const path = require('path')
|
|
4
5
|
const { createHttpTerminator } = require('http-terminator')
|
|
@@ -16,17 +17,12 @@ class Pipe {
|
|
|
16
17
|
if (req.session.authenticated) {
|
|
17
18
|
next();
|
|
18
19
|
} else {
|
|
19
|
-
console.log("not authenticated. redirect /pionkio/login")
|
|
20
20
|
res.redirect('/pinokio/login');
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
// a pipe server for creating publicly shareable endpoints
|
|
24
24
|
async start(url, scriptPath, passcode, config) {
|
|
25
|
-
|
|
26
|
-
if (!config) config = {}
|
|
27
|
-
if (config.icon) {
|
|
28
|
-
}
|
|
29
|
-
|
|
25
|
+
console.log("pipe.start", { url, scriptPath, passcode, config })
|
|
30
26
|
|
|
31
27
|
let port = await this.kernel.port()
|
|
32
28
|
let app = express();
|
|
@@ -49,7 +45,34 @@ class Pipe {
|
|
|
49
45
|
const { password } = req.body;
|
|
50
46
|
|
|
51
47
|
//if (await bcrypt.compare(password, hashedPassword)) {
|
|
52
|
-
|
|
48
|
+
|
|
49
|
+
// multiple passcode options supported (comma separated)
|
|
50
|
+
let passcode_options = passcode.split(",")
|
|
51
|
+
if (passcode_options.includes(password)) {
|
|
52
|
+
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
|
53
|
+
const agent = req.headers['user-agent']
|
|
54
|
+
let event = {
|
|
55
|
+
time: `${new Date().toLocaleString()}`,
|
|
56
|
+
ip,
|
|
57
|
+
agent
|
|
58
|
+
}
|
|
59
|
+
let log = `\r\n[${event.time}] Login from ${event.ip} | ${event.agent}\r\n`
|
|
60
|
+
console.log(log)
|
|
61
|
+
// this.kernel.api.ondata({
|
|
62
|
+
// id: scriptPath,
|
|
63
|
+
// type: "notify",
|
|
64
|
+
// data: {
|
|
65
|
+
// html: `<div><h6>${event.time}</h6><div>IP: ${event.ip}</div>`
|
|
66
|
+
// }
|
|
67
|
+
// })
|
|
68
|
+
this.kernel.api.ondata({
|
|
69
|
+
type: "stream",
|
|
70
|
+
kernel: true,
|
|
71
|
+
id: scriptPath,
|
|
72
|
+
data: {
|
|
73
|
+
raw: yellow(log)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
53
76
|
req.session.authenticated = true;
|
|
54
77
|
res.redirect('/');
|
|
55
78
|
} else {
|
package/pipe/views/login.ejs
CHANGED
|
@@ -15,7 +15,7 @@ body {
|
|
|
15
15
|
color: white;
|
|
16
16
|
}
|
|
17
17
|
main {
|
|
18
|
-
max-width:
|
|
18
|
+
max-width: 300px;
|
|
19
19
|
width: 100%;
|
|
20
20
|
text-align: center;
|
|
21
21
|
text-transform: lowercase;
|
|
@@ -35,8 +35,8 @@ main input {
|
|
|
35
35
|
/*
|
|
36
36
|
background: rgba(0,0,0,0.06);
|
|
37
37
|
*/
|
|
38
|
-
font-size:
|
|
39
|
-
border-radius: 0;
|
|
38
|
+
font-size: 16px;
|
|
39
|
+
border-radius: 10px 0 0 10px;
|
|
40
40
|
|
|
41
41
|
}
|
|
42
42
|
main button {
|
|
@@ -45,6 +45,8 @@ main button {
|
|
|
45
45
|
padding: 10px;
|
|
46
46
|
box-sizing: border-box;
|
|
47
47
|
border: none;
|
|
48
|
+
font-size: 16px;
|
|
49
|
+
border-radius: 0 10px 10px 0;
|
|
48
50
|
}
|
|
49
51
|
.error {
|
|
50
52
|
border-left: 3px solid red;
|
|
@@ -67,7 +69,7 @@ h1 {
|
|
|
67
69
|
-->
|
|
68
70
|
<h1>PINOKIO</h1>
|
|
69
71
|
<form method="POST" action="/pinokio/login">
|
|
70
|
-
<input placeholder="
|
|
72
|
+
<input autofocus placeholder="Enter passcode" type="password" name="password" required>
|
|
71
73
|
<button type="submit">Login</button>
|
|
72
74
|
</form>
|
|
73
75
|
<% if (error) { %>
|
|
@@ -173,6 +173,7 @@ h1 {
|
|
|
173
173
|
font-size: 50px;
|
|
174
174
|
font-family: georgia;
|
|
175
175
|
font-style: italic;
|
|
176
|
+
padding-bottom: 20px;
|
|
176
177
|
}
|
|
177
178
|
footer {
|
|
178
179
|
background: none;
|
|
@@ -263,6 +264,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
263
264
|
</header>
|
|
264
265
|
<% } %>
|
|
265
266
|
<div id='editor' class='<%=init ? "hidden" : ''%>'>
|
|
267
|
+
<h1>Configure</h1>
|
|
266
268
|
<% items.forEach((item) => { %>
|
|
267
269
|
<% if (item.type === "comment") { %>
|
|
268
270
|
<div class='comment'><%=item.val.replaceAll(/^[ #]+/g, "")%></div>
|
|
@@ -104,21 +104,6 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
104
104
|
<% if (error) { %>
|
|
105
105
|
document.querySelector(".requirements .content").innerHTML = '<div class="loading"><i class="fa-solid fa-circle-exclamation"></i> <%=error%></div>'
|
|
106
106
|
<% } %>
|
|
107
|
-
<% if (!install_required) { %>
|
|
108
|
-
/*
|
|
109
|
-
document.querySelector("footer").addEventListener("click", (e) => {
|
|
110
|
-
if (e.target.classList.contains("toggle-expand") || e.target.closest(".toggle-expand")) {
|
|
111
|
-
let el = document.querySelector("footer .toggle-expand i")
|
|
112
|
-
if (el.classList.contains("fa-circle-chevron-up")) {
|
|
113
|
-
el.className = "fa-solid fa-circle-chevron-down"
|
|
114
|
-
} else if (el.classList.contains("fa-circle-chevron-down")) {
|
|
115
|
-
el.className = "fa-solid fa-circle-chevron-up"
|
|
116
|
-
}
|
|
117
|
-
document.querySelector("footer").classList.toggle("expanded")
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
*/
|
|
121
|
-
<% } %>
|
|
122
107
|
<% if (requirements_pending) { %>
|
|
123
108
|
await new Promise((resolve, reject) => {
|
|
124
109
|
let interval = setInterval(() => {
|
|
@@ -641,34 +626,29 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
641
626
|
}
|
|
642
627
|
});
|
|
643
628
|
|
|
644
|
-
document.querySelector(".stop")
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
629
|
+
if (document.querySelector(".stop")) {
|
|
630
|
+
document.querySelector(".stop").addEventListener("click", async (e) => {
|
|
631
|
+
await rpc.stop()
|
|
632
|
+
// socket.run({
|
|
633
|
+
// id: location.pathname,
|
|
634
|
+
// method: "kernel.api.stop"
|
|
635
|
+
// }, (stream) => {
|
|
636
|
+
// console.log("#", stream)
|
|
637
|
+
// })
|
|
638
|
+
document.querySelector(".run .play").classList.remove("hidden")
|
|
639
|
+
document.querySelector(".run .starting").classList.add("hidden")
|
|
640
|
+
document.querySelector(".run .stop").classList.add("hidden")
|
|
641
|
+
})
|
|
642
|
+
}
|
|
643
|
+
if (document.querySelector(".play")) {
|
|
644
|
+
document.querySelector(".play").addEventListener("click", async (e) => {
|
|
645
|
+
document.querySelector(".run .play").classList.add("hidden")
|
|
646
|
+
document.querySelector(".run .starting").classList.remove("hidden")
|
|
647
|
+
await rpc.run()
|
|
648
|
+
})
|
|
649
|
+
}
|
|
661
650
|
<% } %>
|
|
662
651
|
|
|
663
|
-
/*
|
|
664
|
-
document.querySelector("#save").addEventListener("click", async (e) => {
|
|
665
|
-
console.log("SAVE")
|
|
666
|
-
await rpc.save()
|
|
667
|
-
n.Noty({
|
|
668
|
-
text: `saved`,
|
|
669
|
-
})
|
|
670
|
-
})
|
|
671
|
-
*/
|
|
672
652
|
<% if (stop) { %>
|
|
673
653
|
await rpc.stop()
|
|
674
654
|
document.querySelector(".run .play").classList.remove("hidden")
|
|
@@ -685,74 +665,18 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
685
665
|
<% } %>
|
|
686
666
|
<% } %>
|
|
687
667
|
<% } %>
|
|
688
|
-
/*
|
|
689
|
-
document.querySelector("#explore").addEventListener("click", async (e) => {
|
|
690
|
-
e.preventDefault()
|
|
691
|
-
e.stopPropagation()
|
|
692
|
-
location.href = "/?mode=explore"
|
|
693
|
-
})
|
|
694
|
-
Reporter()
|
|
695
|
-
*/
|
|
696
668
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
// let url = target.getAttribute("data-url")
|
|
706
|
-
// const token = prompt("Enter an Ngrok auth token to expose your machine to the public Internet https://ngrok.com/")
|
|
707
|
-
// if (token && token.length > 0) {
|
|
708
|
-
// let response = await fetch("/pinokio/tunnel", {
|
|
709
|
-
// method: "POST",
|
|
710
|
-
// headers: {
|
|
711
|
-
// "Content-Type": "application/json"
|
|
712
|
-
// },
|
|
713
|
-
// body: JSON.stringify({ url, action: "start", token })
|
|
714
|
-
// }).then((res) => {
|
|
715
|
-
// return res.json()
|
|
716
|
-
// })
|
|
717
|
-
// await reloadMemory()
|
|
718
|
-
// }
|
|
719
|
-
// } else if (target2) {
|
|
720
|
-
// console.log("target2", target2)
|
|
721
|
-
// e.preventDefault()
|
|
722
|
-
// e.stopPropagation()
|
|
723
|
-
// let url = target2.getAttribute("data-url")
|
|
724
|
-
// let response = await fetch("/pinokio/tunnel", {
|
|
725
|
-
// method: "POST",
|
|
726
|
-
// headers: {
|
|
727
|
-
// "Content-Type": "application/json"
|
|
728
|
-
// },
|
|
729
|
-
// body: JSON.stringify({ url, action: "stop" })
|
|
730
|
-
// }).then((res) => {
|
|
731
|
-
// return res.json()
|
|
732
|
-
// })
|
|
733
|
-
// await reloadMemory()
|
|
734
|
-
// }
|
|
735
|
-
// })
|
|
736
|
-
/*
|
|
737
|
-
document.querySelector("#source").addEventListener("click", (e) => {
|
|
738
|
-
refreshParent({
|
|
739
|
-
id: "<%=filepath%>",
|
|
740
|
-
action: {
|
|
741
|
-
"type": "newtab",
|
|
742
|
-
"url": e.target.getAttribute("data-editor-url")
|
|
669
|
+
if (document.querySelector("#lv")) {
|
|
670
|
+
document.querySelector("#lv").addEventListener("click", (e) => {
|
|
671
|
+
let m = e.target.closest(".memory")
|
|
672
|
+
m.classList.toggle("collapsed")
|
|
673
|
+
if (m.classList.contains("collapsed")) {
|
|
674
|
+
m.querySelector("i").className = "fa-solid fa-sort-down"
|
|
675
|
+
} else {
|
|
676
|
+
m.querySelector("i").className = "fa-solid fa-sort-up"
|
|
743
677
|
}
|
|
744
678
|
})
|
|
745
|
-
}
|
|
746
|
-
*/
|
|
747
|
-
document.querySelector("#lv").addEventListener("click", (e) => {
|
|
748
|
-
let m = e.target.closest(".memory")
|
|
749
|
-
m.classList.toggle("collapsed")
|
|
750
|
-
if (m.classList.contains("collapsed")) {
|
|
751
|
-
m.querySelector("i").className = "fa-solid fa-sort-down"
|
|
752
|
-
} else {
|
|
753
|
-
m.querySelector("i").className = "fa-solid fa-sort-up"
|
|
754
|
-
}
|
|
755
|
-
})
|
|
679
|
+
}
|
|
756
680
|
})
|
|
757
681
|
const reloadMemory = async () => {
|
|
758
682
|
let url = "/pinokio/memory?filepath=<%=encodedFilePath%>"
|
|
@@ -74,9 +74,10 @@ td.key {
|
|
|
74
74
|
|
|
75
75
|
}
|
|
76
76
|
#editor {
|
|
77
|
+
margin: 0 auto;
|
|
77
78
|
box-sizing: border-box;
|
|
78
79
|
max-width: 800px;
|
|
79
|
-
padding:
|
|
80
|
+
padding: 50px;
|
|
80
81
|
}
|
|
81
82
|
header .runner {
|
|
82
83
|
padding: 10px 0 0;
|
|
@@ -137,7 +138,6 @@ hr {
|
|
|
137
138
|
height: 1px;
|
|
138
139
|
}
|
|
139
140
|
.shared {
|
|
140
|
-
margin-left: 30px;
|
|
141
141
|
}
|
|
142
142
|
.shared a {
|
|
143
143
|
display: block;
|
|
@@ -145,19 +145,96 @@ hr {
|
|
|
145
145
|
}
|
|
146
146
|
.shared img {
|
|
147
147
|
width: 200px;
|
|
148
|
+
border: 1px solid rgba(0,0,0,0.1);
|
|
149
|
+
}
|
|
150
|
+
.shared h2 {
|
|
151
|
+
margin: 0;
|
|
152
|
+
letter-spacing: -1px;
|
|
153
|
+
}
|
|
154
|
+
.passcode ol {
|
|
155
|
+
padding-inline-start: 15px;
|
|
156
|
+
margin: 5px 0 10px;
|
|
157
|
+
}
|
|
158
|
+
body.dark .passcode {
|
|
159
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
160
|
+
}
|
|
161
|
+
.passcode {
|
|
162
|
+
margin-left: 30px;
|
|
163
|
+
padding: 20px !important;
|
|
164
|
+
box-sizing: border-box;
|
|
165
|
+
border: 1px solid rgba(0,0,0,0.1);
|
|
166
|
+
margin-top: 20px;
|
|
167
|
+
margin-bottom: 20px;
|
|
168
|
+
}
|
|
169
|
+
.passcode h2 {
|
|
170
|
+
margin: 0;
|
|
171
|
+
font-size: 20px;
|
|
172
|
+
letter-spacing: -1px;
|
|
173
|
+
}
|
|
174
|
+
.passcode div {
|
|
175
|
+
padding: 0 0 10px;
|
|
176
|
+
}
|
|
177
|
+
.passcode .row {
|
|
178
|
+
display: flex;
|
|
179
|
+
padding: 0;
|
|
180
|
+
}
|
|
181
|
+
body.dark .passcode .row input {
|
|
182
|
+
background: white;
|
|
183
|
+
}
|
|
184
|
+
.passcode .row input {
|
|
185
|
+
border-radius: 0;
|
|
186
|
+
flex-grow: 1;
|
|
187
|
+
padding: 10px;
|
|
188
|
+
box-sizing: border-box;
|
|
189
|
+
border: none;
|
|
190
|
+
background: rgba(0,0,100,0.1);
|
|
191
|
+
}
|
|
192
|
+
body.dark .passcode button {
|
|
193
|
+
background: royalblue;
|
|
194
|
+
}
|
|
195
|
+
.passcode button {
|
|
196
|
+
border-radius: 0;
|
|
197
|
+
}
|
|
198
|
+
body.dark .cloudflare-running {
|
|
199
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
200
|
+
}
|
|
201
|
+
.cloudflare-running {
|
|
202
|
+
margin-left: 30px;
|
|
203
|
+
padding: 20px !important;
|
|
204
|
+
box-sizing: border-box;
|
|
205
|
+
border: 1px solid rgba(0,0,0,0.1);
|
|
206
|
+
}
|
|
207
|
+
h1 {
|
|
208
|
+
font-size: 50px;
|
|
209
|
+
font-family: georgia;
|
|
210
|
+
font-style: italic;
|
|
211
|
+
padding-bottom: 10px;
|
|
148
212
|
}
|
|
149
213
|
|
|
150
214
|
</style>
|
|
151
215
|
<script>
|
|
152
216
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
153
217
|
const n = new N()
|
|
218
|
+
document.querySelector("#reveal").addEventListener("click", async (e) => {
|
|
219
|
+
let el = document.querySelector("#passcode")
|
|
220
|
+
if (el.type === "password") {
|
|
221
|
+
el.type = "text"
|
|
222
|
+
} else {
|
|
223
|
+
el.type = "password"
|
|
224
|
+
}
|
|
225
|
+
})
|
|
154
226
|
document.querySelector("#save").addEventListener("click", async (e) => {
|
|
155
227
|
// save
|
|
156
228
|
const vals = {}
|
|
157
229
|
const els = document.querySelectorAll(".env-item input[type=checkbox]")
|
|
158
230
|
for(let el of els) {
|
|
159
|
-
|
|
231
|
+
let id = el.getAttribute("data-id")
|
|
232
|
+
vals[id] = el.checked
|
|
160
233
|
}
|
|
234
|
+
|
|
235
|
+
const passcode_el = document.querySelector("#passcode")
|
|
236
|
+
vals.PINOKIO_SHARE_PASSCODE = passcode_el.value
|
|
237
|
+
|
|
161
238
|
let res = await fetch("/env", {
|
|
162
239
|
method: "POST",
|
|
163
240
|
headers: {
|
|
@@ -165,7 +242,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
165
242
|
},
|
|
166
243
|
body: JSON.stringify({
|
|
167
244
|
filepath: "api/" + location.pathname.slice(7),
|
|
168
|
-
vals
|
|
245
|
+
vals,
|
|
169
246
|
})
|
|
170
247
|
}).then((res) => {
|
|
171
248
|
return res.json()
|
|
@@ -185,23 +262,41 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
185
262
|
<hr>
|
|
186
263
|
<div class='env-item'>
|
|
187
264
|
<% if (config.PINOKIO_SHARE_CLOUDFLARE.toString() === "true") { %>
|
|
188
|
-
<input type="checkbox" id="PINOKIO_SHARE_CLOUDFLARE" name="cloudflare" checked />
|
|
265
|
+
<input type="checkbox" data-id="PINOKIO_SHARE_CLOUDFLARE" name="cloudflare" checked />
|
|
189
266
|
<% } else { %>
|
|
190
|
-
<input type="checkbox" id="PINOKIO_SHARE_CLOUDFLARE" name="cloudflare" />
|
|
267
|
+
<input type="checkbox" data-id="PINOKIO_SHARE_CLOUDFLARE" name="cloudflare" />
|
|
191
268
|
<% } %>
|
|
192
269
|
<label for="cloudflare"><i class="fa-brands fa-cloudflare"></i> Run a Public Node</label>
|
|
193
270
|
<ul>
|
|
194
271
|
<li>This will let you share your local Pinokio app to the public internet, which anyone with a URL can access.</li>
|
|
195
272
|
<li>Powered by <a href="https://www.cloudflare.com/products/tunnel/" target="_blank" features="browser">Cloudflare Tunnel</a></li>
|
|
196
273
|
<li>You can manually configure this in through the Configure menu (set PINOKIO_SHARE_CLOUDFLARE to true)</li>
|
|
197
|
-
<li>You can add <strong>authentication</strong> by adding a passcode (set PINOKIO_SHARE_PASSCODE in the <strong>Configure</strong> tab)</li>
|
|
198
274
|
</ul>
|
|
199
|
-
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
<
|
|
275
|
+
<div class='passcode'>
|
|
276
|
+
<h2>Passcode (optional)</h2>
|
|
277
|
+
<ol>
|
|
278
|
+
<li><strong>No passcode:</strong> Anyone with the URL can use the app</li>
|
|
279
|
+
<li><strong>Passcode:</strong> Users will need to authenticate with the passcode before using the app.</li>
|
|
280
|
+
<li>To allow multiple passcodes, separate the passode by commas (Example: ab12zd3,D3ad3a,df3d9Fa).</li>
|
|
281
|
+
</ol>
|
|
282
|
+
<div class='row'>
|
|
283
|
+
<input id='passcode' type='password' value="<%=config.PINOKIO_SHARE_PASSCODE || ''%>"/>
|
|
284
|
+
<button class='btn' id='reveal'><i class="fa-solid fa-eye"></i></button>
|
|
203
285
|
</div>
|
|
204
|
-
|
|
286
|
+
</div>
|
|
287
|
+
<% if (cloudflare_links.length > 0) { %>
|
|
288
|
+
<div class='cloudflare-running'>
|
|
289
|
+
<div class='shared'>
|
|
290
|
+
<h2><i class="fa-solid fa-circle-notch fa-spin"></i> Running</h2>
|
|
291
|
+
</div>
|
|
292
|
+
<% cloudflare_links.forEach((link) => { %>
|
|
293
|
+
<div class='shared'>
|
|
294
|
+
<a href="<%=link.url%>" target="_blank" features="browser"><%=link.url%></a>
|
|
295
|
+
<img src="<%=link.qr%>">
|
|
296
|
+
</div>
|
|
297
|
+
<% }) %>
|
|
298
|
+
</div>
|
|
299
|
+
<% } %>
|
|
205
300
|
</div>
|
|
206
301
|
<div class='env-item'>
|
|
207
302
|
<% if (config.PINOKIO_SHARE_LOCAL.toString() === "true") { %>
|