labgate 0.5.34 → 0.5.36

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/dist/lib/ui.html CHANGED
@@ -4,54 +4,67 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>LabGate Settings</title>
7
- <link rel="stylesheet" href="/vendor/xterm/xterm.css">
7
+ <script>
8
+ (function canonicalizeUiPathWithTrailingSlash() {
9
+ try {
10
+ var pathname = String(window.location.pathname || '/');
11
+ if (!pathname || pathname === '/' || pathname.endsWith('/')) return;
12
+ var tail = pathname.slice(pathname.lastIndexOf('/') + 1);
13
+ if (!tail || tail.indexOf('.') !== -1) return;
14
+ window.location.replace(pathname + '/' + window.location.search + window.location.hash);
15
+ } catch (_err) {
16
+ // Best effort: continue without redirect.
17
+ }
18
+ })();
19
+ </script>
20
+ <link rel="stylesheet" href="vendor/xterm/xterm.css">
8
21
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%23374151'/><text x='50' y='68' font-size='52' font-family='system-ui' font-weight='700' fill='white' text-anchor='middle'>L</text></svg>">
9
22
  <style>
10
23
  @font-face {
11
24
  font-family: 'Geist';
12
- src: url('/fonts/Geist-Regular.woff2') format('woff2');
25
+ src: url('fonts/Geist-Regular.woff2') format('woff2');
13
26
  font-weight: 400;
14
27
  font-style: normal;
15
28
  font-display: swap;
16
29
  }
17
30
  @font-face {
18
31
  font-family: 'Geist';
19
- src: url('/fonts/Geist-Medium.woff2') format('woff2');
32
+ src: url('fonts/Geist-Medium.woff2') format('woff2');
20
33
  font-weight: 500;
21
34
  font-style: normal;
22
35
  font-display: swap;
23
36
  }
24
37
  @font-face {
25
38
  font-family: 'Geist';
26
- src: url('/fonts/Geist-SemiBold.woff2') format('woff2');
39
+ src: url('fonts/Geist-SemiBold.woff2') format('woff2');
27
40
  font-weight: 600;
28
41
  font-style: normal;
29
42
  font-display: swap;
30
43
  }
31
44
  @font-face {
32
45
  font-family: 'Geist';
33
- src: url('/fonts/Geist-Bold.woff2') format('woff2');
46
+ src: url('fonts/Geist-Bold.woff2') format('woff2');
34
47
  font-weight: 700;
35
48
  font-style: normal;
36
49
  font-display: swap;
37
50
  }
38
51
  @font-face {
39
52
  font-family: 'GeistMono';
40
- src: url('/fonts/GeistMono-Regular.woff2') format('woff2');
53
+ src: url('fonts/GeistMono-Regular.woff2') format('woff2');
41
54
  font-weight: 400;
42
55
  font-style: normal;
43
56
  font-display: swap;
44
57
  }
45
58
  @font-face {
46
59
  font-family: 'GeistMono';
47
- src: url('/fonts/GeistMono-Medium.woff2') format('woff2');
60
+ src: url('fonts/GeistMono-Medium.woff2') format('woff2');
48
61
  font-weight: 500;
49
62
  font-style: normal;
50
63
  font-display: swap;
51
64
  }
