pinokiod 8.2.2 → 8.2.4
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/Dockerfile +3 -2
- package/kernel/anywhere/index.js +706 -0
- package/kernel/anywhere/localapi.js +147 -0
- package/kernel/anywhere/ports.js +51 -0
- package/kernel/anywhere/serve.js +95 -0
- package/kernel/bin/bun.js +1 -1
- package/kernel/bin/conda-meta.js +15 -0
- package/kernel/bin/conda-pins.js +22 -0
- package/kernel/bin/conda.js +66 -5
- package/kernel/bin/index.js +4 -3
- package/kernel/bin/node.js +1 -1
- package/kernel/bin/setup.js +29 -0
- package/kernel/bin/tailscaled.js +72 -0
- package/kernel/bin/uv.js +1 -1
- package/kernel/index.js +15 -0
- package/kernel/shell.js +8 -1
- package/package.json +1 -1
- package/server/index.js +123 -0
- package/server/public/anywhere.webmanifest +1 -0
- package/server/views/anywhere.ejs +436 -0
- package/server/views/app.ejs +3 -1
- package/server/views/index.ejs +6 -0
- package/server/views/partials/main_sidebar.ejs +39 -0
- package/test/anywhere.test.js +461 -0
- package/test/conda-bin.test.js +126 -8
- package/test/node-bin.test.js +3 -3
- package/test/shell-run-template.test.js +46 -0
- package/test/uv-bin.test.js +5 -5
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<head>
|
|
3
|
+
<title>Pinokio Anywhere</title>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
6
|
+
<% if (locals.anywhere_host) { %>
|
|
7
|
+
<link rel="manifest" href="/anywhere.webmanifest">
|
|
8
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
9
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
10
|
+
<meta name="apple-mobile-web-app-title" content="Pinokio">
|
|
11
|
+
<% } %>
|
|
12
|
+
<link href="/css/fontawesome.min.css" rel="stylesheet">
|
|
13
|
+
<link href="/css/solid.min.css" rel="stylesheet">
|
|
14
|
+
<link href="/css/regular.min.css" rel="stylesheet">
|
|
15
|
+
<link href="/css/brands.min.css" rel="stylesheet">
|
|
16
|
+
<link href="/noty.css" rel="stylesheet"/>
|
|
17
|
+
<link href="/style.css" rel="stylesheet"/>
|
|
18
|
+
<link href="/task-launcher.css" rel="stylesheet"/>
|
|
19
|
+
<link href="/urldropdown.css" rel="stylesheet" />
|
|
20
|
+
<script src="/sweetalert2.js"></script>
|
|
21
|
+
<script src="/commander.js"></script>
|
|
22
|
+
<% if (agent === "electron") { %>
|
|
23
|
+
<link href="/electron.css" rel="stylesheet"/>
|
|
24
|
+
<% } %>
|
|
25
|
+
<style>
|
|
26
|
+
.anywhere-shell-body { padding-bottom: 40px; }
|
|
27
|
+
.anywhere-state { display: none; }
|
|
28
|
+
.anywhere-state.active { display: block; }
|
|
29
|
+
.anywhere-card {
|
|
30
|
+
border: 1px solid rgba(0,0,0,0.1);
|
|
31
|
+
border-radius: 12px;
|
|
32
|
+
padding: 20px;
|
|
33
|
+
margin: 16px 0;
|
|
34
|
+
background: rgba(0,0,100,0.02);
|
|
35
|
+
}
|
|
36
|
+
body.dark .anywhere-card { border-color: rgba(255,255,255,0.1); background: rgba(255,255,255,0.03); }
|
|
37
|
+
.anywhere-card h2 { margin: 0 0 6px; font-size: 16px; }
|
|
38
|
+
.anywhere-card h3 { margin: 18px 0 6px; font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; }
|
|
39
|
+
.anywhere-card p { margin: 6px 0; line-height: 1.5; }
|
|
40
|
+
.anywhere-muted { opacity: 0.7; font-size: 13px; }
|
|
41
|
+
.anywhere-grid { display: grid; grid-template-columns: minmax(280px, 380px) 1fr; gap: 16px; align-items: start; }
|
|
42
|
+
@media (max-width: 900px) { .anywhere-grid { grid-template-columns: 1fr; } }
|
|
43
|
+
.anywhere-qr { display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; }
|
|
44
|
+
.anywhere-qr img { width: 220px; height: 220px; border-radius: 8px; background: white; padding: 6px; }
|
|
45
|
+
.anywhere-address {
|
|
46
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
47
|
+
font-size: 15px; word-break: break-all;
|
|
48
|
+
padding: 8px 12px; border-radius: 8px; background: rgba(0,0,0,0.05);
|
|
49
|
+
}
|
|
50
|
+
body.dark .anywhere-address { background: rgba(255,255,255,0.08); }
|
|
51
|
+
.anywhere-address a { color: inherit; text-decoration: none; }
|
|
52
|
+
.anywhere-btn {
|
|
53
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
54
|
+
padding: 10px 16px; border-radius: 8px; border: none; cursor: pointer;
|
|
55
|
+
background: black; color: white; font-weight: bold; font-size: 14px; text-decoration: none;
|
|
56
|
+
}
|
|
57
|
+
.anywhere-btn.secondary { background: rgba(0,0,0,0.08); color: inherit; font-weight: normal; }
|
|
58
|
+
body.dark .anywhere-btn { background: white; color: black; }
|
|
59
|
+
body.dark .anywhere-btn.secondary { background: rgba(255,255,255,0.1); color: inherit; }
|
|
60
|
+
.anywhere-btn[disabled] { opacity: 0.5; cursor: default; }
|
|
61
|
+
.anywhere-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-top: 1px solid rgba(0,0,0,0.06); }
|
|
62
|
+
body.dark .anywhere-row { border-top-color: rgba(255,255,255,0.06); }
|
|
63
|
+
.anywhere-row:first-child { border-top: none; }
|
|
64
|
+
.anywhere-row img { width: 28px; height: 28px; border-radius: 6px; object-fit: cover; }
|
|
65
|
+
.anywhere-row .grow { flex-grow: 1; min-width: 0; }
|
|
66
|
+
.anywhere-row .title { font-weight: bold; }
|
|
67
|
+
.anywhere-row .sub { font-size: 12px; opacity: 0.7; word-break: break-all; }
|
|
68
|
+
.anywhere-row a.remote { font-family: ui-monospace, Menlo, monospace; font-size: 13px; color: royalblue; text-decoration: none; word-break: break-all; }
|
|
69
|
+
.anywhere-dot { width: 9px; height: 9px; border-radius: 50%; background: silver; flex-shrink: 0; }
|
|
70
|
+
.anywhere-dot.online { background: yellowgreen; }
|
|
71
|
+
.anywhere-steps { padding-left: 20px; margin: 8px 0; }
|
|
72
|
+
.anywhere-steps li { margin: 6px 0; line-height: 1.5; }
|
|
73
|
+
.anywhere-waiting { display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 8px; background: rgba(255,200,0,0.15); font-size: 13px; }
|
|
74
|
+
.anywhere-waiting.joined { background: rgba(154,205,50,0.2); }
|
|
75
|
+
.anywhere-switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 8px 12px; border-radius: 8px; background: rgba(0,0,0,0.05); }
|
|
76
|
+
body.dark .anywhere-switch { background: rgba(255,255,255,0.08); }
|
|
77
|
+
.anywhere-switch[data-on=true] { color: yellowgreen; }
|
|
78
|
+
.anywhere-error { white-space: pre-wrap; font-family: ui-monospace, Menlo, monospace; font-size: 12px; padding: 10px; border-radius: 8px; background: rgba(255,0,0,0.08); }
|
|
79
|
+
.anywhere-badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: rgba(0,0,100,0.1); }
|
|
80
|
+
body.dark .anywhere-badge { background: rgba(255,255,255,0.12); }
|
|
81
|
+
.anywhere-kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 14px; font-size: 13px; }
|
|
82
|
+
.anywhere-kv dt { opacity: 0.7; }
|
|
83
|
+
.anywhere-kv dd { margin: 0; word-break: break-all; }
|
|
84
|
+
</style>
|
|
85
|
+
<script src="/urldropdown.js"></script>
|
|
86
|
+
<script>
|
|
87
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
88
|
+
if (typeof initUrlDropdown === 'function') {
|
|
89
|
+
initUrlDropdown({ clearBehavior: 'empty' });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
93
|
+
</head>
|
|
94
|
+
<body class='<%=theme%> main-sidebar-page task-launcher-page task-page anywhere-page' data-agent="<%=agent%>">
|
|
95
|
+
<header class='navheader grabbable'>
|
|
96
|
+
<h1>
|
|
97
|
+
<a class='home' href="/home"><img class='icon' src="/pinokio-black.png"></a>
|
|
98
|
+
<button class='btn2' id='minimize-header' data-tippy-content="fullscreen" title='fullscreen'>
|
|
99
|
+
<div><i class="fa-solid fa-expand"></i></div>
|
|
100
|
+
</button>
|
|
101
|
+
<a class='btn2 pinokio-explore-nav-button' href="/home?mode=explore" data-pinokio-explore-nav="true" data-tippy-content="Explore" title="Explore" aria-label="Explore">
|
|
102
|
+
<div><i class="fa-solid fa-globe" aria-hidden="true"></i></div>
|
|
103
|
+
</a>
|
|
104
|
+
<button class='btn2' id='back' data-tippy-content="back"><div><i class="fa-solid fa-chevron-left"></i></div></button>
|
|
105
|
+
<button class='btn2' id='forward' data-tippy-content="forward"><div><i class="fa-solid fa-chevron-right"></i></div></button>
|
|
106
|
+
<button class='btn2' id='refresh-page' data-tippy-content="refresh"><div><i class="fa-solid fa-rotate-right"></i></div></button>
|
|
107
|
+
<div class='mobile-flexible'></div>
|
|
108
|
+
<form class='urlbar'>
|
|
109
|
+
<div class='url-input-container'>
|
|
110
|
+
<input type='url' placeholder='enter any local url to open in pinokio'>
|
|
111
|
+
<div class='url-dropdown' id='url-dropdown'></div>
|
|
112
|
+
</div>
|
|
113
|
+
</form>
|
|
114
|
+
<%- include('partials/home_server_popover') %>
|
|
115
|
+
<a class='btn2' href="/columns" data-tippy-content="split into 2 columns">
|
|
116
|
+
<div><i class="fa-solid fa-table-columns"></i></div>
|
|
117
|
+
</a>
|
|
118
|
+
<a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
|
|
119
|
+
<div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
|
|
120
|
+
</a>
|
|
121
|
+
<button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
|
|
122
|
+
<div><i class="fa-solid fa-plus"></i></div>
|
|
123
|
+
</button>
|
|
124
|
+
<button class='btn2 hidden' id='close-window' data-tippy-content='close this section'>
|
|
125
|
+
<div><i class="fa-solid fa-xmark"></i></div>
|
|
126
|
+
</button>
|
|
127
|
+
</h1>
|
|
128
|
+
</header>
|
|
129
|
+
<%- include('partials/home_server_popover_assets') %>
|
|
130
|
+
<main>
|
|
131
|
+
<div class='task-container'>
|
|
132
|
+
<section class='task-shell anywhere-shell'>
|
|
133
|
+
<header class='task-shell-header'>
|
|
134
|
+
<div class='task-shell-header-main'>
|
|
135
|
+
<h1 class='task-title'>Pinokio Anywhere</h1>
|
|
136
|
+
<p class='task-description'>Open this Pinokio and its running apps from your phone or another computer, anywhere, over a private encrypted network.</p>
|
|
137
|
+
</div>
|
|
138
|
+
<div class='task-header-actions'>
|
|
139
|
+
<div class='anywhere-switch' id='anywhere-switch' data-on='false' data-busy='false'>
|
|
140
|
+
<i class="fa-solid fa-power-off"></i> <span id='anywhere-switch-label'>Off</span>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</header>
|
|
144
|
+
<div class='task-shell-body anywhere-shell-body'>
|
|
145
|
+
<div class='container' id='anywhere-root' data-phase='<%= status && status.phase ? status.phase : "starting" %>'>
|
|
146
|
+
|
|
147
|
+
<div class='anywhere-state' data-state='off'>
|
|
148
|
+
<div class='anywhere-card'>
|
|
149
|
+
<h2>Pinokio Anywhere is off</h2>
|
|
150
|
+
<p>Turn it on to reach this computer from your phone. Nothing is exposed to the public internet: only devices signed in to your own Tailscale account can connect.</p>
|
|
151
|
+
<p><button class='anywhere-btn' data-action='start'><i class="fa-solid fa-power-off"></i> Turn on</button></p>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div class='anywhere-state' data-state='starting'>
|
|
156
|
+
<div class='anywhere-card'>
|
|
157
|
+
<h2><i class="fa-solid fa-circle-notch fa-spin"></i> Starting the secure network…</h2>
|
|
158
|
+
<p class='anywhere-muted'>Launching the bundled Tailscale daemon. This usually takes a few seconds.</p>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class='anywhere-state' data-state='login'>
|
|
163
|
+
<div class='anywhere-card'>
|
|
164
|
+
<h2>Sign in in your browser</h2>
|
|
165
|
+
<p>Pinokio Anywhere uses <b>Tailscale</b>, a free private network. Sign in with a <b>personal</b> Google, Apple, GitHub or Microsoft account. You will use the same account on your phone.</p>
|
|
166
|
+
<p><a class='anywhere-btn' id='anywhere-login-link' href='#' target='_blank' rel='noopener'><i class="fa-solid fa-arrow-up-right-from-square"></i> Sign in with Tailscale</a></p>
|
|
167
|
+
<p class='anywhere-muted'>Waiting for you to finish signing in… <span id='anywhere-login-url' class='anywhere-muted'></span></p>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div class='anywhere-state' data-state='machine_auth'>
|
|
172
|
+
<div class='anywhere-card'>
|
|
173
|
+
<h2>Approve this device</h2>
|
|
174
|
+
<p>Your Tailscale network requires new devices to be approved. Open the admin console and approve <b>pinokio</b>.</p>
|
|
175
|
+
<p><a class='anywhere-btn' href='https://login.tailscale.com/admin/machines' target='_blank' rel='noopener'>Open Tailscale admin console</a></p>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div class='anywhere-state' data-state='https'>
|
|
180
|
+
<div class='anywhere-card'>
|
|
181
|
+
<h2>One more click: enable HTTPS</h2>
|
|
182
|
+
<p id='anywhere-approval-text'>Tailscale needs your permission once to issue HTTPS certificates for your network. Apps need HTTPS for things like camera and microphone access on your phone.</p>
|
|
183
|
+
<p><a class='anywhere-btn' id='anywhere-approval-link' href='#' target='_blank' rel='noopener'><i class="fa-solid fa-lock"></i> Enable HTTPS</a>
|
|
184
|
+
<a class='anywhere-btn secondary' href='https://login.tailscale.com/admin/dns' target='_blank' rel='noopener'>Open DNS settings</a></p>
|
|
185
|
+
<p class='anywhere-muted'>Waiting for HTTPS to be enabled… (in the admin console: DNS → HTTPS Certificates → Enable)</p>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div class='anywhere-state' data-state='error'>
|
|
190
|
+
<div class='anywhere-card'>
|
|
191
|
+
<h2>Something went wrong</h2>
|
|
192
|
+
<div class='anywhere-error' id='anywhere-error'></div>
|
|
193
|
+
<p><button class='anywhere-btn' data-action='start'><i class="fa-solid fa-rotate-right"></i> Try again</button></p>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div class='anywhere-state' data-state='ready'>
|
|
198
|
+
<div class='anywhere-grid'>
|
|
199
|
+
<div class='anywhere-card'>
|
|
200
|
+
<h2>Connect your phone</h2>
|
|
201
|
+
<div class='anywhere-qr'>
|
|
202
|
+
<img id='anywhere-qr' alt='QR code to connect' src=''>
|
|
203
|
+
<div class='anywhere-address'><a id='anywhere-address' href='#' target='_blank' rel='noopener'></a></div>
|
|
204
|
+
<div class='anywhere-muted'>signed in as <b id='anywhere-account'></b></div>
|
|
205
|
+
<div class='anywhere-waiting' id='anywhere-waiting'><i class="fa-solid fa-circle-notch fa-spin"></i> waiting for a phone to join…</div>
|
|
206
|
+
</div>
|
|
207
|
+
<h3>Steps</h3>
|
|
208
|
+
<ol class='anywhere-steps'>
|
|
209
|
+
<li>Install the <b>Tailscale</b> app on your phone (App Store / Play Store).</li>
|
|
210
|
+
<li>Sign in with <b class='anywhere-account-inline'></b> and turn the connection on.</li>
|
|
211
|
+
<li>Scan the QR code, or open the address above in your phone's browser. Use <i>Add to Home Screen</i> for an app-like icon.</li>
|
|
212
|
+
</ol>
|
|
213
|
+
</div>
|
|
214
|
+
<div>
|
|
215
|
+
<div class='anywhere-card'>
|
|
216
|
+
<h2>Running apps</h2>
|
|
217
|
+
<p class='anywhere-muted'>Every app Pinokio launches gets its own address automatically. Entries disappear when an app stops.</p>
|
|
218
|
+
<div id='anywhere-apps'></div>
|
|
219
|
+
<h3>Published addresses</h3>
|
|
220
|
+
<div id='anywhere-ports' class='anywhere-muted'></div>
|
|
221
|
+
</div>
|
|
222
|
+
<div class='anywhere-card'>
|
|
223
|
+
<h2>Devices on your network</h2>
|
|
224
|
+
<div id='anywhere-peers'></div>
|
|
225
|
+
</div>
|
|
226
|
+
<div class='anywhere-card'>
|
|
227
|
+
<h2>Details</h2>
|
|
228
|
+
<dl class='anywhere-kv'>
|
|
229
|
+
<dt>Node</dt><dd id='anywhere-dnsname'></dd>
|
|
230
|
+
<dt>Tailscale IPs</dt><dd id='anywhere-ips'></dd>
|
|
231
|
+
<dt>Key expires</dt><dd id='anywhere-expiry'></dd>
|
|
232
|
+
<dt>Daemon</dt><dd id='anywhere-version'></dd>
|
|
233
|
+
</dl>
|
|
234
|
+
<p style='margin-top:14px'>
|
|
235
|
+
<button class='anywhere-btn secondary' data-action='logout'><i class="fa-solid fa-right-from-bracket"></i> Sign out / switch account</button>
|
|
236
|
+
<button class='anywhere-btn secondary' data-action='stop'><i class="fa-solid fa-power-off"></i> Turn off</button>
|
|
237
|
+
</p>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</section>
|
|
246
|
+
</div>
|
|
247
|
+
<%- include('partials/main_sidebar', { selected: 'anywhere' }) %>
|
|
248
|
+
</main>
|
|
249
|
+
<script>
|
|
250
|
+
(() => {
|
|
251
|
+
const root = document.getElementById('anywhere-root')
|
|
252
|
+
const states = Array.from(root.querySelectorAll('.anywhere-state'))
|
|
253
|
+
const switchEl = document.getElementById('anywhere-switch')
|
|
254
|
+
const switchLabel = document.getElementById('anywhere-switch-label')
|
|
255
|
+
const agent = document.body.getAttribute('data-agent')
|
|
256
|
+
let current = <%- JSON.stringify(status || {}).replace(/</g, '\\u003c') %>
|
|
257
|
+
let busy = false
|
|
258
|
+
let timer = null
|
|
259
|
+
|
|
260
|
+
const esc = (value) => String(value == null ? '' : value).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
261
|
+
const openExternal = (href) => {
|
|
262
|
+
if (!href) return
|
|
263
|
+
if (agent === 'electron') {
|
|
264
|
+
window.open(href, '_blank', 'browser')
|
|
265
|
+
} else {
|
|
266
|
+
window.open(href, '_blank')
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const formatDate = (value) => {
|
|
270
|
+
if (!value) return '—'
|
|
271
|
+
try {
|
|
272
|
+
const d = new Date(value)
|
|
273
|
+
const days = Math.round((d.getTime() - Date.now()) / 86400000)
|
|
274
|
+
return d.toLocaleDateString() + (days >= 0 ? ` (in ${days} day${days === 1 ? '' : 's'})` : ' (expired)')
|
|
275
|
+
} catch (e) { return String(value) }
|
|
276
|
+
}
|
|
277
|
+
const osIcon = (os) => {
|
|
278
|
+
const o = String(os || '').toLowerCase()
|
|
279
|
+
if (o === 'ios' || o === 'ipados') return 'fa-brands fa-apple'
|
|
280
|
+
if (o === 'android') return 'fa-brands fa-android'
|
|
281
|
+
if (o === 'macos') return 'fa-solid fa-laptop'
|
|
282
|
+
if (o === 'windows') return 'fa-brands fa-windows'
|
|
283
|
+
if (o === 'linux') return 'fa-brands fa-linux'
|
|
284
|
+
return 'fa-solid fa-desktop'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const show = (phase) => {
|
|
288
|
+
root.dataset.phase = phase
|
|
289
|
+
for (const el of states) {
|
|
290
|
+
el.classList.toggle('active', el.dataset.state === phase)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const render = (status) => {
|
|
295
|
+
current = status || {}
|
|
296
|
+
const phase = current.phase && current.phase !== 'setup' ? current.phase : 'starting'
|
|
297
|
+
const on = !!current.enabled && phase !== 'off' && phase !== 'setup'
|
|
298
|
+
switchEl.dataset.on = on ? 'true' : 'false'
|
|
299
|
+
switchLabel.textContent = on ? 'On' : 'Off'
|
|
300
|
+
show(phase)
|
|
301
|
+
document.dispatchEvent(new CustomEvent('pinokio:anywhere-status', { detail: { payload: current } }))
|
|
302
|
+
|
|
303
|
+
if (phase === 'login') {
|
|
304
|
+
const link = document.getElementById('anywhere-login-link')
|
|
305
|
+
link.href = current.loginUrl || '#'
|
|
306
|
+
document.getElementById('anywhere-login-url').textContent = current.loginUrl || ''
|
|
307
|
+
}
|
|
308
|
+
if (phase === 'https') {
|
|
309
|
+
const link = document.getElementById('anywhere-approval-link')
|
|
310
|
+
const url = current.approval && current.approval.url
|
|
311
|
+
link.href = url || 'https://login.tailscale.com/admin/dns'
|
|
312
|
+
if (current.approval && current.approval.text) {
|
|
313
|
+
document.getElementById('anywhere-approval-text').textContent = current.approval.text
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (phase === 'error') {
|
|
317
|
+
document.getElementById('anywhere-error').textContent = current.error || current.serveError || 'unknown error'
|
|
318
|
+
}
|
|
319
|
+
if (phase === 'ready') {
|
|
320
|
+
const qr = document.getElementById('anywhere-qr')
|
|
321
|
+
if (current.qr && qr.getAttribute('src') !== current.qr) qr.src = current.qr
|
|
322
|
+
const address = document.getElementById('anywhere-address')
|
|
323
|
+
address.textContent = (current.address || '').replace(/^https?:\/\//, '')
|
|
324
|
+
address.href = current.address || '#'
|
|
325
|
+
const email = current.account && current.account.email ? current.account.email : ''
|
|
326
|
+
document.getElementById('anywhere-account').textContent = email
|
|
327
|
+
for (const el of document.querySelectorAll('.anywhere-account-inline')) el.textContent = email
|
|
328
|
+
const waiting = document.getElementById('anywhere-waiting')
|
|
329
|
+
if (current.mobilePeerPresent) {
|
|
330
|
+
waiting.classList.add('joined')
|
|
331
|
+
waiting.innerHTML = `<i class="fa-solid fa-check"></i> phone ${current.mobilePeerOnline ? 'connected' : 'joined (currently offline)'}`
|
|
332
|
+
} else {
|
|
333
|
+
waiting.classList.remove('joined')
|
|
334
|
+
waiting.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i> waiting for a phone to join…`
|
|
335
|
+
}
|
|
336
|
+
const apps = Array.isArray(current.apps) ? current.apps : []
|
|
337
|
+
document.getElementById('anywhere-apps').innerHTML = apps.length === 0
|
|
338
|
+
? `<p class='anywhere-muted'>No app is running right now. Launch one and it will show up here.</p>`
|
|
339
|
+
: apps.map((app) => `
|
|
340
|
+
<div class='anywhere-row'>
|
|
341
|
+
<img src='${esc(app.icon || '/pinokio-black.png')}' alt=''>
|
|
342
|
+
<div class='grow'>
|
|
343
|
+
<div class='title'>${esc(app.name)} ${app.exposed ? '' : `<span class='anywhere-badge'>${esc(app.state || 'starting')}</span>`}</div>
|
|
344
|
+
${app.remote ? `<a class='remote' href='${esc(app.remote)}' target='_blank' rel='noopener'>${esc(app.remote)}</a>` : `<div class='sub'>no web UI port yet</div>`}
|
|
345
|
+
</div>
|
|
346
|
+
</div>`).join('')
|
|
347
|
+
const serve = Array.isArray(current.serve) ? current.serve : []
|
|
348
|
+
document.getElementById('anywhere-ports').innerHTML = serve.length === 0 ? '—' : serve.map((entry) => `${esc(entry.name || (entry.port === 443 ? 'Pinokio' : ''))} <a class='remote' href='${esc(entry.url)}' target='_blank' rel='noopener'>${esc(entry.url)}</a> → ${esc(entry.target)}`).join('<br>')
|
|
349
|
+
const peers = Array.isArray(current.peers) ? current.peers : []
|
|
350
|
+
document.getElementById('anywhere-peers').innerHTML = peers.length === 0
|
|
351
|
+
? `<p class='anywhere-muted'>No other devices yet. Sign in to Tailscale on your phone with the same account.</p>`
|
|
352
|
+
: peers.map((peer) => `
|
|
353
|
+
<div class='anywhere-row'>
|
|
354
|
+
<span class='anywhere-dot ${peer.online ? 'online' : ''}'></span>
|
|
355
|
+
<i class='${osIcon(peer.os)}'></i>
|
|
356
|
+
<div class='grow'>
|
|
357
|
+
<div class='title'>${esc(peer.name)} <span class='anywhere-badge'>${esc(peer.os || 'unknown')}</span></div>
|
|
358
|
+
<div class='sub'>${peer.online ? 'online' : (peer.lastSeen ? 'last seen ' + esc(new Date(peer.lastSeen).toLocaleString()) : 'offline')}${peer.ips && peer.ips.length ? ' · ' + esc(peer.ips[0]) : ''}</div>
|
|
359
|
+
</div>
|
|
360
|
+
</div>`).join('')
|
|
361
|
+
document.getElementById('anywhere-dnsname').textContent = current.dnsName || '—'
|
|
362
|
+
document.getElementById('anywhere-ips').textContent = (current.ips || []).join(', ') || '—'
|
|
363
|
+
document.getElementById('anywhere-expiry').textContent = current.keyExpired ? 'expired — sign in again' : formatDate(current.keyExpiry)
|
|
364
|
+
document.getElementById('anywhere-version').textContent = current.version ? `tailscaled ${current.version}` : '—'
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const poll = async () => {
|
|
369
|
+
try {
|
|
370
|
+
const response = await fetch('/anywhere/status', { cache: 'no-store' })
|
|
371
|
+
if (response.ok) {
|
|
372
|
+
render(await response.json())
|
|
373
|
+
}
|
|
374
|
+
} catch (e) { }
|
|
375
|
+
const phase = current.phase || ''
|
|
376
|
+
const delay = (phase === 'ready' || phase === 'off') ? 5000 : 2000
|
|
377
|
+
timer = setTimeout(poll, delay)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const act = async (action) => {
|
|
381
|
+
if (busy) return
|
|
382
|
+
busy = true
|
|
383
|
+
switchEl.dataset.busy = 'true'
|
|
384
|
+
try {
|
|
385
|
+
const response = await fetch(`/anywhere/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
|
386
|
+
const data = await response.json()
|
|
387
|
+
if (data && data.status) {
|
|
388
|
+
render(data.status)
|
|
389
|
+
} else if (data && data.error) {
|
|
390
|
+
render(Object.assign({}, current, { phase: 'error', error: data.error }))
|
|
391
|
+
}
|
|
392
|
+
} catch (e) {
|
|
393
|
+
render(Object.assign({}, current, { phase: 'error', error: e.message }))
|
|
394
|
+
} finally {
|
|
395
|
+
busy = false
|
|
396
|
+
switchEl.dataset.busy = 'false'
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
root.addEventListener('click', (event) => {
|
|
401
|
+
const button = event.target.closest('[data-action]')
|
|
402
|
+
if (button) {
|
|
403
|
+
event.preventDefault()
|
|
404
|
+
const action = button.dataset.action
|
|
405
|
+
if (action === 'logout') {
|
|
406
|
+
Swal.fire({
|
|
407
|
+
title: 'Sign out of Tailscale?',
|
|
408
|
+
text: 'Your phone will no longer be able to reach this Pinokio until you sign in again.',
|
|
409
|
+
showCancelButton: true,
|
|
410
|
+
confirmButtonText: 'Sign out'
|
|
411
|
+
}).then((result) => { if (result.isConfirmed) act('logout') })
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
act(action)
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
const link = event.target.closest('a[target=_blank]')
|
|
418
|
+
if (link && agent === 'electron') {
|
|
419
|
+
event.preventDefault()
|
|
420
|
+
openExternal(link.href)
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
switchEl.addEventListener('click', () => {
|
|
424
|
+
if (switchEl.dataset.on === 'true') {
|
|
425
|
+
act('stop')
|
|
426
|
+
} else {
|
|
427
|
+
act('start')
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
render(current)
|
|
432
|
+
timer = setTimeout(poll, 1500)
|
|
433
|
+
})()
|
|
434
|
+
</script>
|
|
435
|
+
</body>
|
|
436
|
+
</html>
|
package/server/views/app.ejs
CHANGED
|
@@ -12963,7 +12963,9 @@ const rerenderMenuSection = (container, html) => {
|
|
|
12963
12963
|
let origin = event.origin
|
|
12964
12964
|
if (origin) {
|
|
12965
12965
|
let port = new URL(origin).port || 80
|
|
12966
|
-
|
|
12966
|
+
// a frame on the page's own origin is always Pinokio's (e.g. over Pinokio Anywhere, where
|
|
12967
|
+
// the https default port makes the port comparison below fail)
|
|
12968
|
+
if (origin === location.origin || String(port) === String(location.port) || /https:\/\/.*pinokio\..*(localhost|co)/.test(origin)) {
|
|
12967
12969
|
//if (String(port) === "<%=port%>" || /https:\/\/pinokio\..*localhost/.test(origin)) {
|
|
12968
12970
|
//if (String(port) === "<%=port%>") {
|
|
12969
12971
|
if (event.data) {
|
package/server/views/index.ejs
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
<title>Pinokio</title>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
6
|
+
<% if (locals.anywhere_host) { %>
|
|
7
|
+
<link rel="manifest" href="/anywhere.webmanifest">
|
|
8
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
9
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
10
|
+
<meta name="apple-mobile-web-app-title" content="Pinokio">
|
|
11
|
+
<% } %>
|
|
6
12
|
<link href="/xterm.min.css" rel="stylesheet" />
|
|
7
13
|
<link href="/css/fontawesome.min.css" rel="stylesheet">
|
|
8
14
|
<link href="/css/solid.min.css" rel="stylesheet">
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
<div class='main-sidebar-section' aria-label="Configure">
|
|
41
41
|
<div class="main-sidebar-section-title">Configure</div>
|
|
42
42
|
<a href="/network" class="tab <%= sidebarSelected === 'network' ? 'selected' : '' %>" data-main-sidebar-home-server-tab><i class="fa-solid fa-wifi"></i><div class='caption'>Home Server</div><span class="main-sidebar-status-badge" data-main-sidebar-home-server-status aria-hidden="true" hidden></span></a>
|
|
43
|
+
<a href="/anywhere" class="tab <%= sidebarSelected === 'anywhere' ? 'selected' : '' %>" data-main-sidebar-anywhere-tab><i class="fa-solid fa-mobile-screen"></i><div class='caption'>Pinokio Anywhere</div><span class="main-sidebar-status-badge" data-main-sidebar-anywhere-status aria-hidden="true" hidden></span></a>
|
|
43
44
|
<a class="tab <%= sidebarSelected === 'autolaunch' ? 'selected' : '' %>" href="/autolaunch"><i class="fa-solid fa-power-off"></i><div class='caption'>Autolaunch</div></a>
|
|
44
45
|
<a href="/connect" class="tab <%= sidebarSelected === 'connect' ? 'selected' : '' %>"><i class="fa-solid fa-plug"></i><div class='caption'>Login</div></a>
|
|
45
46
|
<a class="tab <%= sidebarSelected === 'settings' ? 'selected' : '' %>" href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
|
|
@@ -121,6 +122,44 @@
|
|
|
121
122
|
|
|
122
123
|
initHomeServerStatusBadge();
|
|
123
124
|
|
|
125
|
+
const initAnywhereStatusBadge = () => {
|
|
126
|
+
if (!sidebar || window.__pinokioMainSidebarAnywhereStatusInit) return;
|
|
127
|
+
const tab = sidebar.querySelector("[data-main-sidebar-anywhere-tab]");
|
|
128
|
+
const badge = sidebar.querySelector("[data-main-sidebar-anywhere-status]");
|
|
129
|
+
if (!tab || !badge) return;
|
|
130
|
+
window.__pinokioMainSidebarAnywhereStatusInit = true;
|
|
131
|
+
const render = (data) => {
|
|
132
|
+
const phase = data && data.phase ? String(data.phase) : "off";
|
|
133
|
+
if (phase === "setup") {
|
|
134
|
+
badge.hidden = true;
|
|
135
|
+
delete badge.dataset.state;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const on = phase === "ready";
|
|
139
|
+
const label = on ? "ON" : (phase === "off" || phase === "error" ? "OFF" : "…");
|
|
140
|
+
badge.textContent = label;
|
|
141
|
+
badge.hidden = false;
|
|
142
|
+
badge.dataset.state = on ? "on" : "off";
|
|
143
|
+
tab.dataset.anywhereStatus = phase;
|
|
144
|
+
tab.setAttribute("title", `Pinokio Anywhere ${label}`);
|
|
145
|
+
};
|
|
146
|
+
document.addEventListener("pinokio:anywhere-status", (event) => {
|
|
147
|
+
const detail = event && event.detail ? event.detail : {};
|
|
148
|
+
render(detail.payload || detail);
|
|
149
|
+
});
|
|
150
|
+
fetch("/anywhere/status?light=1", { cache: "no-store" })
|
|
151
|
+
.then((response) => {
|
|
152
|
+
if (!response.ok) throw new Error(`Anywhere status failed: ${response.status}`);
|
|
153
|
+
return response.json();
|
|
154
|
+
})
|
|
155
|
+
.then(render)
|
|
156
|
+
.catch(() => {
|
|
157
|
+
badge.hidden = true;
|
|
158
|
+
delete badge.dataset.state;
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
initAnywhereStatusBadge();
|
|
162
|
+
|
|
124
163
|
if (body && sidebar && headerToggle && !headerToggle.dataset.mainSidebarHeaderToggle) {
|
|
125
164
|
headerToggle.dataset.mainSidebarHeaderToggle = "true";
|
|
126
165
|
headerToggle.classList.add("sidebar-toggle", "main-sidebar-header-toggle");
|