pinokiod 5.1.5 → 5.1.11
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/fs/download_worker.js +158 -0
- package/kernel/api/fs/index.js +95 -91
- package/kernel/api/index.js +3 -0
- package/kernel/bin/index.js +5 -2
- package/kernel/environment.js +19 -2
- package/kernel/git.js +972 -1
- package/kernel/index.js +65 -30
- package/kernel/peer.js +1 -2
- package/kernel/plugin.js +0 -8
- package/kernel/procs.js +92 -36
- package/kernel/prototype.js +45 -22
- package/kernel/shells.js +30 -6
- package/kernel/sysinfo.js +33 -13
- package/kernel/util.js +61 -24
- package/kernel/workspace_status.js +131 -7
- package/package.json +1 -1
- package/pipe/index.js +1 -1
- package/server/index.js +1173 -348
- package/server/public/create-launcher.js +157 -2
- package/server/public/install.js +135 -41
- package/server/public/style.css +32 -1
- package/server/public/tab-link-popover.js +45 -14
- package/server/public/terminal-settings.js +51 -35
- package/server/public/urldropdown.css +89 -3
- package/server/socket.js +12 -7
- package/server/views/agents.ejs +4 -3
- package/server/views/app.ejs +798 -30
- package/server/views/bootstrap.ejs +2 -1
- package/server/views/checkpoints.ejs +1014 -0
- package/server/views/checkpoints_registry_beta.ejs +260 -0
- package/server/views/columns.ejs +4 -4
- package/server/views/connect.ejs +1 -0
- package/server/views/d.ejs +74 -4
- package/server/views/download.ejs +28 -28
- package/server/views/editor.ejs +4 -5
- package/server/views/env_editor.ejs +1 -1
- package/server/views/file_explorer.ejs +1 -1
- package/server/views/index.ejs +3 -1
- package/server/views/init/index.ejs +2 -1
- package/server/views/install.ejs +2 -1
- package/server/views/net.ejs +9 -7
- package/server/views/network.ejs +15 -14
- package/server/views/pro.ejs +5 -2
- package/server/views/prototype/index.ejs +2 -1
- package/server/views/registry_link.ejs +76 -0
- package/server/views/rows.ejs +4 -4
- package/server/views/screenshots.ejs +1 -0
- package/server/views/settings.ejs +1 -0
- package/server/views/shell.ejs +4 -6
- package/server/views/terminal.ejs +528 -38
- package/server/views/tools.ejs +1 -0
- package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/1764297248545 +0 -45
- package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/events +0 -4
- package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/latest +0 -45
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<head>
|
|
3
|
+
<meta charset="UTF-8">
|
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
5
|
+
<link href="/xterm.min.css" rel="stylesheet" />
|
|
6
|
+
<link href="/css/fontawesome.min.css" rel="stylesheet">
|
|
7
|
+
<link href="/css/solid.min.css" rel="stylesheet">
|
|
8
|
+
<link href="/css/regular.min.css" rel="stylesheet">
|
|
9
|
+
<link href="/css/brands.min.css" rel="stylesheet">
|
|
10
|
+
<link href="/markdown.css" rel="stylesheet"/>
|
|
11
|
+
<link href="/noty.css" rel="stylesheet"/>
|
|
12
|
+
<link href="/style.css" rel="stylesheet"/>
|
|
13
|
+
<link href="/urldropdown.css" rel="stylesheet" />
|
|
14
|
+
<% if (agent === "electron") { %>
|
|
15
|
+
<link href="/electron.css" rel="stylesheet"/>
|
|
16
|
+
<% } %>
|
|
17
|
+
<style>
|
|
18
|
+
main {
|
|
19
|
+
display: flex;
|
|
20
|
+
}
|
|
21
|
+
aside {
|
|
22
|
+
width: 200px;
|
|
23
|
+
display: block;
|
|
24
|
+
flex-shrink: 0;
|
|
25
|
+
}
|
|
26
|
+
aside .tab i {
|
|
27
|
+
width: 20px;
|
|
28
|
+
text-align: center;
|
|
29
|
+
}
|
|
30
|
+
body.dark aside .tab {
|
|
31
|
+
color: white;
|
|
32
|
+
}
|
|
33
|
+
body.dark aside .tab:hover, aside .tab:hover {
|
|
34
|
+
color: rgba(127, 91, 243, 0.9) !important;
|
|
35
|
+
opacity: 1;
|
|
36
|
+
}
|
|
37
|
+
aside .tab {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: 5px;
|
|
41
|
+
color: black;
|
|
42
|
+
text-decoration: none;
|
|
43
|
+
padding: 10px;
|
|
44
|
+
font-size: 12px;
|
|
45
|
+
opacity: 0.5;
|
|
46
|
+
border-left: 10px solid transparent;
|
|
47
|
+
}
|
|
48
|
+
body.dark aside .tab.selected {
|
|
49
|
+
color: white;
|
|
50
|
+
}
|
|
51
|
+
aside .tab.selected {
|
|
52
|
+
font-weight: bold;
|
|
53
|
+
opacity: 1;
|
|
54
|
+
}
|
|
55
|
+
@media only screen and (max-width: 600px) {
|
|
56
|
+
aside {
|
|
57
|
+
width: unset;
|
|
58
|
+
flex-shrink: unset;
|
|
59
|
+
}
|
|
60
|
+
aside {
|
|
61
|
+
padding: 0 10px;
|
|
62
|
+
}
|
|
63
|
+
aside .tab.submenu {
|
|
64
|
+
padding: 10px;
|
|
65
|
+
}
|
|
66
|
+
aside .tab i {
|
|
67
|
+
width: 100%;
|
|
68
|
+
}
|
|
69
|
+
aside .tab .caption {
|
|
70
|
+
display: none;
|
|
71
|
+
}
|
|
72
|
+
aside .tab {
|
|
73
|
+
margin: 0;
|
|
74
|
+
padding: 10px;
|
|
75
|
+
border-left: none;
|
|
76
|
+
}
|
|
77
|
+
aside .btn-tab {
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
padding: 10px 0;
|
|
80
|
+
}
|
|
81
|
+
aside .btn-tab .btn {
|
|
82
|
+
display: flex;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
}
|
|
85
|
+
aside .btn-tab .btn .caption {
|
|
86
|
+
display: none;
|
|
87
|
+
}
|
|
88
|
+
header .flexible {
|
|
89
|
+
min-width: unset;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
@media only screen and (max-width: 480px) {
|
|
93
|
+
.btn2 {
|
|
94
|
+
padding: 5px;
|
|
95
|
+
font-size: 11px;
|
|
96
|
+
}
|
|
97
|
+
.nav-btns {
|
|
98
|
+
flex-grow: 1;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
padding: 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
.backups-container {
|
|
104
|
+
padding: 20px;
|
|
105
|
+
}
|
|
106
|
+
.backups-container h2 {
|
|
107
|
+
font-size: 24px;
|
|
108
|
+
margin-bottom: 15px;
|
|
109
|
+
}
|
|
110
|
+
.backup-meta {
|
|
111
|
+
font-size: 12px;
|
|
112
|
+
opacity: 0.75;
|
|
113
|
+
}
|
|
114
|
+
body.dark .swal2-popup.backup-modal-shell, .swal2-popup.backup-modal-shell {
|
|
115
|
+
border: none;
|
|
116
|
+
background: transparent;
|
|
117
|
+
box-shadow: none;
|
|
118
|
+
padding: 0;
|
|
119
|
+
width: auto;
|
|
120
|
+
}
|
|
121
|
+
.backup-modal-wrapper {
|
|
122
|
+
display: flex;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
}
|
|
125
|
+
.backup-modal-content {
|
|
126
|
+
text-align: left;
|
|
127
|
+
width: min(640px, calc(100vw - 48px));
|
|
128
|
+
gap: 14px;
|
|
129
|
+
position: relative;
|
|
130
|
+
max-height: calc(100vh - 160px);
|
|
131
|
+
max-height: calc(100dvh - 160px);
|
|
132
|
+
min-height: 0;
|
|
133
|
+
}
|
|
134
|
+
.backup-modal-header h3 {
|
|
135
|
+
margin: 0 0 4px 0;
|
|
136
|
+
font-size: 22px;
|
|
137
|
+
font-weight: 700;
|
|
138
|
+
}
|
|
139
|
+
.backup-modal-description {
|
|
140
|
+
margin: 0 0 6px 0;
|
|
141
|
+
font-size: 14px;
|
|
142
|
+
color: rgba(71, 85, 105, 0.78);
|
|
143
|
+
}
|
|
144
|
+
body.dark .backup-modal-description {
|
|
145
|
+
color: rgba(203, 213, 225, 0.88);
|
|
146
|
+
}
|
|
147
|
+
.backup-modal.snapshot-options {
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
gap: 10px;
|
|
151
|
+
flex: 1 1 auto;
|
|
152
|
+
min-height: 0;
|
|
153
|
+
overflow-y: auto;
|
|
154
|
+
padding: 2px 6px 6px 2px;
|
|
155
|
+
overscroll-behavior: contain;
|
|
156
|
+
}
|
|
157
|
+
.line.backup-row {
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
transition: background 0.15s ease, box-shadow 0.15s ease;
|
|
160
|
+
}
|
|
161
|
+
.line.backup-row:hover {
|
|
162
|
+
background: rgba(127, 91, 243, 0.05);
|
|
163
|
+
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.08);
|
|
164
|
+
}
|
|
165
|
+
.snapshot-option {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: flex-start;
|
|
168
|
+
gap: 12px;
|
|
169
|
+
font-size: 14px;
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
margin: 0;
|
|
172
|
+
}
|
|
173
|
+
.snapshot-card {
|
|
174
|
+
flex: 1;
|
|
175
|
+
border: 1px solid rgba(15, 23, 42, 0.12);
|
|
176
|
+
border-radius: 6px;
|
|
177
|
+
padding: 12px;
|
|
178
|
+
background: rgba(248, 250, 255, 0.9);
|
|
179
|
+
/*
|
|
180
|
+
box-shadow: 0 5px 12px rgba(15, 23, 42, 0.1);
|
|
181
|
+
*/
|
|
182
|
+
}
|
|
183
|
+
.snapshot-card.snapshot-card--with-repos {
|
|
184
|
+
align-items: flex-start;
|
|
185
|
+
}
|
|
186
|
+
body.dark .snapshot-card {
|
|
187
|
+
background: rgba(15, 23, 42, 0.86);
|
|
188
|
+
border-color: rgba(148, 163, 184, 0.2);
|
|
189
|
+
}
|
|
190
|
+
.snapshot-header {
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
justify-content: space-between;
|
|
194
|
+
gap: 8px;
|
|
195
|
+
margin-bottom: 6px;
|
|
196
|
+
}
|
|
197
|
+
.snapshot-card.snapshot-card--with-repos .snapshot-header {
|
|
198
|
+
grid-column: 1 / 3;
|
|
199
|
+
}
|
|
200
|
+
.snapshot-title {
|
|
201
|
+
font-weight: 700;
|
|
202
|
+
font-size: 15px;
|
|
203
|
+
display: flex;
|
|
204
|
+
align-items: center;
|
|
205
|
+
gap: 6px;
|
|
206
|
+
}
|
|
207
|
+
.snapshot-meta {
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
opacity: 0.75;
|
|
210
|
+
}
|
|
211
|
+
.snapshot-card.snapshot-card--with-repos .snapshot-meta {
|
|
212
|
+
grid-column: 1 / 3;
|
|
213
|
+
margin-bottom: 4px;
|
|
214
|
+
}
|
|
215
|
+
.snapshot-actions {
|
|
216
|
+
display: flex;
|
|
217
|
+
justify-content: flex-end;
|
|
218
|
+
margin-top: 10px;
|
|
219
|
+
gap: 10px;
|
|
220
|
+
flex-wrap: wrap;
|
|
221
|
+
align-items: center;
|
|
222
|
+
}
|
|
223
|
+
.snapshot-header .snapshot-actions {
|
|
224
|
+
margin-top: 0;
|
|
225
|
+
}
|
|
226
|
+
.snapshot-actions .snapshot-install {
|
|
227
|
+
padding: 8px 16px;
|
|
228
|
+
border-radius: 6px;
|
|
229
|
+
font-size: 14px;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
}
|
|
232
|
+
.snapshot-option input[type="radio"] {
|
|
233
|
+
margin-top: 6px;
|
|
234
|
+
}
|
|
235
|
+
.repo-list {
|
|
236
|
+
margin-top: 10px;
|
|
237
|
+
}
|
|
238
|
+
.snapshot-card.snapshot-card--with-repos .repo-list {
|
|
239
|
+
border-top: 1px solid rgba(0,0,0,0.06);
|
|
240
|
+
}
|
|
241
|
+
body.dark .repo-line {
|
|
242
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
243
|
+
}
|
|
244
|
+
.repo-line {
|
|
245
|
+
display: flex;
|
|
246
|
+
flex-direction: column;
|
|
247
|
+
gap: 2px;
|
|
248
|
+
align-items: flex-start;
|
|
249
|
+
font-size: 14px;
|
|
250
|
+
padding: 8px 0;
|
|
251
|
+
border-bottom: 1px solid rgba(0,0,0,0.05);
|
|
252
|
+
}
|
|
253
|
+
.repo-line:last-child {
|
|
254
|
+
border-bottom: none;
|
|
255
|
+
}
|
|
256
|
+
.repo-line-main {
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
gap: 8px;
|
|
260
|
+
}
|
|
261
|
+
.repo-path {
|
|
262
|
+
font-weight: 700;
|
|
263
|
+
}
|
|
264
|
+
.repo-commit code {
|
|
265
|
+
background: rgba(0,0,0,0.05);
|
|
266
|
+
padding: 3px 6px;
|
|
267
|
+
border-radius: 4px;
|
|
268
|
+
font-size: 12px;
|
|
269
|
+
}
|
|
270
|
+
body.dark .repo-message {
|
|
271
|
+
color: silver;
|
|
272
|
+
}
|
|
273
|
+
.repo-message {
|
|
274
|
+
color: #333;
|
|
275
|
+
}
|
|
276
|
+
.repo-person {
|
|
277
|
+
font-style: italic;
|
|
278
|
+
opacity: 0.75;
|
|
279
|
+
}
|
|
280
|
+
.installed-list {
|
|
281
|
+
margin-top: 8px;
|
|
282
|
+
font-size: 12px;
|
|
283
|
+
display: flex;
|
|
284
|
+
flex-wrap: wrap;
|
|
285
|
+
gap: 6px;
|
|
286
|
+
align-items: center;
|
|
287
|
+
}
|
|
288
|
+
.snapshot-card.snapshot-card--with-repos .installed-list {
|
|
289
|
+
grid-column: 2 / 3;
|
|
290
|
+
}
|
|
291
|
+
.installed-list span {
|
|
292
|
+
font-weight: 700;
|
|
293
|
+
}
|
|
294
|
+
.installed-link {
|
|
295
|
+
color: rgba(127, 91, 243, 0.95);
|
|
296
|
+
text-decoration: none;
|
|
297
|
+
}
|
|
298
|
+
.installed-link:hover {
|
|
299
|
+
text-decoration: underline;
|
|
300
|
+
}
|
|
301
|
+
.backup-modal.folder-row {
|
|
302
|
+
margin-top: 10px;
|
|
303
|
+
}
|
|
304
|
+
.backup-modal label {
|
|
305
|
+
display: block;
|
|
306
|
+
font-weight: 600;
|
|
307
|
+
margin-bottom: 5px;
|
|
308
|
+
}
|
|
309
|
+
.backup-modal input[type="text"] {
|
|
310
|
+
width: 100%;
|
|
311
|
+
padding: 12px 14px;
|
|
312
|
+
border: 1px solid rgba(15, 23, 42, 0.12);
|
|
313
|
+
border-radius: 12px;
|
|
314
|
+
box-sizing: border-box;
|
|
315
|
+
}
|
|
316
|
+
body.dark .backup-modal input[type="text"] {
|
|
317
|
+
border-color: rgba(148, 163, 184, 0.3);
|
|
318
|
+
background: rgba(148, 163, 184, 0.14);
|
|
319
|
+
color: rgba(226, 232, 240, 0.95);
|
|
320
|
+
}
|
|
321
|
+
.backup-modal.hidden,
|
|
322
|
+
.backup-loading.hidden {
|
|
323
|
+
display: none;
|
|
324
|
+
}
|
|
325
|
+
.folder-hint {
|
|
326
|
+
font-size: 12px;
|
|
327
|
+
color: #c0392b;
|
|
328
|
+
margin-top: 6px;
|
|
329
|
+
}
|
|
330
|
+
.backup-loading {
|
|
331
|
+
position: absolute;
|
|
332
|
+
inset: 0;
|
|
333
|
+
display: flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
justify-content: center;
|
|
336
|
+
gap: 10px;
|
|
337
|
+
background: rgba(15, 23, 42, 0.06);
|
|
338
|
+
border-radius: 18px;
|
|
339
|
+
backdrop-filter: blur(2px);
|
|
340
|
+
z-index: 10;
|
|
341
|
+
}
|
|
342
|
+
body.dark .backup-loading {
|
|
343
|
+
background: rgba(15, 23, 42, 0.2);
|
|
344
|
+
}
|
|
345
|
+
.backup-loading.hidden {
|
|
346
|
+
display: none;
|
|
347
|
+
}
|
|
348
|
+
.backup-loading .spinner {
|
|
349
|
+
width: 18px;
|
|
350
|
+
height: 18px;
|
|
351
|
+
border: 3px solid rgba(15, 23, 42, 0.15);
|
|
352
|
+
border-top-color: rgba(127, 91, 243, 0.95);
|
|
353
|
+
border-radius: 50%;
|
|
354
|
+
animation: backup-spin 0.8s linear infinite;
|
|
355
|
+
}
|
|
356
|
+
@keyframes backup-spin {
|
|
357
|
+
to { transform: rotate(360deg); }
|
|
358
|
+
}
|
|
359
|
+
.backup-loading .backup-loading-text {
|
|
360
|
+
font-weight: 600;
|
|
361
|
+
color: rgba(15, 23, 42, 0.9);
|
|
362
|
+
}
|
|
363
|
+
body.dark .backup-loading .backup-loading-text {
|
|
364
|
+
color: rgba(226, 232, 240, 0.95);
|
|
365
|
+
}
|
|
366
|
+
</style>
|
|
367
|
+
<script src="/window_storage.js"></script>
|
|
368
|
+
<script src="/hotkeys.min.js"></script>
|
|
369
|
+
<script src="/sweetalert2.js"></script>
|
|
370
|
+
<script src="/noty.js"></script>
|
|
371
|
+
<script src="/notyq.js"></script>
|
|
372
|
+
<script src="/xterm.js"></script>
|
|
373
|
+
<script src="/xterm-addon-fit.js"></script>
|
|
374
|
+
<script src="/xterm-addon-web-links.js"></script>
|
|
375
|
+
<script src="/xterm-theme.js"></script>
|
|
376
|
+
<script src="/Socket.js"></script>
|
|
377
|
+
<script src="/common.js"></script>
|
|
378
|
+
<script src="/opener.js"></script>
|
|
379
|
+
<script src="/nav.js"></script>
|
|
380
|
+
<script src="/urldropdown.js"></script>
|
|
381
|
+
<script src="/report.js"></script>
|
|
382
|
+
<script>
|
|
383
|
+
window.backupItems = <%- JSON.stringify(items || []) %>;
|
|
384
|
+
window.backupAutoInstall = <%- JSON.stringify(autoInstall || null) %>;
|
|
385
|
+
window.backupImportError = <%- JSON.stringify(importError || null) %>;
|
|
386
|
+
window.registryBetaEnabled = <%- JSON.stringify(!!registryBetaEnabled) %>;
|
|
387
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
388
|
+
const items = Array.isArray(window.backupItems) ? window.backupItems : []
|
|
389
|
+
const autoInstall = window.backupAutoInstall && typeof window.backupAutoInstall === "object" ? window.backupAutoInstall : null
|
|
390
|
+
const importError = typeof window.backupImportError === "string" && window.backupImportError.trim() ? window.backupImportError.trim() : null
|
|
391
|
+
const registryBetaEnabled = window.registryBetaEnabled === true
|
|
392
|
+
|
|
393
|
+
const escapeHtml = (value) => {
|
|
394
|
+
const str = value == null ? '' : String(value)
|
|
395
|
+
return str.replace(/[&<>"']/g, (ch) => {
|
|
396
|
+
switch (ch) {
|
|
397
|
+
case '&': return '&'
|
|
398
|
+
case '<': return '<'
|
|
399
|
+
case '>': return '>'
|
|
400
|
+
case '"': return '"'
|
|
401
|
+
case "'": return '''
|
|
402
|
+
default: return ch
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
const escapeAttr = (value) => escapeHtml(value)
|
|
407
|
+
|
|
408
|
+
const guessFolderName = (remoteUrl) => {
|
|
409
|
+
if (!remoteUrl || typeof remoteUrl !== "string") return "project"
|
|
410
|
+
let str = remoteUrl.replace(/\.git$/i, '')
|
|
411
|
+
const atIdx = str.lastIndexOf('@')
|
|
412
|
+
if (atIdx !== -1) {
|
|
413
|
+
str = str.slice(atIdx + 1)
|
|
414
|
+
}
|
|
415
|
+
const parts = str.split(/[/:\\]/).filter(Boolean)
|
|
416
|
+
const last = parts.length ? parts[parts.length - 1] : "project"
|
|
417
|
+
const safe = last.replace(/[^a-zA-Z0-9._-]/g, '-')
|
|
418
|
+
return safe || "project"
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const formatPerson = (person) => {
|
|
422
|
+
if (!person) return null
|
|
423
|
+
const name = person.name || ""
|
|
424
|
+
let date = null
|
|
425
|
+
if (Number.isFinite(person.timestamp)) {
|
|
426
|
+
try {
|
|
427
|
+
date = new Date(person.timestamp * 1000).toLocaleString()
|
|
428
|
+
} catch (_) {}
|
|
429
|
+
}
|
|
430
|
+
const label = name.trim()
|
|
431
|
+
if (label && date) return `${label} · ${date}`
|
|
432
|
+
if (label) return label
|
|
433
|
+
if (date) return date
|
|
434
|
+
return null
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const renderSnapshotOption = (snap, isLatest, installedNames) => {
|
|
438
|
+
if (isLatest) {
|
|
439
|
+
return `
|
|
440
|
+
<div class="snapshot-option">
|
|
441
|
+
<div class="snapshot-card snapshot-card--latest" data-snapshot-id="latest">
|
|
442
|
+
<div class="snapshot-header">
|
|
443
|
+
<div class="snapshot-title"><i class="fa-solid fa-clock-rotate-left"></i> Latest</div>
|
|
444
|
+
<div class="snapshot-actions">
|
|
445
|
+
<button type="button" class="url-modal-button confirm snapshot-install" data-snapshot-id="latest">Install</button>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
<div class="snapshot-meta">Clone the current default branch HEAD.</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
`
|
|
452
|
+
}
|
|
453
|
+
const repos = Array.isArray(snap.repos) ? snap.repos : []
|
|
454
|
+
const repoList = repos.map((repo) => {
|
|
455
|
+
const hash = escapeHtml(repo.commit ? repo.commit.slice(0, 8) : "unknown")
|
|
456
|
+
const message = escapeHtml(repo.message ? repo.message.split('\n')[0].trim() : "(no message recorded)")
|
|
457
|
+
const person = formatPerson(repo.author || repo.committer)
|
|
458
|
+
const personLabel = person ? escapeHtml(person) : null
|
|
459
|
+
const pathLabel = escapeHtml(repo.path || '.')
|
|
460
|
+
return `
|
|
461
|
+
<div class="repo-line">
|
|
462
|
+
<div class="repo-line-main">
|
|
463
|
+
<div class="repo-path">${pathLabel}</div>
|
|
464
|
+
<div class="repo-commit"><code>${hash}</code></div>
|
|
465
|
+
</div>
|
|
466
|
+
<div class="repo-message">${message}</div>
|
|
467
|
+
${personLabel ? `<div class="repo-person">${personLabel}</div>` : ''}
|
|
468
|
+
</div>
|
|
469
|
+
`
|
|
470
|
+
}).join("")
|
|
471
|
+
const label = "Checkpoint"
|
|
472
|
+
const snapTimestamp = snap && snap.id != null ? Number(snap.id) : Number.NaN
|
|
473
|
+
const whenRaw = Number.isFinite(snapTimestamp) && snapTimestamp > 0 ? new Date(snapTimestamp).toLocaleString() : ""
|
|
474
|
+
const when = whenRaw ? escapeHtml(whenRaw) : ""
|
|
475
|
+
const plat = snap && snap.platform ? escapeHtml(String(snap.platform)) : ""
|
|
476
|
+
const arch = snap && snap.arch ? escapeHtml(String(snap.arch)) : ""
|
|
477
|
+
const gpu = snap && snap.gpu ? escapeHtml(String(snap.gpu)) : ""
|
|
478
|
+
const ramVal = snap && typeof snap.ram === "number" ? snap.ram : null
|
|
479
|
+
const vramVal = snap && typeof snap.vram === "number" ? snap.vram : null
|
|
480
|
+
const envParts = []
|
|
481
|
+
if (plat) envParts.push(plat)
|
|
482
|
+
if (arch) envParts.push(arch)
|
|
483
|
+
if (gpu) envParts.push(gpu)
|
|
484
|
+
if (ramVal != null && ramVal > 0) envParts.push(`${ramVal} GB RAM`)
|
|
485
|
+
if (vramVal != null && vramVal > 0) envParts.push(`${vramVal} GB VRAM`)
|
|
486
|
+
const envText = envParts.join(" · ")
|
|
487
|
+
const metaParts = []
|
|
488
|
+
metaParts.push(`${repos.length} repo${repos.length === 1 ? '' : 's'}`)
|
|
489
|
+
if (when) metaParts.push(when)
|
|
490
|
+
const metaText = metaParts.join(" · ")
|
|
491
|
+
const syncStatus = snap && snap.sync && snap.sync.status ? String(snap.sync.status) : null
|
|
492
|
+
const publishMarkup = registryBetaEnabled
|
|
493
|
+
? (syncStatus === "published"
|
|
494
|
+
? `<span class="badge">Cloud saved</span>`
|
|
495
|
+
: `<button type="button" class="url-modal-button confirm snapshot-publish" data-snapshot-id="${snap.id}">Save to Cloud</button>`)
|
|
496
|
+
: ""
|
|
497
|
+
const installed = Array.isArray(installedNames) ? installedNames : []
|
|
498
|
+
const installedMarkup = installed.length
|
|
499
|
+
? `<div class="installed-list"><span>Installed:</span> ${installed.map((name) => `<a href="/p/${encodeURIComponent(name)}" class="installed-link">/p/${escapeHtml(name)}</a>`).join(", ")}</div>`
|
|
500
|
+
: ""
|
|
501
|
+
return `
|
|
502
|
+
<div class="snapshot-option">
|
|
503
|
+
<div class="snapshot-card snapshot-card--with-repos" data-snapshot-id="${snap.id}">
|
|
504
|
+
<div class="snapshot-header">
|
|
505
|
+
<div class="snapshot-title"><i class="fa-regular fa-clock"></i> ${label}</div>
|
|
506
|
+
<div class="snapshot-actions">
|
|
507
|
+
${publishMarkup}
|
|
508
|
+
<button type="button" class="url-modal-button confirm snapshot-install" data-snapshot-id="${snap.id}">Install</button>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
<div class="snapshot-meta">
|
|
512
|
+
${envText ? `${envText} · ` : ""}${metaText}
|
|
513
|
+
</div>
|
|
514
|
+
<div class="repo-list">${repoList}</div>
|
|
515
|
+
${installedMarkup}
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
`
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const openInstallModal = (item, opts = {}) => {
|
|
522
|
+
const conflictNames = new Set((item.folders || []).map((f) => f.name))
|
|
523
|
+
const installedBySnapshot = item.installedBySnapshot || {}
|
|
524
|
+
const defaultFolder = (item.folders && item.folders[0] && item.folders[0].name) || guessFolderName(item.remoteUrl)
|
|
525
|
+
const suggestName = (base) => {
|
|
526
|
+
const clean = base && base.trim() ? base.trim() : defaultFolder
|
|
527
|
+
if (!conflictNames.has(clean)) return clean
|
|
528
|
+
let n = 2
|
|
529
|
+
let candidate = `${clean}-${n}`
|
|
530
|
+
while (conflictNames.has(candidate) && n < 100) {
|
|
531
|
+
n += 1
|
|
532
|
+
candidate = `${clean}-${n}`
|
|
533
|
+
}
|
|
534
|
+
return candidate
|
|
535
|
+
}
|
|
536
|
+
const snapshots = [renderSnapshotOption(null, true, (item.folders || []).map((f) => f.name))].concat(
|
|
537
|
+
(item.snapshots || []).map((snap) => renderSnapshotOption(snap, false, installedBySnapshot[snap.id]))
|
|
538
|
+
)
|
|
539
|
+
const html = `
|
|
540
|
+
<div class="backup-modal-wrapper">
|
|
541
|
+
<div class="url-modal-content backup-modal-content" role="dialog" aria-modal="true" aria-labelledby="backup-modal-title" aria-describedby="backup-modal-description">
|
|
542
|
+
<div class="backup-loading hidden" id="backup-loading">
|
|
543
|
+
<div class="spinner"></div>
|
|
544
|
+
<div class="backup-loading-text">Installing…</div>
|
|
545
|
+
</div>
|
|
546
|
+
<button type="button" class="url-modal-close" aria-label="Close">×</button>
|
|
547
|
+
<div class="backup-modal-header">
|
|
548
|
+
<h3 id="backup-modal-title">Install</h3>
|
|
549
|
+
<p class="backup-modal-description" id="backup-modal-description">Pick a version and, if needed, a new folder name.</p>
|
|
550
|
+
</div>
|
|
551
|
+
<div class="backup-modal snapshot-options">${snapshots.join("")}</div>
|
|
552
|
+
<div class="backup-modal folder-row hidden" id="folder-row">
|
|
553
|
+
<label for="folder-name">New folder name</label>
|
|
554
|
+
<input id="folder-name" type="text" value="${escapeAttr(defaultFolder)}" autocomplete="off" />
|
|
555
|
+
<div class="folder-hint" id="folder-hint"></div>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
`
|
|
560
|
+
Swal.fire({
|
|
561
|
+
html,
|
|
562
|
+
showConfirmButton: false,
|
|
563
|
+
showCancelButton: false,
|
|
564
|
+
customClass: {
|
|
565
|
+
popup: 'backup-modal-shell'
|
|
566
|
+
},
|
|
567
|
+
didOpen: () => {
|
|
568
|
+
const container = Swal.getHtmlContainer()
|
|
569
|
+
if (!container) return
|
|
570
|
+
const folderRow = container.querySelector("#folder-row")
|
|
571
|
+
const folderInput = container.querySelector("#folder-name")
|
|
572
|
+
const hint = container.querySelector("#folder-hint")
|
|
573
|
+
const closeBtn = container.querySelector('.url-modal-close')
|
|
574
|
+
const installButtons = Array.from(container.querySelectorAll(".snapshot-install"))
|
|
575
|
+
const loadingOverlay = container.querySelector("#backup-loading")
|
|
576
|
+
|
|
577
|
+
const setLoading = (isLoading) => {
|
|
578
|
+
if (loadingOverlay) {
|
|
579
|
+
loadingOverlay.classList.toggle("hidden", !isLoading)
|
|
580
|
+
}
|
|
581
|
+
installButtons.forEach((btn) => {
|
|
582
|
+
btn.disabled = isLoading
|
|
583
|
+
if (isLoading) btn.textContent = "Installing..."
|
|
584
|
+
})
|
|
585
|
+
if (closeBtn) closeBtn.disabled = isLoading
|
|
586
|
+
}
|
|
587
|
+
const openRenameModal = (baseName, message) => {
|
|
588
|
+
const suggested = escapeAttr(suggestName(baseName))
|
|
589
|
+
return new Promise((resolve) => {
|
|
590
|
+
const overlay = document.createElement("div")
|
|
591
|
+
overlay.className = "modal-overlay url-modal-overlay backup-rename-overlay is-visible"
|
|
592
|
+
overlay.innerHTML = `
|
|
593
|
+
<div class="url-modal-content backup-modal-content" role="dialog" aria-modal="true" aria-labelledby="backup-rename-title">
|
|
594
|
+
<button type="button" class="url-modal-close" aria-label="Close">×</button>
|
|
595
|
+
<div class="backup-modal-header">
|
|
596
|
+
<h3 id="backup-rename-title">Choose a new folder</h3>
|
|
597
|
+
<p class="backup-modal-description">${escapeHtml(message || "Pick a different folder name for this install.")}</p>
|
|
598
|
+
</div>
|
|
599
|
+
<div class="backup-modal folder-row" style="margin-top: 0;">
|
|
600
|
+
<label for="rename-folder-name">Folder name</label>
|
|
601
|
+
<input id="rename-folder-name" type="text" value="${suggested}" autocomplete="off" />
|
|
602
|
+
<div class="folder-hint" id="rename-folder-hint"></div>
|
|
603
|
+
</div>
|
|
604
|
+
<div class="url-modal-actions">
|
|
605
|
+
<button type="button" class="url-modal-button cancel" data-action="cancel">Cancel</button>
|
|
606
|
+
<button type="button" class="url-modal-button confirm" data-action="confirm">Use name</button>
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
`
|
|
610
|
+
document.body.appendChild(overlay)
|
|
611
|
+
const input = overlay.querySelector("#rename-folder-name")
|
|
612
|
+
const hintEl = overlay.querySelector("#rename-folder-hint")
|
|
613
|
+
const cancelBtn = overlay.querySelector('[data-action="cancel"]')
|
|
614
|
+
const confirmBtn = overlay.querySelector('[data-action="confirm"]')
|
|
615
|
+
const closeBtn = overlay.querySelector('.url-modal-close')
|
|
616
|
+
const cleanup = (value) => {
|
|
617
|
+
overlay.remove()
|
|
618
|
+
resolve(value)
|
|
619
|
+
}
|
|
620
|
+
const validate = () => {
|
|
621
|
+
const val = input.value.trim()
|
|
622
|
+
if (!val) {
|
|
623
|
+
hintEl.textContent = "Folder name required."
|
|
624
|
+
return null
|
|
625
|
+
}
|
|
626
|
+
if (val.includes('/') || val.includes('\\')) {
|
|
627
|
+
hintEl.textContent = "Folder name cannot contain slashes."
|
|
628
|
+
return null
|
|
629
|
+
}
|
|
630
|
+
hintEl.textContent = ""
|
|
631
|
+
return val
|
|
632
|
+
}
|
|
633
|
+
confirmBtn.addEventListener("click", () => {
|
|
634
|
+
const val = validate()
|
|
635
|
+
if (val) cleanup(val)
|
|
636
|
+
})
|
|
637
|
+
cancelBtn.addEventListener("click", () => cleanup(null))
|
|
638
|
+
closeBtn.addEventListener("click", () => cleanup(null))
|
|
639
|
+
input.addEventListener("keydown", (e) => {
|
|
640
|
+
if (e.key === "Enter") {
|
|
641
|
+
e.preventDefault()
|
|
642
|
+
confirmBtn.click()
|
|
643
|
+
}
|
|
644
|
+
if (e.key === "Escape") {
|
|
645
|
+
e.preventDefault()
|
|
646
|
+
cleanup(null)
|
|
647
|
+
}
|
|
648
|
+
})
|
|
649
|
+
setTimeout(() => {
|
|
650
|
+
input.focus()
|
|
651
|
+
input.select()
|
|
652
|
+
}, 0)
|
|
653
|
+
})
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const handleInstall = async (snapshotId, forcedName) => {
|
|
657
|
+
let name = forcedName || (folderInput ? folderInput.value.trim() : '') || defaultFolder
|
|
658
|
+
const alreadyInstalled = snapshotId !== 'latest' ? (installedBySnapshot[snapshotId] || []) : []
|
|
659
|
+
const nameConflict = conflictNames.has(name)
|
|
660
|
+
const sameFolder = snapshotId !== 'latest' && alreadyInstalled.includes(name)
|
|
661
|
+
if (!name || name.includes('/') || name.includes('\\') || nameConflict || sameFolder) {
|
|
662
|
+
const reason = nameConflict
|
|
663
|
+
? "Folder exists. Pick a different name."
|
|
664
|
+
: sameFolder
|
|
665
|
+
? `Snapshot already installed in: ${alreadyInstalled.join(", ")}. Choose a different folder.`
|
|
666
|
+
: "Folder name required."
|
|
667
|
+
const rename = await openRenameModal(name || defaultFolder, reason)
|
|
668
|
+
if (!rename) return
|
|
669
|
+
name = rename
|
|
670
|
+
}
|
|
671
|
+
const installBtn = installButtons.find((b) => b.dataset.snapshotId === String(snapshotId))
|
|
672
|
+
if (installBtn) {
|
|
673
|
+
installBtn.disabled = true
|
|
674
|
+
installBtn.textContent = "Installing..."
|
|
675
|
+
}
|
|
676
|
+
setLoading(true)
|
|
677
|
+
try {
|
|
678
|
+
const res = await fetch("/checkpoints/install", {
|
|
679
|
+
method: "POST",
|
|
680
|
+
headers: {
|
|
681
|
+
"Content-Type": "application/json",
|
|
682
|
+
"Accept": "application/json"
|
|
683
|
+
},
|
|
684
|
+
body: JSON.stringify({
|
|
685
|
+
remote: item.remoteUrl,
|
|
686
|
+
snapshotId,
|
|
687
|
+
folder: name
|
|
688
|
+
})
|
|
689
|
+
})
|
|
690
|
+
const payload = res && res.ok ? await res.json() : null
|
|
691
|
+
if (payload && payload.ok && payload.redirect) {
|
|
692
|
+
window.location = payload.redirect
|
|
693
|
+
} else {
|
|
694
|
+
if (payload && payload.code === "exists") {
|
|
695
|
+
const rename = await openRenameModal(name, "Folder exists. Pick a different name.")
|
|
696
|
+
if (rename) {
|
|
697
|
+
await handleInstall(snapshotId, rename)
|
|
698
|
+
}
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
const msg = payload && payload.error ? payload.error : "Install failed"
|
|
702
|
+
Swal.fire({ icon: "error", title: "Error", text: msg })
|
|
703
|
+
}
|
|
704
|
+
} catch (error) {
|
|
705
|
+
Swal.fire({ icon: "error", title: "Error", text: error && error.message ? error.message : "Install failed" })
|
|
706
|
+
} finally {
|
|
707
|
+
if (installBtn) {
|
|
708
|
+
installBtn.disabled = false
|
|
709
|
+
installBtn.textContent = "Install"
|
|
710
|
+
}
|
|
711
|
+
setLoading(false)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
installButtons.forEach((btn) => {
|
|
716
|
+
btn.addEventListener("click", () => {
|
|
717
|
+
const snapshotId = btn.getAttribute("data-snapshot-id") || 'latest'
|
|
718
|
+
handleInstall(snapshotId)
|
|
719
|
+
})
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
const publishButtons = Array.from(container.querySelectorAll(".snapshot-publish"))
|
|
723
|
+
|
|
724
|
+
const highlightSnapshotId = opts && opts.highlightSnapshotId != null ? String(opts.highlightSnapshotId) : null
|
|
725
|
+
setTimeout(() => {
|
|
726
|
+
let focusBtn = null
|
|
727
|
+
if (highlightSnapshotId) {
|
|
728
|
+
const card = container.querySelector(`.snapshot-card[data-snapshot-id="${highlightSnapshotId}"]`)
|
|
729
|
+
if (card) {
|
|
730
|
+
try {
|
|
731
|
+
card.scrollIntoView({ behavior: "smooth", block: "center" })
|
|
732
|
+
} catch (_) {}
|
|
733
|
+
try {
|
|
734
|
+
card.style.boxShadow = "0 0 0 2px rgba(127, 91, 243, 0.9)"
|
|
735
|
+
} catch (_) {}
|
|
736
|
+
focusBtn = card.querySelector(".snapshot-install")
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (!focusBtn && installButtons.length) {
|
|
740
|
+
focusBtn = installButtons[0]
|
|
741
|
+
}
|
|
742
|
+
if (focusBtn) {
|
|
743
|
+
try { focusBtn.focus() } catch (_) {}
|
|
744
|
+
}
|
|
745
|
+
}, 0)
|
|
746
|
+
|
|
747
|
+
const waitForRegistryLink = async () => {
|
|
748
|
+
const startedAt = Date.now()
|
|
749
|
+
while (Date.now() - startedAt < 120000) {
|
|
750
|
+
try {
|
|
751
|
+
const s = await fetch("/api/registry/status", { method: "GET", headers: { "Accept": "application/json" } })
|
|
752
|
+
if (s.ok) {
|
|
753
|
+
const data = await s.json()
|
|
754
|
+
if (data && data.linked) return true
|
|
755
|
+
}
|
|
756
|
+
} catch (_) {}
|
|
757
|
+
await new Promise((r) => setTimeout(r, 2000))
|
|
758
|
+
}
|
|
759
|
+
return false
|
|
760
|
+
}
|
|
761
|
+
const publishSnapshot = async (snapshotId, allowConnect = true) => {
|
|
762
|
+
const btn = publishButtons.find((b) => b.dataset.snapshotId === String(snapshotId))
|
|
763
|
+
const original = btn ? btn.textContent : "Save to Cloud"
|
|
764
|
+
if (btn) {
|
|
765
|
+
btn.disabled = true
|
|
766
|
+
btn.textContent = "Saving..."
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
const qs = new URLSearchParams()
|
|
770
|
+
qs.set("snapshotId", String(snapshotId))
|
|
771
|
+
const res = await fetch(`/checkpoints/publish?${qs.toString()}`, { method: "POST", headers: { "Accept": "application/json" } })
|
|
772
|
+
const payload = res && res.ok ? await res.json() : null
|
|
773
|
+
if (payload && payload.publish && payload.publish.ok) {
|
|
774
|
+
const publishUrl = payload && payload.publish && payload.publish.url ? String(payload.publish.url) : ""
|
|
775
|
+
const linkHtml = publishUrl
|
|
776
|
+
? `<a href="${escapeHtml(publishUrl)}" target="_blank" rel="noopener">View published checkpoint</a>`
|
|
777
|
+
: ""
|
|
778
|
+
Swal.fire({
|
|
779
|
+
icon: "success",
|
|
780
|
+
title: "Saved to Cloud",
|
|
781
|
+
html: linkHtml || undefined,
|
|
782
|
+
showConfirmButton: true,
|
|
783
|
+
confirmButtonText: "Close"
|
|
784
|
+
}).then(() => {
|
|
785
|
+
window.location.reload()
|
|
786
|
+
})
|
|
787
|
+
return
|
|
788
|
+
}
|
|
789
|
+
if (allowConnect && payload && payload.publish && payload.publish.code === "not_linked") {
|
|
790
|
+
const suggestedConnectUrl = payload.publish.connectUrl ? String(payload.publish.connectUrl) : ""
|
|
791
|
+
const connectUrl = await new Promise((resolve) => {
|
|
792
|
+
let settled = false
|
|
793
|
+
const cleanup = (value) => {
|
|
794
|
+
if (settled) return
|
|
795
|
+
settled = true
|
|
796
|
+
try { Swal.close() } catch (_) {}
|
|
797
|
+
resolve(value)
|
|
798
|
+
}
|
|
799
|
+
const html = `
|
|
800
|
+
<div class="backup-modal-wrapper">
|
|
801
|
+
<div class="url-modal-content backup-modal-content" role="dialog" aria-modal="true" aria-labelledby="registry-connect-title" aria-describedby="registry-connect-description">
|
|
802
|
+
<button type="button" class="url-modal-close" aria-label="Close">×</button>
|
|
803
|
+
<div class="backup-modal-header">
|
|
804
|
+
<h3 id="registry-connect-title">Connect to Registry</h3>
|
|
805
|
+
<p class="backup-modal-description" id="registry-connect-description">Enter your registry connect URL to link Pinokio, then we’ll retry the cloud save.</p>
|
|
806
|
+
</div>
|
|
807
|
+
<div class="backup-modal folder-row" style="margin-top: 0;">
|
|
808
|
+
<label for="registry-connect-url">Registry URL</label>
|
|
809
|
+
<input id="registry-connect-url" class="url-modal-input" type="url" value="${escapeAttr(suggestedConnectUrl)}" placeholder="https://your-registry/connect/pinokio" autocomplete="off" />
|
|
810
|
+
<div class="folder-hint" id="registry-connect-hint"></div>
|
|
811
|
+
</div>
|
|
812
|
+
<div class="url-modal-actions">
|
|
813
|
+
<button type="button" class="url-modal-button cancel" data-action="cancel">Cancel</button>
|
|
814
|
+
<button type="button" class="url-modal-button confirm" data-action="confirm">Open connect page</button>
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
</div>
|
|
818
|
+
`
|
|
819
|
+
Swal.fire({
|
|
820
|
+
html,
|
|
821
|
+
showConfirmButton: false,
|
|
822
|
+
showCancelButton: false,
|
|
823
|
+
allowOutsideClick: true,
|
|
824
|
+
customClass: { popup: 'backup-modal-shell' },
|
|
825
|
+
didOpen: () => {
|
|
826
|
+
const container = Swal.getHtmlContainer()
|
|
827
|
+
if (!container) return
|
|
828
|
+
const input = container.querySelector("#registry-connect-url")
|
|
829
|
+
const hint = container.querySelector("#registry-connect-hint")
|
|
830
|
+
const closeBtn = container.querySelector(".url-modal-close")
|
|
831
|
+
const cancelBtn = container.querySelector('[data-action="cancel"]')
|
|
832
|
+
const confirmBtn = container.querySelector('[data-action="confirm"]')
|
|
833
|
+
const validate = () => {
|
|
834
|
+
const val = input && input.value != null ? String(input.value).trim() : ""
|
|
835
|
+
if (!val) {
|
|
836
|
+
if (hint) hint.textContent = "Registry URL required."
|
|
837
|
+
return null
|
|
838
|
+
}
|
|
839
|
+
if (hint) hint.textContent = ""
|
|
840
|
+
return val
|
|
841
|
+
}
|
|
842
|
+
if (input) {
|
|
843
|
+
try { input.focus() } catch (_) {}
|
|
844
|
+
input.addEventListener("keydown", (e) => {
|
|
845
|
+
if (e.key === "Enter") {
|
|
846
|
+
e.preventDefault()
|
|
847
|
+
const val = validate()
|
|
848
|
+
if (val) cleanup(val)
|
|
849
|
+
}
|
|
850
|
+
})
|
|
851
|
+
}
|
|
852
|
+
if (confirmBtn) {
|
|
853
|
+
confirmBtn.addEventListener("click", () => {
|
|
854
|
+
const val = validate()
|
|
855
|
+
if (val) cleanup(val)
|
|
856
|
+
})
|
|
857
|
+
}
|
|
858
|
+
if (cancelBtn) cancelBtn.addEventListener("click", () => cleanup(null))
|
|
859
|
+
if (closeBtn) closeBtn.addEventListener("click", () => cleanup(null))
|
|
860
|
+
}
|
|
861
|
+
}).then(() => cleanup(null))
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
if (!connectUrl) return
|
|
865
|
+
try {
|
|
866
|
+
window.open(connectUrl, "pinokio-registry-connect")
|
|
867
|
+
} catch (_) {
|
|
868
|
+
window.location.href = connectUrl
|
|
869
|
+
return
|
|
870
|
+
}
|
|
871
|
+
const linked = await waitForRegistryLink()
|
|
872
|
+
if (linked) {
|
|
873
|
+
await publishSnapshot(snapshotId, false)
|
|
874
|
+
return
|
|
875
|
+
}
|
|
876
|
+
Swal.fire({ icon: "error", title: "Not connected", text: "Could not confirm the registry connection." })
|
|
877
|
+
return
|
|
878
|
+
}
|
|
879
|
+
const msg = payload && payload.publish && payload.publish.error ? payload.publish.error : "Cloud save failed"
|
|
880
|
+
Swal.fire({ icon: "error", title: "Error", text: msg })
|
|
881
|
+
} catch (error) {
|
|
882
|
+
Swal.fire({ icon: "error", title: "Error", text: error && error.message ? error.message : "Cloud save failed" })
|
|
883
|
+
} finally {
|
|
884
|
+
if (btn) {
|
|
885
|
+
btn.disabled = false
|
|
886
|
+
btn.textContent = original
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
publishButtons.forEach((btn) => {
|
|
892
|
+
btn.addEventListener("click", () => {
|
|
893
|
+
const snapshotId = btn.getAttribute("data-snapshot-id") || ''
|
|
894
|
+
if (snapshotId) publishSnapshot(snapshotId)
|
|
895
|
+
})
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
const handleCancel = () => Swal.close()
|
|
899
|
+
closeBtn && closeBtn.addEventListener("click", handleCancel)
|
|
900
|
+
}
|
|
901
|
+
})
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
document.querySelectorAll(".backup-row").forEach((row) => {
|
|
905
|
+
row.addEventListener("click", () => {
|
|
906
|
+
const key = row.getAttribute("data-remote-key")
|
|
907
|
+
const item = items.find((i) => i.remoteKey === key)
|
|
908
|
+
if (item) {
|
|
909
|
+
openInstallModal(item)
|
|
910
|
+
}
|
|
911
|
+
})
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
if (importError) {
|
|
915
|
+
Swal.fire({ icon: "error", title: "Import failed", text: importError })
|
|
916
|
+
} else if (autoInstall && autoInstall.remoteKey) {
|
|
917
|
+
const item = items.find((i) => i.remoteKey === autoInstall.remoteKey)
|
|
918
|
+
if (item) {
|
|
919
|
+
openInstallModal(item, { highlightSnapshotId: autoInstall.snapshotId })
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
})
|
|
923
|
+
</script>
|
|
924
|
+
</head>
|
|
925
|
+
<body class='<%=theme%>' data-agent="<%=agent%>">
|
|
926
|
+
<header class='navheader grabbable'>
|
|
927
|
+
<h1>
|
|
928
|
+
<a class='home' href="/home"><img class='icon' src="/pinokio-black.png"></a>
|
|
929
|
+
</h1>
|
|
930
|
+
</header>
|
|
931
|
+
<main>
|
|
932
|
+
<div class='container'>
|
|
933
|
+
<div class='content backups-container'>
|
|
934
|
+
<h2>Checkpoints</h2>
|
|
935
|
+
<p class="subtitle">Local version history for workspaces on this machine.</p>
|
|
936
|
+
<div style="margin: 10px 0 20px 0;">
|
|
937
|
+
<% if (checkpointsDir) { %>
|
|
938
|
+
<button type="button" class="btn" data-filepath="<%= checkpointsDir %>">
|
|
939
|
+
<i class="fa-solid fa-folder-open"></i> Open checkpoints folder
|
|
940
|
+
</button>
|
|
941
|
+
<% } %>
|
|
942
|
+
</div>
|
|
943
|
+
<% if (items && items.length > 0) { %>
|
|
944
|
+
<% items.forEach((item) => { %>
|
|
945
|
+
<div class='line align-top backup-row' data-remote-key="<%=item.remoteKey%>">
|
|
946
|
+
<h3>
|
|
947
|
+
<div class='item-icon'>
|
|
948
|
+
<% if (item.icon) { %>
|
|
949
|
+
<img src="<%= item.icon %>" onerror="this.onerror=null; this.style.opacity=0.4; this.src='/pinokio-black.png'">
|
|
950
|
+
<% } else { %>
|
|
951
|
+
<img src="/pinokio-black.png" style="opacity:0.4;" onerror="this.onerror=null; this.src='/pinokio-black.png'">
|
|
952
|
+
<% } %>
|
|
953
|
+
</div>
|
|
954
|
+
<div class='col'>
|
|
955
|
+
<div class='title'>
|
|
956
|
+
<i class="fa-solid fa-circle"></i>
|
|
957
|
+
<span><%= item.title || item.displayName %></span>
|
|
958
|
+
</div>
|
|
959
|
+
<div class='uri'><%= item.remoteUrl %></div>
|
|
960
|
+
<% if (item.description) { %>
|
|
961
|
+
<div class='description'><%= item.description %></div>
|
|
962
|
+
<% } %>
|
|
963
|
+
<div class='description backup-meta'>
|
|
964
|
+
<span class="badge"><%= (item.folders && item.folders.length) ? item.folders.length + ' local folder' + (item.folders.length === 1 ? '' : 's') : 'No local folders' %></span>
|
|
965
|
+
<span class="badge"><%= (item.snapshots && item.snapshots.length) ? item.snapshots.length + ' snapshot' + (item.snapshots.length === 1 ? '' : 's') : 'No snapshots yet' %></span>
|
|
966
|
+
</div>
|
|
967
|
+
<% if (item.folders && item.folders.length > 0) { %>
|
|
968
|
+
<div class="description backup-meta">
|
|
969
|
+
<i class="fa-solid fa-folder-open"></i>
|
|
970
|
+
<span>Local: <%= item.folders.map((f) => f.name).join(", ") %></span>
|
|
971
|
+
</div>
|
|
972
|
+
<% } %>
|
|
973
|
+
<% if (item.snapshots && item.snapshots.length > 0) { %>
|
|
974
|
+
<% const latest = item.snapshots[0]; %>
|
|
975
|
+
<div class="description backup-meta">
|
|
976
|
+
<i class="fa-regular fa-clock"></i>
|
|
977
|
+
<span>Latest checkpoint: <%= new Date(latest.id).toLocaleString() %></span>
|
|
978
|
+
</div>
|
|
979
|
+
<% } %>
|
|
980
|
+
</div>
|
|
981
|
+
</h3>
|
|
982
|
+
</div>
|
|
983
|
+
<% }) %>
|
|
984
|
+
<% } else { %>
|
|
985
|
+
<p>No history yet.</p>
|
|
986
|
+
<% } %>
|
|
987
|
+
</div>
|
|
988
|
+
</div>
|
|
989
|
+
<aside>
|
|
990
|
+
<div class='btn-tab'>
|
|
991
|
+
<button type='button' class='btn' id='create-launcher-button'><i class="fa-solid fa-plus"></i><div class='caption'>Create</div></button>
|
|
992
|
+
<a class='btn' id='explore' href="/home?mode=explore"><i class="fa-solid fa-globe"></i><div class='caption'>Discover</div></a>
|
|
993
|
+
</div>
|
|
994
|
+
<a href="/home" class='tab'><i class='fas fa-laptop-code'></i><div class='caption'>This machine</div></a>
|
|
995
|
+
<a href="/network" class='tab'><i class="fa-solid fa-wifi"></i><div class='caption'>Local network</div></a>
|
|
996
|
+
<% if (list && list.length > 0) { %>
|
|
997
|
+
<% let brands = { win32: "windows", darwin: "apple", linux: "Linux" } %>
|
|
998
|
+
<% list.forEach(({ host, name, platform }) => { %>
|
|
999
|
+
<a href="/net/<%=name%>" class='submenu tab'><i class="fa-brands fa-<%=brands[platform]%>"></i><div class='caption'><%=name%> (<%=current_host === host ? 'this machine' : host%>)</div></a>
|
|
1000
|
+
<% }) %>
|
|
1001
|
+
<% } %>
|
|
1002
|
+
<a href="/connect" class='tab'><i class="fa-solid fa-plug"></i><div class='caption'>Login</div></a>
|
|
1003
|
+
<a class='tab' href="<%=portal%>" target="_blank"><i class="fa-solid fa-question"></i><div class='caption'>Help</div></a>
|
|
1004
|
+
<a class='tab' id='genlog' href="/logs"><i class="fa-solid fa-laptop-code"></i><div class='caption'>Logs</div></a>
|
|
1005
|
+
<a class='tab selected' href="/checkpoints"><i class="fa-solid fa-clock-rotate-left"></i><div class='caption'>Checkpoints</div></a>
|
|
1006
|
+
<a class='tab' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
|
|
1007
|
+
<a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
|
|
1008
|
+
<a class='tab' href="/agents"><i class="fa-solid fa-robot"></i><div class='caption'>Agents</div></a>
|
|
1009
|
+
<a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
|
|
1010
|
+
<%- include('partials/peer_access_points', { peer_access_points, peer_url, peer_qr }) %>
|
|
1011
|
+
</aside>
|
|
1012
|
+
</main>
|
|
1013
|
+
</body>
|
|
1014
|
+
</html>
|