52
65
  @font-face {
53
66
  font-family: 'GeistPixelSquare';
54
- src: url('/fonts/GeistPixel-Square.woff2') format('woff2');
67
+ src: url('fonts/GeistPixel-Square.woff2') format('woff2');
55
68
  font-weight: 400;
56
69
  font-style: normal;
57
70
  font-display: swap;
@@ -6895,9 +6908,9 @@
6895
6908
  </div>
6896
6909
  </div>
6897
6910
 
6898
- <script src="/vendor/xterm/xterm.js"></script>
6899
- <script src="/vendor/xterm/addon-fit.js"></script>
6900
- <script src="/vendor/xterm/addon-web-links.js"></script>
6911
+ <script src="vendor/xterm/xterm.js"></script>
6912
+ <script src="vendor/xterm/addon-fit.js"></script>
6913
+ <script src="vendor/xterm/addon-web-links.js"></script>
6901
6914
  <script>
6902
6915
  var config = {};
6903
6916
  var originalConfig = '';
@@ -6966,6 +6979,76 @@ var autoCopySelectionState = {
6966
6979
  lastToastAt: 0
6967
6980
  };
6968
6981
 
6982
+ function getLabgateUiBasePath() {
6983
+ var pathname = String(window.location.pathname || '/');
6984
+ if (!pathname) return '/';
6985
+ if (pathname.charAt(0) !== '/') pathname = '/' + pathname;
6986
+ if (pathname === '/') return '/';
6987
+ if (pathname.endsWith('/')) return pathname;
6988
+ var tail = pathname.slice(pathname.lastIndexOf('/') + 1);
6989
+ if (tail && tail.indexOf('.') === -1) {
6990
+ return pathname + '/';
6991
+ }
6992
+ var slashIdx = pathname.lastIndexOf('/');
6993
+ return slashIdx >= 0 ? pathname.slice(0, slashIdx + 1) : '/';
6994
+ }
6995
+
6996
+ var LABGATE_UI_BASE_PATH = getLabgateUiBasePath();
6997
+
6998
+ function shouldPrefixLabgateUrl(url) {
6999
+ return /^\/(?:api|vendor|fonts)(?:\/|\?|$)/.test(url);
7000
+ }
7001
+
7002
+ function applyLabgateUiBasePath(rawPath) {
7003
+ if (!shouldPrefixLabgateUrl(rawPath)) return rawPath;
7004
+ if (LABGATE_UI_BASE_PATH === '/') return rawPath;
7005
+ return LABGATE_UI_BASE_PATH + rawPath.slice(1);
7006
+ }
7007
+
7008
+ function resolveLabgateUiUrl(url) {
7009
+ var raw = String(url || '');
7010
+ if (!raw) return raw;
7011
+ if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(raw)) {
7012
+ try {
7013
+ var parsed = new URL(raw);
7014
+ if (parsed.origin !== window.location.origin) return raw;
7015
+ var rewrittenPath = applyLabgateUiBasePath(parsed.pathname);
7016
+ if (rewrittenPath === parsed.pathname) return raw;
7017
+ parsed.pathname = rewrittenPath;
7018
+ return parsed.toString();
7019
+ } catch (_err) {
7020
+ return raw;
7021
+ }
7022
+ }
7023
+ return applyLabgateUiBasePath(raw);
7024
+ }
7025
+
7026
+ function buildLabgateWebSocketUrl(pathWithQuery) {
7027
+ var resolved = resolveLabgateUiUrl(pathWithQuery);
7028
+ var wsUrl = new URL(resolved, window.location.origin);
7029
+ wsUrl.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
7030
+ return wsUrl.toString();
7031
+ }
7032
+
7033
+ if (typeof window.fetch === 'function') {
7034
+ var nativeFetch = window.fetch.bind(window);
7035
+ window.fetch = function(input, init) {
7036
+ if (typeof input === 'string') {
7037
+ return nativeFetch(resolveLabgateUiUrl(input), init);
7038
+ }
7039
+ if (typeof URL !== 'undefined' && input instanceof URL) {
7040
+ return nativeFetch(new URL(resolveLabgateUiUrl(String(input))), init);
7041
+ }
7042
+ if (typeof Request !== 'undefined' && input instanceof Request) {
7043
+ var resolvedUrl = resolveLabgateUiUrl(input.url);
7044
+ if (resolvedUrl !== input.url) {
7045
+ return nativeFetch(new Request(resolvedUrl, input), init);
7046
+ }
7047
+ }
7048
+ return nativeFetch(input, init);
7049
+ };
7050
+ }
7051
+
6969
7052
  function getCachedClaudeEmail() {
6970
7053
  try {
6971
7054
  return (localStorage.getItem(CLAUDE_EMAIL_CACHE_KEY) || '').trim();
@@ -7687,8 +7770,8 @@ function renderImageWidget(data, container) {
7687
7770
  return;
7688
7771
  }
7689
7772
  var img = document.createElement('img');
7690
- img.src = '/api/display/file?path=' + encodeURIComponent(path)
7691
- + '&writeToken=' + encodeURIComponent(window.LABGATE_WRITE_TOKEN || '');
7773
+ img.src = resolveLabgateUiUrl('/api/display/file?path=' + encodeURIComponent(path)
7774
+ + '&writeToken=' + encodeURIComponent(window.LABGATE_WRITE_TOKEN || ''));
7692
7775
  img.alt = data.alt || '';
7693
7776
  img.style.maxWidth = '100%';
7694
7777
  img.style.borderRadius = '6px';
@@ -7718,8 +7801,8 @@ function renderPdfWidget(data, container, opts) {
7718
7801
  pdfDiv.className = 'widget-pdf';
7719
7802
  container.appendChild(pdfDiv);
7720
7803
 
7721
- var pdfUrl = '/api/display/file?path=' + encodeURIComponent(path)
7722
- + '&writeToken=' + encodeURIComponent(window.LABGATE_WRITE_TOKEN || '');
7804
+ var pdfUrl = resolveLabgateUiUrl('/api/display/file?path=' + encodeURIComponent(path)
7805
+ + '&writeToken=' + encodeURIComponent(window.LABGATE_WRITE_TOKEN || ''));
7723
7806
 
7724
7807
  // Use browser's built-in PDF viewer via iframe — zero dependencies, works everywhere
7725
7808
  var iframe = document.createElement('iframe');
@@ -7877,8 +7960,8 @@ function renderMoleculeWidget(data, container, opts) {
7877
7960
  if (pdbId) {
7878
7961
  viewer.loadPdb(pdbId.toUpperCase());
7879
7962
  } else if (path) {
7880
- var fileUrl = '/api/display/file?path=' + encodeURIComponent(path)
7881
- + '&writeToken=' + encodeURIComponent(window.LABGATE_WRITE_TOKEN || '');
7963
+ var fileUrl = resolveLabgateUiUrl('/api/display/file?path=' + encodeURIComponent(path)
7964
+ + '&writeToken=' + encodeURIComponent(window.LABGATE_WRITE_TOKEN || ''));
7882
7965
  viewer.loadStructureFromUrl(fileUrl, 'pdb');
7883
7966
  } else if (pdbData) {
7884
7967
  viewer.loadStructureFromData(pdbData, 'pdb');
@@ -7954,8 +8037,8 @@ function renderFilePreviewWidget(data, container) {
7954
8037
 
7955
8038
  container.innerHTML = '<div class="widget-placeholder">Loading file preview...</div>';
7956
8039
 
7957
- var url = '/api/display/file?path=' + encodeURIComponent(path)
7958
- + '&writeToken=' + encodeURIComponent(window.LABGATE_WRITE_TOKEN || '');
8040
+ var url = resolveLabgateUiUrl('/api/display/file?path=' + encodeURIComponent(path)
8041
+ + '&writeToken=' + encodeURIComponent(window.LABGATE_WRITE_TOKEN || ''));
7959
8042
 
7960
8043
  fetch(url)
7961
8044
  .then(function(resp) {
@@ -8222,7 +8305,7 @@ function renderTracksWidget(data, container, opts) {
8222
8305
  tracksDiv.innerHTML = '';
8223
8306
  var processedTracks = trackList.map(function(t) {
8224
8307
  var track = Object.assign({}, t);
8225
- if (track.path && !track.url) { track.url = '/api/display/file?path=' + encodeURIComponent(track.path) + '&writeToken=' + encodeURIComponent(window.LABGATE_WRITE_TOKEN || ''); delete track.path; }
8308
+ if (track.path && !track.url) { track.url = resolveLabgateUiUrl('/api/display/file?path=' + encodeURIComponent(track.path) + '&writeToken=' + encodeURIComponent(window.LABGATE_WRITE_TOKEN || '')); delete track.path; }
8226
8309
  if (!track.type) track.type = 'annotation';
8227
8310
  if (!track.displayMode) track.displayMode = 'EXPANDED';
8228
8311
  return track;
@@ -8381,10 +8464,9 @@ function submitClaudeHeadlessPrompt(promptText) {
8381
8464
  webTerm.headlessRunning = true;
8382
8465
  updateTerminalInputAvailability();
8383
8466
 
8384
- var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
8385
- var wsUrl = proto + '//' + location.host
8386
- + '/api/claude/ws?id=' + encodeURIComponent(sessionId)
8467
+ var wsPath = '/api/claude/ws?id=' + encodeURIComponent(sessionId)
8387
8468
  + '&writeToken=' + encodeURIComponent(LABGATE_WRITE_TOKEN);
8469
+ var wsUrl = buildLabgateWebSocketUrl(wsPath);
8388
8470
  var socket = new WebSocket(wsUrl);
8389
8471
  webTerm.headlessSocket = socket;
8390
8472
 
@@ -9530,15 +9612,14 @@ function openWebTerminalSocket(id, opts) {
9530
9612
  var afterSeq = Number.isFinite(options.afterSeq) ? Math.max(0, Math.floor(options.afterSeq)) : null;
9531
9613
  var disableLegacyReplay = !!options.disableLegacyReplay;
9532
9614
  var attachNonce = String(options.attachNonce || '');
9533
- var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
9534
- var wsUrl = proto + '//' + location.host
9535
- + '/api/terminal/ws?id=' + encodeURIComponent(id)
9615
+ var wsPath = '/api/terminal/ws?id=' + encodeURIComponent(id)
9536
9616
  + '&writeToken=' + encodeURIComponent(LABGATE_WRITE_TOKEN);
9537
9617
  if (afterSeq !== null) {
9538
- wsUrl += '&afterSeq=' + encodeURIComponent(String(afterSeq));
9618
+ wsPath += '&afterSeq=' + encodeURIComponent(String(afterSeq));
9539
9619
  } else if (disableLegacyReplay) {
9540
- wsUrl += '&replay=0';
9620
+ wsPath += '&replay=0';
9541
9621
  }
9622
+ var wsUrl = buildLabgateWebSocketUrl(wsPath);
9542
9623
 
9543
9624
  var socket = new WebSocket(wsUrl);
9544
9625
  webTerm.socket = socket;
@@ -12966,7 +13047,6 @@ function pollUiUpdateStatus() {
12966
13047
  setUiUpdateStatus('Update complete (' + next + '). Restart `labgate ui`, then reload this page.', 'success');
12967
13048
  showToast('Update complete. Restart `labgate ui`.', 'success');
12968
13049
  setUiUpdateButtonsState({ showApply: false, disableCheck: false, disableApply: false });
12969
- checkUiVersion(true);
12970
13050
  return;
12971
13051
  }
12972
13052
  if (status === 'error') {
@@ -15194,7 +15274,7 @@ function handleResultsChangedEvent() {
15194
15274
  function connectSSE() {
15195
15275
  if (evtSource) return;
15196
15276
  try {
15197
- evtSource = new EventSource('/api/events');
15277
+ evtSource = new EventSource(resolveLabgateUiUrl('/api/events'));
15198
15278
  evtSource.addEventListener('sessions', function(e) {
15199
15279
  try {
15200
15280
  var d = JSON.parse(e.data);
@@ -15656,7 +15736,11 @@ function loadSlurmJobs() {
15656
15736
  // Session-aware filtering
15657
15737
  if (slurmScope === 'mine') {
15658
15738
  var sid = getAttachedWebTerminalId();
15659
- if (sid) url += '&session_id=' + encodeURIComponent(sid);
15739
+ if (!sid) {
15740
+ el.innerHTML = '<div class="empty-state" style="padding:12px">Attach to a terminal session to view jobs for this session.</div>';
15741
+ return;
15742
+ }
15743
+ url += '&session_id=' + encodeURIComponent(sid);
15660
15744
  }
15661
15745
 
15662
15746
  fetch(url).then(function(r) { return r.json(); }).then(function(data) {
package/dist/lib/ui.js CHANGED
@@ -5933,8 +5933,12 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
5933
5933
  const prewarmImageOnStartup = options.prewarmImageOnStartup === true;
5934
5934
  const requestedPort = tcpPort ?? 0;
5935
5935
  const maxPort = requestedPort + 3;
5936
+ const requestedShortCode = typeof options.shortCode === 'string' ? options.shortCode.trim() : '';
5937
+ if (requestedShortCode && !/^[A-Za-z0-9_-]{12}$/.test(requestedShortCode)) {
5938
+ throw new Error('Invalid shortCode: expected exactly 12 characters matching [A-Za-z0-9_-].');
5939
+ }
5936
5940
  const uiAccessToken = useTcp ? (0, crypto_1.randomBytes)(24).toString('hex') : '';
5937
- const uiShortCode = useTcp ? (0, crypto_1.randomBytes)(9).toString('base64url') : '';
5941
+ const uiShortCode = useTcp ? (requestedShortCode || (0, crypto_1.randomBytes)(9).toString('base64url')) : '';
5938
5942
  let listenPort = requestedPort;
5939
5943
  let started = false;
5940
5944
  let dashboardQuickLink = '';