@bakapiano/ccsm 0.17.10 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/devices.js +215 -0
- package/lib/tunnel.js +253 -0
- package/package.json +1 -1
- package/public/css/layout.css +13 -3
- package/public/css/responsive.css +123 -3
- package/public/css/terminals.css +137 -32
- package/public/css/wco.css +68 -19
- package/public/css/widgets.css +276 -2
- package/public/js/api.js +43 -2
- package/public/js/backend.js +66 -10
- package/public/js/components/App.js +38 -2
- package/public/js/components/HealthOverlay.js +12 -0
- package/public/js/components/MobileNavFab.js +29 -0
- package/public/js/components/PendingApprovalOverlay.js +86 -0
- package/public/js/components/Sidebar.js +13 -4
- package/public/js/components/TerminalView.js +19 -3
- package/public/js/icons.js +29 -0
- package/public/js/main.js +95 -13
- package/public/js/pages/RemotePage.js +446 -0
- package/public/js/pages/SessionsPage.js +75 -28
- package/public/js/state.js +10 -0
- package/scripts/dev.js +11 -0
- package/server.js +214 -8
package/public/css/terminals.css
CHANGED
|
@@ -226,55 +226,146 @@
|
|
|
226
226
|
min-height: 0;
|
|
227
227
|
height: 100%;
|
|
228
228
|
margin: 0 calc(-1 * var(--s-4));
|
|
229
|
-
/* Cancel the tab-panel gap above so the terminal sits flush under the
|
|
230
|
-
title bar with no white band. */
|
|
231
|
-
margin-top: calc(-1 * var(--s-4));
|
|
232
229
|
background: var(--bg-elev);
|
|
233
230
|
overflow: hidden;
|
|
234
231
|
}
|
|
235
|
-
|
|
232
|
+
|
|
233
|
+
/* Session tabs · sits between the page-title-bar and the session-pane.
|
|
234
|
+
One row of small tab buttons; the active one has a darker bg + accent
|
|
235
|
+
underline. Last child is a "+" button that bounces to /launch. The
|
|
236
|
+
strip is full-bleed (matches .session-pane horizontal extents). */
|
|
237
|
+
.session-tabs {
|
|
236
238
|
display: flex;
|
|
237
239
|
align-items: stretch;
|
|
238
|
-
justify-content: flex-start;
|
|
239
240
|
gap: 0;
|
|
240
|
-
height:
|
|
241
|
-
min-height: calc(24px * var(--anti-zoom, 1));
|
|
242
|
-
max-height: calc(24px * var(--anti-zoom, 1));
|
|
241
|
+
height: 30px;
|
|
243
242
|
flex-shrink: 0;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
padding: 0 2px 0 0;
|
|
244
|
+
/* Negative bottom margin cancels the tab-panel's gap to the
|
|
245
|
+
session-pane underneath, so the strip sits flush against the
|
|
246
|
+
terminal. Negative horizontal margin cancels .main's padding so
|
|
247
|
+
the strip is full-bleed like the terminal underneath. */
|
|
248
|
+
margin: 0 calc(-1 * var(--s-4)) calc(-1 * var(--s-4));
|
|
249
|
+
background: #2d2a26;
|
|
250
|
+
border-bottom: 0;
|
|
251
|
+
}
|
|
252
|
+
.session-tabs-list {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: stretch;
|
|
255
|
+
gap: 0;
|
|
256
|
+
flex: 1;
|
|
257
|
+
min-width: 0;
|
|
258
|
+
overflow: hidden;
|
|
248
259
|
}
|
|
249
|
-
.session-
|
|
250
|
-
display:
|
|
260
|
+
.session-tabs-right {
|
|
261
|
+
display: flex;
|
|
251
262
|
align-items: center;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
flex-shrink: 0;
|
|
264
|
+
padding-right: 2px;
|
|
265
|
+
}
|
|
266
|
+
/* Close the gap to the page-title-bar above — only when there IS one.
|
|
267
|
+
In standalone PWA the title-bar is display:none and .session-tabs is
|
|
268
|
+
the first tab-panel child; a negative margin-top there would push
|
|
269
|
+
the strip up over the OS title-bar border. */
|
|
270
|
+
.page-title-bar + .session-tabs {
|
|
271
|
+
margin-top: calc(-1 * var(--s-4));
|
|
272
|
+
}
|
|
273
|
+
.session-tab {
|
|
274
|
+
appearance: none;
|
|
275
|
+
background: #423d37;
|
|
260
276
|
border: 0;
|
|
261
|
-
border-
|
|
277
|
+
border-bottom: 2px solid transparent;
|
|
278
|
+
margin-bottom: -1px; /* overlap container border-bottom */
|
|
279
|
+
padding: 0 10px;
|
|
280
|
+
display: inline-flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
gap: 6px;
|
|
283
|
+
font: inherit;
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
color: rgba(255, 255, 255, 0.65);
|
|
262
286
|
cursor: pointer;
|
|
287
|
+
max-width: 200px;
|
|
288
|
+
min-width: 0;
|
|
263
289
|
transition: background-color .12s, color .12s;
|
|
264
290
|
}
|
|
265
|
-
.session-
|
|
291
|
+
.session-tab:hover { background: #4f4942; color: #fff; }
|
|
292
|
+
.session-tab.is-active {
|
|
293
|
+
background: var(--ink);
|
|
266
294
|
color: #fff;
|
|
267
|
-
|
|
295
|
+
border-bottom-color: var(--ink);
|
|
268
296
|
}
|
|
269
|
-
.session-
|
|
297
|
+
.session-tab-icon { display: inline-flex; flex-shrink: 0; }
|
|
298
|
+
.session-tab-icon svg { width: 14px; height: 14px; }
|
|
299
|
+
.session-tab-icon img { width: 14px; height: 14px; }
|
|
300
|
+
.session-tab-label {
|
|
301
|
+
white-space: nowrap;
|
|
302
|
+
overflow: hidden;
|
|
303
|
+
text-overflow: ellipsis;
|
|
304
|
+
min-width: 0;
|
|
305
|
+
}
|
|
306
|
+
.session-tab-meta { color: rgba(255, 255, 255, 0.5); font-size: 11px; }
|
|
307
|
+
.session-tab.is-active .session-tab-meta { color: rgba(255, 255, 255, 0.6); }
|
|
308
|
+
.session-tab-add {
|
|
309
|
+
background: transparent;
|
|
310
|
+
max-width: none;
|
|
311
|
+
padding: 0 8px;
|
|
270
312
|
color: #fff;
|
|
271
|
-
background: rgba(0, 0, 0, 0.22);
|
|
272
313
|
}
|
|
273
|
-
.session-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
314
|
+
.session-tab-add:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
|
315
|
+
.session-tab-add svg { width: 14px; height: 14px; }
|
|
316
|
+
|
|
317
|
+
/* Kebab in the page-title-bar (top-right). Compact 24px square so it
|
|
318
|
+
doesn't dominate the masthead. In WCO mode the title-bar already
|
|
319
|
+
reserves padding-right for OS controls, so this slides cleanly to
|
|
320
|
+
the left of them. */
|
|
321
|
+
.session-menu-btn {
|
|
322
|
+
appearance: none;
|
|
323
|
+
background: transparent;
|
|
324
|
+
border: 0;
|
|
325
|
+
width: 26px;
|
|
326
|
+
height: 26px;
|
|
327
|
+
border-radius: 5px;
|
|
328
|
+
display: inline-flex;
|
|
329
|
+
align-items: center;
|
|
330
|
+
justify-content: center;
|
|
331
|
+
color: #fff;
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
flex-shrink: 0;
|
|
334
|
+
transition: background-color .12s, color .12s;
|
|
277
335
|
}
|
|
336
|
+
.session-menu-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
|
337
|
+
.session-menu-btn svg { width: 16px; height: 16px; }
|
|
338
|
+
|
|
339
|
+
.session-menu {
|
|
340
|
+
background: var(--bg-elev);
|
|
341
|
+
border: 1px solid var(--border);
|
|
342
|
+
border-radius: 6px;
|
|
343
|
+
padding: 4px;
|
|
344
|
+
box-shadow: var(--shadow-md, 0 4px 16px rgba(0,0,0,0.08));
|
|
345
|
+
display: flex;
|
|
346
|
+
flex-direction: column;
|
|
347
|
+
gap: 2px;
|
|
348
|
+
}
|
|
349
|
+
.session-menu-item {
|
|
350
|
+
appearance: none;
|
|
351
|
+
background: transparent;
|
|
352
|
+
border: 0;
|
|
353
|
+
padding: 7px 10px;
|
|
354
|
+
border-radius: 4px;
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
gap: 8px;
|
|
358
|
+
font: inherit;
|
|
359
|
+
font-size: 13px;
|
|
360
|
+
color: var(--ink);
|
|
361
|
+
cursor: pointer;
|
|
362
|
+
text-align: left;
|
|
363
|
+
}
|
|
364
|
+
.session-menu-item:hover { background: var(--bg); }
|
|
365
|
+
.session-menu-item.danger { color: var(--danger, #b73f3f); }
|
|
366
|
+
.session-menu-item.danger:hover { background: rgba(183, 63, 63, 0.08); }
|
|
367
|
+
.session-menu-item svg { width: 14px; height: 14px; }
|
|
368
|
+
|
|
278
369
|
.session-pane-head {
|
|
279
370
|
display: flex;
|
|
280
371
|
align-items: center;
|
|
@@ -327,11 +418,25 @@
|
|
|
327
418
|
height: 100%;
|
|
328
419
|
}
|
|
329
420
|
.session-pane-body .terminal-empty {
|
|
330
|
-
background:
|
|
421
|
+
background: #1a1815;
|
|
422
|
+
color: #e8e3d5;
|
|
331
423
|
display: flex;
|
|
332
424
|
flex-direction: column;
|
|
333
425
|
align-items: center;
|
|
334
426
|
justify-content: center;
|
|
335
427
|
gap: var(--s-3);
|
|
336
428
|
height: 100%;
|
|
429
|
+
font-size: 13px;
|
|
430
|
+
}
|
|
431
|
+
.session-pane-body .terminal-empty .mono {
|
|
432
|
+
color: #e07b6e;
|
|
433
|
+
}
|
|
434
|
+
.session-pane-body .terminal-empty .action.primary {
|
|
435
|
+
background: #e8e3d5;
|
|
436
|
+
color: #1a1815;
|
|
437
|
+
border-color: #e8e3d5;
|
|
438
|
+
}
|
|
439
|
+
.session-pane-body .terminal-empty .action.primary:hover {
|
|
440
|
+
background: #faf9f5;
|
|
441
|
+
border-color: #faf9f5;
|
|
337
442
|
}
|
package/public/css/wco.css
CHANGED
|
@@ -80,16 +80,34 @@ body.is-app:not(.is-wco) .sidebar-top {
|
|
|
80
80
|
body.is-app:not(.is-wco) .page-title-bar {
|
|
81
81
|
display: none;
|
|
82
82
|
}
|
|
83
|
+
/* terminals.css uses `.page-title-bar + .session-tabs { margin-top: -s-4 }`
|
|
84
|
+
to flush the tab strip against the in-page title-bar. Adjacent-sibling
|
|
85
|
+
selectors match by DOM order regardless of display, so the rule still
|
|
86
|
+
pulls session-tabs up by 16px even when the title-bar is hidden — the
|
|
87
|
+
top half of the tabs ends up clipped above the viewport / under the OS
|
|
88
|
+
title bar. Reset to 0 in standalone PWA. */
|
|
89
|
+
body.is-app:not(.is-wco) .page-title-bar + .session-tabs {
|
|
90
|
+
margin-top: 0;
|
|
91
|
+
}
|
|
83
92
|
body.is-app:not(.is-wco) .app {
|
|
84
|
-
/* Hairline separator under the OS title bar.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
/* Hairline separator under the OS title bar. Has to sit ABOVE the
|
|
94
|
+
sidebar's own background — an inset box-shadow on .app gets covered
|
|
95
|
+
by .sidebar (which paints var(--ui-bg) across its full area inside
|
|
96
|
+
.app), so the line was invisible. An absolutely-positioned ::before
|
|
97
|
+
paints over both columns; it's outside the grid track so it doesn't
|
|
98
|
+
reintroduce the 1px overflow that a real `border-top: 1px` did. */
|
|
99
|
+
position: relative;
|
|
100
|
+
}
|
|
101
|
+
body.is-app:not(.is-wco) .app::before {
|
|
102
|
+
content: "";
|
|
103
|
+
position: absolute;
|
|
104
|
+
top: 0;
|
|
105
|
+
left: 0;
|
|
106
|
+
right: 0;
|
|
107
|
+
height: 1px;
|
|
108
|
+
background: var(--border);
|
|
109
|
+
z-index: 10;
|
|
110
|
+
pointer-events: none;
|
|
93
111
|
}
|
|
94
112
|
/* With page-title-bar hidden, session-pane's `margin-top: calc(-1 *
|
|
95
113
|
var(--s-4))` — designed to flush it against the (now invisible)
|
|
@@ -101,13 +119,21 @@ body.is-app:not(.is-wco) .session-pane {
|
|
|
101
119
|
margin-top: 0;
|
|
102
120
|
height: auto;
|
|
103
121
|
}
|
|
104
|
-
/* Settings
|
|
105
|
-
would otherwise butt straight against the OS
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
body.is-app:not(.is-wco) [data-panel="configure"]
|
|
122
|
+
/* Settings + Launch · with the in-page title-bar hidden in standalone
|
|
123
|
+
mode, their content would otherwise butt straight against the OS
|
|
124
|
+
title-bar border. Give a little breathing room. Sessions has its
|
|
125
|
+
own full-bleed terminal pane; About has its own hero header. */
|
|
126
|
+
body.is-app:not(.is-wco) [data-panel="configure"],
|
|
127
|
+
body.is-app:not(.is-wco) [data-panel="launch"],
|
|
128
|
+
body.is-app:not(.is-wco) [data-panel="remote"] {
|
|
109
129
|
padding-top: var(--s-4);
|
|
110
130
|
}
|
|
131
|
+
/* Sidebar nav rows (New Session / Settings) also need a top gap in
|
|
132
|
+
standalone — body.is-app zeros .sidebar's padding-top so the in-page
|
|
133
|
+
title-bar can sit flush, but with that title-bar gone in standalone
|
|
134
|
+
mode the nav buttons end up jammed against the OS title-bar border.
|
|
135
|
+
Restore a small inset so they read as a real nav, not as overflow. */
|
|
136
|
+
body.is-app:not(.is-wco) .sidebar { padding-top: var(--s-3); }
|
|
111
137
|
|
|
112
138
|
/* WCO mode only: the browser has hidden its own title bar and floats OS
|
|
113
139
|
controls (min/max/close) over our content top-right. Our 34px top band
|
|
@@ -124,16 +150,39 @@ body.is-wco .page-title-bar {
|
|
|
124
150
|
}
|
|
125
151
|
body.is-wco .page-title-bar,
|
|
126
152
|
body.is-wco .sidebar-top {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
153
|
+
/* --titlebar-h is set from JS reading
|
|
154
|
+
navigator.windowControlsOverlay.getTitlebarAreaRect().height — the
|
|
155
|
+
OS-reported strip the overlay reserves for us. CSS env() and a 32px
|
|
156
|
+
baseline are layered fallbacks when the JS API is unavailable. No
|
|
157
|
+
min-floor on the JS path: the OS knows its own caption height best,
|
|
158
|
+
and over-padding (the previous max(40px, …)) made the chrome look
|
|
159
|
+
visibly chunkier than the rest of the window. */
|
|
160
|
+
height: calc(var(--titlebar-h, env(titlebar-area-height, 32px)) * var(--anti-zoom, 1));
|
|
161
|
+
min-height: calc(var(--titlebar-h, env(titlebar-area-height, 32px)) * var(--anti-zoom, 1));
|
|
162
|
+
max-height: calc(var(--titlebar-h, env(titlebar-area-height, 32px)) * var(--anti-zoom, 1));
|
|
130
163
|
}
|
|
131
164
|
body.is-wco .sidebar-brand,
|
|
132
165
|
body.is-wco .sidebar-brand-button,
|
|
133
166
|
body.is-wco .collapse-toggle {
|
|
134
|
-
height:
|
|
135
|
-
min-height:
|
|
167
|
+
height: var(--titlebar-h, env(titlebar-area-height, 32px));
|
|
168
|
+
min-height: var(--titlebar-h, env(titlebar-area-height, 32px));
|
|
136
169
|
}
|
|
170
|
+
/* terminals.css uses the .tab-panel's gap (s-4) plus a -s-4 margin-top on
|
|
171
|
+
.session-tabs to close that gap, so the tab strip visually flushes
|
|
172
|
+
against the page-title-bar above. In WCO the negative margin pulls the
|
|
173
|
+
tabs row UP by 16px — i.e. its top edge lands ABOVE the OS overlay's
|
|
174
|
+
bottom edge, and the kebab inside gets clipped by the floating window
|
|
175
|
+
controls. Cancel both the gap and the negative margin here so the tabs
|
|
176
|
+
row sits at exactly y=titlebar-area-height (flush below the overlay)
|
|
177
|
+
without losing flushness against the title-bar. */
|
|
178
|
+
body.is-wco .tab-panel { gap: 0; }
|
|
179
|
+
body.is-wco .page-title-bar + .session-tabs { margin-top: 0; }
|
|
180
|
+
/* terminals.css also gives .session-tabs `margin-bottom: -s-4` to close
|
|
181
|
+
the gap to session-pane below — but with tab-panel gap now 0 in WCO
|
|
182
|
+
that negative margin pulls session-pane UP 16px instead, overlapping
|
|
183
|
+
the bottom of the tab labels (kebab + tab text get clipped from
|
|
184
|
+
below). Zero it out too. */
|
|
185
|
+
body.is-wco .session-tabs { margin-bottom: 0; }
|
|
137
186
|
|
|
138
187
|
@media (display-mode: window-controls-overlay) {
|
|
139
188
|
body.is-wco .page-title-bar {
|
package/public/css/widgets.css
CHANGED
|
@@ -749,8 +749,15 @@
|
|
|
749
749
|
display: flex;
|
|
750
750
|
flex-direction: column;
|
|
751
751
|
gap: var(--s-4);
|
|
752
|
-
|
|
753
|
-
|
|
752
|
+
/* Negative right margin = -var(--s-4) cancels .main's padding-right
|
|
753
|
+
so the scroll container — and therefore the scrollbar — reach the
|
|
754
|
+
full window edge. Padding-right = var(--s-4) preserves the same
|
|
755
|
+
visual gap between content and the right edge that was there
|
|
756
|
+
before. Top padding bumped to var(--s-3) so the first section title
|
|
757
|
+
has visible breathing room from the page-title-bar separator above
|
|
758
|
+
it (4px was almost flush). Negative margin-top stays in sync. */
|
|
759
|
+
padding: var(--s-4) var(--s-4) var(--s-4) 4px;
|
|
760
|
+
margin: -4px calc(-1 * var(--s-4)) 0 -4px;
|
|
754
761
|
}
|
|
755
762
|
/* In a flex column container, items default to flex-shrink:1 which
|
|
756
763
|
causes the cards to compress instead of pushing the scroll container
|
|
@@ -1626,3 +1633,270 @@
|
|
|
1626
1633
|
background: var(--bg);
|
|
1627
1634
|
border: 1px solid var(--border);
|
|
1628
1635
|
}
|
|
1636
|
+
|
|
1637
|
+
/* ── Remote page ──────────────────────────────────────────────────
|
|
1638
|
+
Uses the existing .settings-scroll + Section + .config-grid + .field
|
|
1639
|
+
+ .chip system from ConfigurePage. Only adds the bits that don't
|
|
1640
|
+
already exist: the inline status line per provider, the token-row
|
|
1641
|
+
input + action cluster, the URL row, the CLI log block, and the
|
|
1642
|
+
bulleted security list. */
|
|
1643
|
+
|
|
1644
|
+
.remote-status-line {
|
|
1645
|
+
display: flex;
|
|
1646
|
+
align-items: center;
|
|
1647
|
+
flex-wrap: wrap;
|
|
1648
|
+
gap: var(--s-2);
|
|
1649
|
+
font-size: 12.5px;
|
|
1650
|
+
color: var(--ink-mid);
|
|
1651
|
+
}
|
|
1652
|
+
.remote-status-line .small-mono { font-size: 11px; }
|
|
1653
|
+
.remote-status-line .warn { color: #b86a2a; font-weight: 500; }
|
|
1654
|
+
.remote-status-line .muted { color: var(--ink-muted); }
|
|
1655
|
+
.remote-status-line code {
|
|
1656
|
+
font-family: var(--mono);
|
|
1657
|
+
font-size: 11.5px;
|
|
1658
|
+
background: var(--bg);
|
|
1659
|
+
padding: 1px 5px;
|
|
1660
|
+
border-radius: 3px;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
.remote-token-row {
|
|
1664
|
+
display: flex;
|
|
1665
|
+
gap: var(--s-2);
|
|
1666
|
+
align-items: center;
|
|
1667
|
+
flex-wrap: wrap;
|
|
1668
|
+
}
|
|
1669
|
+
.remote-token-input {
|
|
1670
|
+
flex: 1;
|
|
1671
|
+
min-width: 240px;
|
|
1672
|
+
font-family: var(--mono);
|
|
1673
|
+
font-size: 12.5px;
|
|
1674
|
+
padding: 7px 11px;
|
|
1675
|
+
border: 1px solid var(--border-strong);
|
|
1676
|
+
border-radius: var(--r-sm);
|
|
1677
|
+
background: var(--bg-elev);
|
|
1678
|
+
color: var(--ink);
|
|
1679
|
+
}
|
|
1680
|
+
.remote-token-input:focus {
|
|
1681
|
+
outline: none;
|
|
1682
|
+
border-color: var(--ink);
|
|
1683
|
+
box-shadow: 0 0 0 1px var(--ink);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
.remote-url-line {
|
|
1687
|
+
display: flex;
|
|
1688
|
+
gap: var(--s-2);
|
|
1689
|
+
align-items: center;
|
|
1690
|
+
flex-wrap: wrap;
|
|
1691
|
+
}
|
|
1692
|
+
.remote-url-value {
|
|
1693
|
+
flex: 1;
|
|
1694
|
+
min-width: 0;
|
|
1695
|
+
font-family: var(--mono);
|
|
1696
|
+
font-size: 12px;
|
|
1697
|
+
color: var(--ink);
|
|
1698
|
+
background: var(--bg);
|
|
1699
|
+
padding: 6px 10px;
|
|
1700
|
+
border-radius: 4px;
|
|
1701
|
+
border: 1px solid var(--border);
|
|
1702
|
+
overflow: hidden;
|
|
1703
|
+
text-overflow: ellipsis;
|
|
1704
|
+
white-space: nowrap;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
.remote-log {
|
|
1708
|
+
font-size: 11.5px;
|
|
1709
|
+
color: var(--ink-mid);
|
|
1710
|
+
}
|
|
1711
|
+
.remote-log summary { cursor: pointer; user-select: none; }
|
|
1712
|
+
.remote-log pre {
|
|
1713
|
+
margin-top: var(--s-2);
|
|
1714
|
+
padding: var(--s-3);
|
|
1715
|
+
background: var(--ink);
|
|
1716
|
+
color: var(--bg-elev);
|
|
1717
|
+
border-radius: var(--r-sm);
|
|
1718
|
+
font-family: var(--mono);
|
|
1719
|
+
font-size: 11px;
|
|
1720
|
+
line-height: 1.5;
|
|
1721
|
+
max-height: 220px;
|
|
1722
|
+
overflow: auto;
|
|
1723
|
+
white-space: pre-wrap;
|
|
1724
|
+
word-break: break-all;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
.remote-empty {
|
|
1728
|
+
margin: 0;
|
|
1729
|
+
font-size: 12.5px;
|
|
1730
|
+
color: var(--ink-muted);
|
|
1731
|
+
padding: var(--s-3);
|
|
1732
|
+
background: var(--bg);
|
|
1733
|
+
border-radius: var(--r-sm);
|
|
1734
|
+
text-align: center;
|
|
1735
|
+
}
|
|
1736
|
+
.remote-devices {
|
|
1737
|
+
display: flex;
|
|
1738
|
+
flex-direction: column;
|
|
1739
|
+
gap: var(--s-4);
|
|
1740
|
+
}
|
|
1741
|
+
.remote-devices-group {
|
|
1742
|
+
display: flex;
|
|
1743
|
+
flex-direction: column;
|
|
1744
|
+
gap: 6px;
|
|
1745
|
+
}
|
|
1746
|
+
.remote-devices-group-head {
|
|
1747
|
+
display: flex;
|
|
1748
|
+
align-items: baseline;
|
|
1749
|
+
gap: var(--s-2);
|
|
1750
|
+
margin-bottom: 2px;
|
|
1751
|
+
}
|
|
1752
|
+
.remote-devices-group-title {
|
|
1753
|
+
font-size: 11px;
|
|
1754
|
+
font-weight: 600;
|
|
1755
|
+
text-transform: uppercase;
|
|
1756
|
+
letter-spacing: 0.06em;
|
|
1757
|
+
color: var(--ink-mid);
|
|
1758
|
+
}
|
|
1759
|
+
.remote-devices-group-count {
|
|
1760
|
+
font-family: var(--mono);
|
|
1761
|
+
font-size: 11px;
|
|
1762
|
+
color: var(--ink-muted);
|
|
1763
|
+
background: var(--bg);
|
|
1764
|
+
padding: 1px 7px;
|
|
1765
|
+
border-radius: 999px;
|
|
1766
|
+
border: 1px solid var(--border);
|
|
1767
|
+
}
|
|
1768
|
+
.remote-devices-group-hint {
|
|
1769
|
+
font-size: 11px;
|
|
1770
|
+
font-style: italic;
|
|
1771
|
+
color: var(--ink-muted);
|
|
1772
|
+
}
|
|
1773
|
+
.remote-device {
|
|
1774
|
+
display: flex;
|
|
1775
|
+
align-items: center;
|
|
1776
|
+
gap: var(--s-3);
|
|
1777
|
+
padding: 10px 12px;
|
|
1778
|
+
background: var(--bg-elev);
|
|
1779
|
+
border: 1px solid var(--border);
|
|
1780
|
+
border-radius: var(--r-sm);
|
|
1781
|
+
}
|
|
1782
|
+
.remote-device.is-pending {
|
|
1783
|
+
border-color: #b86a2a;
|
|
1784
|
+
background: rgba(184, 106, 42, 0.04);
|
|
1785
|
+
}
|
|
1786
|
+
.remote-device.is-rejected {
|
|
1787
|
+
background: var(--bg);
|
|
1788
|
+
opacity: 0.8;
|
|
1789
|
+
}
|
|
1790
|
+
.remote-device-main {
|
|
1791
|
+
flex: 1;
|
|
1792
|
+
min-width: 0;
|
|
1793
|
+
}
|
|
1794
|
+
.remote-device-label {
|
|
1795
|
+
display: flex;
|
|
1796
|
+
align-items: center;
|
|
1797
|
+
gap: 6px;
|
|
1798
|
+
font-size: 13px;
|
|
1799
|
+
font-weight: 500;
|
|
1800
|
+
color: var(--ink);
|
|
1801
|
+
}
|
|
1802
|
+
.remote-device-label .icon-btn {
|
|
1803
|
+
background: transparent;
|
|
1804
|
+
border: 0;
|
|
1805
|
+
padding: 2px;
|
|
1806
|
+
cursor: pointer;
|
|
1807
|
+
color: var(--ink-muted);
|
|
1808
|
+
border-radius: 3px;
|
|
1809
|
+
display: inline-flex;
|
|
1810
|
+
align-items: center;
|
|
1811
|
+
}
|
|
1812
|
+
.remote-device-label .icon-btn:hover { color: var(--ink); background: var(--bg); }
|
|
1813
|
+
.remote-device-meta {
|
|
1814
|
+
font-size: 11.5px;
|
|
1815
|
+
color: var(--ink-mid);
|
|
1816
|
+
margin-top: 2px;
|
|
1817
|
+
display: flex;
|
|
1818
|
+
align-items: baseline;
|
|
1819
|
+
flex-wrap: wrap;
|
|
1820
|
+
gap: 4px;
|
|
1821
|
+
}
|
|
1822
|
+
.remote-device-meta .mono { font-family: var(--mono); font-size: 11px; }
|
|
1823
|
+
.remote-device-ua {
|
|
1824
|
+
font-family: var(--mono);
|
|
1825
|
+
font-size: 11px;
|
|
1826
|
+
color: var(--ink-muted);
|
|
1827
|
+
overflow: hidden;
|
|
1828
|
+
text-overflow: ellipsis;
|
|
1829
|
+
max-width: 380px;
|
|
1830
|
+
white-space: nowrap;
|
|
1831
|
+
}
|
|
1832
|
+
.remote-device-actions {
|
|
1833
|
+
display: flex;
|
|
1834
|
+
gap: 6px;
|
|
1835
|
+
flex-shrink: 0;
|
|
1836
|
+
}
|
|
1837
|
+
.remote-device-actions .action.small { padding: 4px 10px; font-size: 11.5px; }
|
|
1838
|
+
|
|
1839
|
+
/* Remote · "How access works" — three fact rows separated by hairlines,
|
|
1840
|
+
inset on the left by a 2px ink bar so the section reads as quiet
|
|
1841
|
+
reference material instead of an alert. */
|
|
1842
|
+
.remote-facts {
|
|
1843
|
+
margin: 0;
|
|
1844
|
+
padding: 0;
|
|
1845
|
+
display: flex;
|
|
1846
|
+
flex-direction: column;
|
|
1847
|
+
}
|
|
1848
|
+
.remote-fact {
|
|
1849
|
+
position: relative;
|
|
1850
|
+
padding: var(--s-3) var(--s-3) var(--s-3) var(--s-5);
|
|
1851
|
+
border-bottom: 1px solid var(--border);
|
|
1852
|
+
}
|
|
1853
|
+
.remote-fact:first-child { padding-top: var(--s-2); }
|
|
1854
|
+
.remote-fact:last-child { border-bottom: 0; padding-bottom: var(--s-2); }
|
|
1855
|
+
.remote-fact::before {
|
|
1856
|
+
content: "";
|
|
1857
|
+
position: absolute;
|
|
1858
|
+
left: 0;
|
|
1859
|
+
top: var(--s-3);
|
|
1860
|
+
bottom: var(--s-3);
|
|
1861
|
+
width: 2px;
|
|
1862
|
+
background: var(--ink);
|
|
1863
|
+
border-radius: 1px;
|
|
1864
|
+
opacity: 0.35;
|
|
1865
|
+
}
|
|
1866
|
+
.remote-fact dt {
|
|
1867
|
+
font-size: 12.5px;
|
|
1868
|
+
font-weight: 600;
|
|
1869
|
+
color: var(--ink);
|
|
1870
|
+
letter-spacing: -0.005em;
|
|
1871
|
+
margin-bottom: 4px;
|
|
1872
|
+
}
|
|
1873
|
+
.remote-fact dd {
|
|
1874
|
+
margin: 0;
|
|
1875
|
+
font-size: 12px;
|
|
1876
|
+
color: var(--ink-mid);
|
|
1877
|
+
line-height: 1.6;
|
|
1878
|
+
}
|
|
1879
|
+
.remote-fact dd code {
|
|
1880
|
+
font-family: var(--mono);
|
|
1881
|
+
font-size: 11px;
|
|
1882
|
+
background: var(--bg);
|
|
1883
|
+
padding: 1px 5px;
|
|
1884
|
+
border-radius: 3px;
|
|
1885
|
+
}
|
|
1886
|
+
.remote-fact dd strong {
|
|
1887
|
+
color: var(--ink);
|
|
1888
|
+
font-weight: 600;
|
|
1889
|
+
}
|
|
1890
|
+
.remote-fact dd em {
|
|
1891
|
+
font-style: italic;
|
|
1892
|
+
color: var(--ink);
|
|
1893
|
+
}
|
|
1894
|
+
.remote-fact-pill {
|
|
1895
|
+
display: inline-block;
|
|
1896
|
+
font-size: 11px;
|
|
1897
|
+
padding: 1px 7px;
|
|
1898
|
+
border-radius: 999px;
|
|
1899
|
+
background: rgba(184, 106, 42, 0.14);
|
|
1900
|
+
color: #8b4f1f;
|
|
1901
|
+
font-weight: 500;
|
|
1902
|
+
}
|
package/public/js/api.js
CHANGED
|
@@ -1,17 +1,58 @@
|
|
|
1
1
|
// Fetch wrapper + every loader. Loaders push into signals from ./state.js.
|
|
2
2
|
// Cross-origin (hosted frontend → local backend) flows through httpBase().
|
|
3
3
|
|
|
4
|
+
import { signal } from '@preact/signals';
|
|
4
5
|
import * as S from './state.js';
|
|
5
|
-
import { httpBase } from './backend.js';
|
|
6
|
+
import { httpBase, getToken, getDeviceId, isRemoteAccess } from './backend.js';
|
|
7
|
+
|
|
8
|
+
// Global pending-approval signal. Flipped to true whenever any /api
|
|
9
|
+
// call returns 403 {pending:true}; PendingApprovalOverlay watches this
|
|
10
|
+
// and shows the blocking screen. We also stash the server's record so
|
|
11
|
+
// the overlay can display "we recorded you at HH:MM" detail.
|
|
12
|
+
export const pendingDevice = signal(null);
|
|
6
13
|
|
|
7
14
|
export async function api(method, url, body) {
|
|
8
15
|
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
16
|
+
// When a remote token is configured (Remote page set it OR the page
|
|
17
|
+
// was loaded with ?token= and we stashed it in localStorage), attach
|
|
18
|
+
// it to every API call. The server middleware lets loopback Hosts
|
|
19
|
+
// through without the token; for tunnel-served pages this is the
|
|
20
|
+
// only way past the 401.
|
|
21
|
+
const tok = getToken();
|
|
22
|
+
if (tok) opts.headers['Authorization'] = `Bearer ${tok}`;
|
|
23
|
+
// Always send our device id when one exists in localStorage. The host
|
|
24
|
+
// browser at localhost doesn't strictly need it (loopback bypass),
|
|
25
|
+
// but harmless — the server simply records lastSeen for it. Required
|
|
26
|
+
// for any tunnel-served page to clear the device-approval gate.
|
|
27
|
+
const dev = getDeviceId();
|
|
28
|
+
if (dev) opts.headers['X-Device-Id'] = dev;
|
|
9
29
|
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
10
30
|
const r = await fetch(httpBase() + url, opts);
|
|
11
31
|
const text = await r.text();
|
|
12
32
|
let json;
|
|
13
33
|
try { json = text ? JSON.parse(text) : {}; } catch { json = { raw: text }; }
|
|
14
|
-
if (!r.ok)
|
|
34
|
+
if (!r.ok) {
|
|
35
|
+
// Surface device-approval pending state. Only matters on remote
|
|
36
|
+
// tabs — host's loopback browser never gets a 401/403 from these
|
|
37
|
+
// checks.
|
|
38
|
+
if (isRemoteAccess()) {
|
|
39
|
+
if (r.status === 403 && json && (json.pending || json.rejected)) {
|
|
40
|
+
pendingDevice.value = { ...json, at: Date.now() };
|
|
41
|
+
} else if (r.status === 401) {
|
|
42
|
+
// Server doesn't recognise our device — either fresh page load
|
|
43
|
+
// (no /api/devices/me hit yet) or our record got deleted /
|
|
44
|
+
// pruned. Drop into the pending overlay; its /me poll will
|
|
45
|
+
// re-register us using the token we still have in localStorage,
|
|
46
|
+
// and the response sets pendingDevice to the correct state.
|
|
47
|
+
pendingDevice.value = { pending: true, at: Date.now() };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw new Error(json.error || `HTTP ${r.status}`);
|
|
51
|
+
}
|
|
52
|
+
// PendingApprovalOverlay clears pendingDevice itself based on the
|
|
53
|
+
// /api/devices/me body (which can return 200 with status:'pending'
|
|
54
|
+
// since that endpoint is gate-exempt). Doing an auto-clear here on
|
|
55
|
+
// any 2xx would race the overlay's poll and dismiss it prematurely.
|
|
15
56
|
return json;
|
|
16
57
|
}
|
|
17
58
|
|