termbeam 0.0.8 → 0.1.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/README.md +14 -4
- package/package.json +1 -1
- package/public/index.html +142 -12
- package/public/sw.js +23 -5
- package/public/terminal.html +1636 -483
- package/src/auth.js +13 -0
- package/src/cli.js +71 -15
- package/src/routes.js +14 -1
- package/src/sessions.js +28 -4
- package/src/shells.js +1 -1
- package/src/tunnel.js +17 -12
- package/src/websocket.js +25 -3
package/README.md
CHANGED
|
@@ -41,8 +41,18 @@ termbeam --password mysecret
|
|
|
41
41
|
|
|
42
42
|
## Features
|
|
43
43
|
|
|
44
|
-
- **Mobile-first UI** with on-screen touch bar (arrow keys, Tab, Ctrl shortcuts, Esc) and
|
|
45
|
-
- **
|
|
44
|
+
- **Mobile-first UI** with on-screen touch bar (arrow keys, Tab, Enter, Ctrl shortcuts, Esc) and touch-optimized controls
|
|
45
|
+
- **Tabbed multi-session terminal** — open, switch, and manage multiple sessions from a single tab bar with drag-to-reorder
|
|
46
|
+
- **Split view** — view two sessions side-by-side (horizontal on desktop, vertical on mobile)
|
|
47
|
+
- **Session colors** — assign a color to each session for quick identification
|
|
48
|
+
- **Activity indicators** — see how recently each session had output (e.g. "3s ago", "5m ago")
|
|
49
|
+
- **Tab previews** — hover (desktop) or long-press (mobile) a tab to preview the last few lines of output
|
|
50
|
+
- **Side panel** (mobile) — slide-out session list with output previews for quick switching
|
|
51
|
+
- **Create sessions anywhere** — new session modal available from both the hub page and the terminal page
|
|
52
|
+
- **Touch scrolling** — swipe to scroll through terminal history
|
|
53
|
+
- **Share button** — share the TermBeam URL via Web Share API, clipboard, or legacy copy fallback (works over HTTP)
|
|
54
|
+
- **Refresh button** — clear PWA/service worker cache and reload to get the latest version
|
|
55
|
+
- **iPhone PWA safe area** — full support for `viewport-fit=cover` and safe area insets on notched devices
|
|
46
56
|
- **Password auth** with token-based cookies and rate-limited login
|
|
47
57
|
- **Folder browser** to pick working directories without typing paths
|
|
48
58
|
- **Initial command** — optionally launch a session straight into `htop`, `vim`, or any command
|
|
@@ -80,14 +90,14 @@ termbeam --host 127.0.0.1 # restrict to localhost (default: 0.0.0.0)
|
|
|
80
90
|
|
|
81
91
|
| Flag | Description | Default |
|
|
82
92
|
| --------------------- | ---------------------------------------- | ----------- |
|
|
83
|
-
| `--password <pw>` | Set access password
|
|
93
|
+
| `--password <pw>` | Set access password (also accepts `--password=<pw>`) | None |
|
|
84
94
|
| `--generate-password` | Auto-generate a secure password | — |
|
|
85
95
|
| `--tunnel` | Create an ephemeral devtunnel URL | Off |
|
|
86
96
|
| `--persisted-tunnel` | Create a reusable devtunnel URL | Off |
|
|
87
97
|
| `--port <port>` | Server port | `3456` |
|
|
88
98
|
| `--host <addr>` | Bind address | `0.0.0.0` |
|
|
89
99
|
|
|
90
|
-
Environment variables: `PORT`, `TERMBEAM_PASSWORD`, `TERMBEAM_CWD` (
|
|
100
|
+
Environment variables: `PORT`, `TERMBEAM_PASSWORD`, `TERMBEAM_CWD`, `SHELL` (Unix fallback), `COMSPEC` (Windows fallback). See [Configuration docs](https://dorlugasigal.github.io/TermBeam/configuration/).
|
|
91
101
|
|
|
92
102
|
## Security
|
|
93
103
|
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta
|
|
6
6
|
name="viewport"
|
|
7
|
-
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
|
7
|
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
|
8
8
|
/>
|
|
9
9
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
10
10
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
@@ -85,6 +85,28 @@
|
|
|
85
85
|
color: var(--text-secondary);
|
|
86
86
|
margin-top: 4px;
|
|
87
87
|
}
|
|
88
|
+
.header-btn {
|
|
89
|
+
position: absolute;
|
|
90
|
+
top: 16px;
|
|
91
|
+
background: none;
|
|
92
|
+
border: 1px solid var(--border);
|
|
93
|
+
color: var(--text-dim);
|
|
94
|
+
width: 32px;
|
|
95
|
+
height: 32px;
|
|
96
|
+
border-radius: 8px;
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
font-size: 16px;
|
|
102
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
103
|
+
-webkit-tap-highlight-color: transparent;
|
|
104
|
+
}
|
|
105
|
+
.header-btn:hover {
|
|
106
|
+
color: var(--text);
|
|
107
|
+
border-color: var(--border-subtle);
|
|
108
|
+
background: var(--border);
|
|
109
|
+
}
|
|
88
110
|
.theme-toggle {
|
|
89
111
|
position: absolute;
|
|
90
112
|
top: 16px;
|
|
@@ -111,7 +133,7 @@
|
|
|
111
133
|
|
|
112
134
|
.sessions-list {
|
|
113
135
|
padding: 16px;
|
|
114
|
-
padding-bottom: 80px;
|
|
136
|
+
padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
|
|
115
137
|
display: flex;
|
|
116
138
|
flex-direction: column;
|
|
117
139
|
gap: 12px;
|
|
@@ -235,9 +257,9 @@
|
|
|
235
257
|
|
|
236
258
|
.new-session {
|
|
237
259
|
position: fixed;
|
|
238
|
-
bottom: 16px;
|
|
239
|
-
left: 16px;
|
|
240
|
-
right: 16px;
|
|
260
|
+
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
|
261
|
+
left: calc(16px + env(safe-area-inset-left, 0px));
|
|
262
|
+
right: calc(16px + env(safe-area-inset-right, 0px));
|
|
241
263
|
padding: 14px;
|
|
242
264
|
background: var(--accent);
|
|
243
265
|
color: #ffffff;
|
|
@@ -250,7 +272,6 @@
|
|
|
250
272
|
z-index: 50;
|
|
251
273
|
transition: background 0.15s, transform 0.1s, box-shadow 0.15s;
|
|
252
274
|
box-shadow: 0 2px 8px rgba(0, 120, 212, 0.3);
|
|
253
|
-
padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
|
|
254
275
|
}
|
|
255
276
|
.new-session:hover {
|
|
256
277
|
background: var(--accent-hover);
|
|
@@ -563,12 +584,39 @@
|
|
|
563
584
|
background: var(--accent-active);
|
|
564
585
|
transform: scale(0.98);
|
|
565
586
|
}
|
|
587
|
+
|
|
588
|
+
.color-picker {
|
|
589
|
+
display: flex;
|
|
590
|
+
gap: 8px;
|
|
591
|
+
padding: 6px 0;
|
|
592
|
+
flex-wrap: wrap;
|
|
593
|
+
}
|
|
594
|
+
.color-swatch {
|
|
595
|
+
width: 32px;
|
|
596
|
+
height: 32px;
|
|
597
|
+
border-radius: 50%;
|
|
598
|
+
border: 3px solid transparent;
|
|
599
|
+
cursor: pointer;
|
|
600
|
+
transition: border-color 0.15s, transform 0.1s;
|
|
601
|
+
-webkit-tap-highlight-color: transparent;
|
|
602
|
+
padding: 0;
|
|
603
|
+
outline: none;
|
|
604
|
+
}
|
|
605
|
+
.color-swatch:hover {
|
|
606
|
+
transform: scale(1.1);
|
|
607
|
+
}
|
|
608
|
+
.color-swatch.selected {
|
|
609
|
+
border-color: var(--text);
|
|
610
|
+
transform: scale(1.15);
|
|
611
|
+
}
|
|
566
612
|
</style>
|
|
567
613
|
</head>
|
|
568
614
|
<body>
|
|
569
615
|
<div class="header">
|
|
570
616
|
<h1>📡 Term<span>Beam</span></h1>
|
|
571
617
|
<p>Beam your terminal to any device · <span id="version" style="color: var(--accent)"></span></p>
|
|
618
|
+
<button class="header-btn" id="share-btn" style="right: 96px;top:16px" title="Share link"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg></button>
|
|
619
|
+
<button class="header-btn" id="refresh-btn" style="right: 56px;top:16px" title="Refresh app"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></button>
|
|
572
620
|
<button class="theme-toggle" id="theme-toggle" title="Toggle theme"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button>
|
|
573
621
|
</div>
|
|
574
622
|
|
|
@@ -586,6 +634,17 @@
|
|
|
586
634
|
</select>
|
|
587
635
|
<label for="sess-cmd">Initial Command <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
|
|
588
636
|
<input type="text" id="sess-cmd" placeholder="e.g. copilot, htop, vim" />
|
|
637
|
+
<label>Color</label>
|
|
638
|
+
<div class="color-picker" id="color-picker">
|
|
639
|
+
<button type="button" class="color-swatch selected" data-color="#4a9eff" style="background:#4a9eff" title="Blue"></button>
|
|
640
|
+
<button type="button" class="color-swatch" data-color="#4ade80" style="background:#4ade80" title="Green"></button>
|
|
641
|
+
<button type="button" class="color-swatch" data-color="#fbbf24" style="background:#fbbf24" title="Amber"></button>
|
|
642
|
+
<button type="button" class="color-swatch" data-color="#c084fc" style="background:#c084fc" title="Purple"></button>
|
|
643
|
+
<button type="button" class="color-swatch" data-color="#f87171" style="background:#f87171" title="Red"></button>
|
|
644
|
+
<button type="button" class="color-swatch" data-color="#22d3ee" style="background:#22d3ee" title="Cyan"></button>
|
|
645
|
+
<button type="button" class="color-swatch" data-color="#fb923c" style="background:#fb923c" title="Orange"></button>
|
|
646
|
+
<button type="button" class="color-swatch" data-color="#f472b6" style="background:#f472b6" title="Pink"></button>
|
|
647
|
+
</div>
|
|
589
648
|
<label for="sess-cwd">Working Directory</label>
|
|
590
649
|
<div class="cwd-picker">
|
|
591
650
|
<input type="text" id="sess-cwd" placeholder="/Users/dorlugasigal" />
|
|
@@ -666,6 +725,15 @@
|
|
|
666
725
|
const listEl = document.getElementById('sessions-list');
|
|
667
726
|
const modal = document.getElementById('modal');
|
|
668
727
|
|
|
728
|
+
function getActivityLabel(ts) {
|
|
729
|
+
if (!ts) return '';
|
|
730
|
+
const diff = (Date.now() - ts) / 1000;
|
|
731
|
+
if (diff < 10) return 'Active now';
|
|
732
|
+
if (diff < 60) return Math.floor(diff) + 's ago';
|
|
733
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
734
|
+
return Math.floor(diff / 3600) + 'h ago';
|
|
735
|
+
}
|
|
736
|
+
|
|
669
737
|
async function loadSessions() {
|
|
670
738
|
const res = await fetch('/api/sessions');
|
|
671
739
|
const sessions = await res.json();
|
|
@@ -678,19 +746,20 @@
|
|
|
678
746
|
listEl.innerHTML = sessions
|
|
679
747
|
.map(
|
|
680
748
|
(s) => `
|
|
681
|
-
<div class="swipe-wrap" data-session-id="${s.id}">
|
|
749
|
+
<div class="swipe-wrap" data-session-id="${esc(s.id)}">
|
|
682
750
|
<div class="swipe-delete">
|
|
683
|
-
<button
|
|
751
|
+
<button data-delete-id="${esc(s.id)}"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg><span>Delete</span></button>
|
|
684
752
|
</div>
|
|
685
|
-
<div class="session-card"
|
|
753
|
+
<div class="session-card" data-nav-id="${esc(s.id)}">
|
|
686
754
|
<div class="top">
|
|
687
|
-
<div class="name"><span class="dot"></span>${esc(s.name)}</div>
|
|
755
|
+
<div class="name"><span class="dot" data-color="${esc(s.color || '')}"></span>${esc(s.name)}</div>
|
|
688
756
|
<span class="pid">PID ${s.pid}</span>
|
|
689
757
|
</div>
|
|
690
758
|
<div class="details">
|
|
691
759
|
<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> ${esc(s.cwd)}</span>
|
|
692
760
|
<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg> ${esc(s.shell)}</span>
|
|
693
761
|
<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> ${s.clients} connected</span>
|
|
762
|
+
<span title="Last activity">${getActivityLabel(s.lastActivity)}</span>
|
|
694
763
|
</div>
|
|
695
764
|
<button class="connect-btn">Connect →</button>
|
|
696
765
|
</div>
|
|
@@ -699,8 +768,17 @@
|
|
|
699
768
|
)
|
|
700
769
|
.join('');
|
|
701
770
|
|
|
702
|
-
// Attach swipe handlers after rendering
|
|
771
|
+
// Attach swipe handlers and click handlers after rendering
|
|
703
772
|
listEl.querySelectorAll('.swipe-wrap').forEach(initSwipe);
|
|
773
|
+
listEl.querySelectorAll('[data-delete-id]').forEach(btn => {
|
|
774
|
+
btn.addEventListener('click', (e) => deleteSession(btn.dataset.deleteId, e));
|
|
775
|
+
});
|
|
776
|
+
listEl.querySelectorAll('[data-nav-id]').forEach(card => {
|
|
777
|
+
card.addEventListener('click', () => { location.href = '/terminal?id=' + encodeURIComponent(card.dataset.navId); });
|
|
778
|
+
});
|
|
779
|
+
listEl.querySelectorAll('.dot[data-color]').forEach(dot => {
|
|
780
|
+
dot.style.background = dot.dataset.color || 'var(--success)';
|
|
781
|
+
});
|
|
704
782
|
}
|
|
705
783
|
|
|
706
784
|
function esc(str) {
|
|
@@ -720,17 +798,28 @@
|
|
|
720
798
|
if (e.target === modal) modal.classList.remove('visible');
|
|
721
799
|
});
|
|
722
800
|
|
|
801
|
+
// Color picker
|
|
802
|
+
document.getElementById('color-picker').addEventListener('click', (e) => {
|
|
803
|
+
const swatch = e.target.closest('.color-swatch');
|
|
804
|
+
if (!swatch) return;
|
|
805
|
+
document.querySelectorAll('#color-picker .color-swatch').forEach(s => s.classList.remove('selected'));
|
|
806
|
+
swatch.classList.add('selected');
|
|
807
|
+
});
|
|
808
|
+
|
|
723
809
|
document.getElementById('modal-create').addEventListener('click', async () => {
|
|
724
810
|
const name = document.getElementById('sess-name').value.trim();
|
|
725
811
|
const shell = document.getElementById('sess-shell').value.trim();
|
|
726
812
|
const cwd = document.getElementById('sess-cwd').value.trim();
|
|
727
813
|
const initialCommand = document.getElementById('sess-cmd').value.trim();
|
|
728
814
|
|
|
815
|
+
const colorEl = document.querySelector('#color-picker .color-swatch.selected');
|
|
816
|
+
const color = colorEl ? colorEl.dataset.color : null;
|
|
729
817
|
const body = {};
|
|
730
818
|
if (name) body.name = name;
|
|
731
819
|
if (shell) body.shell = shell;
|
|
732
820
|
if (cwd) body.cwd = cwd;
|
|
733
821
|
if (initialCommand) body.initialCommand = initialCommand;
|
|
822
|
+
if (color) body.color = color;
|
|
734
823
|
|
|
735
824
|
const res = await fetch('/api/sessions', {
|
|
736
825
|
method: 'POST',
|
|
@@ -897,7 +986,7 @@
|
|
|
897
986
|
}
|
|
898
987
|
browserList.innerHTML = data.dirs
|
|
899
988
|
.map((d) => {
|
|
900
|
-
const name = d.split(
|
|
989
|
+
const name = d.split(/[/\\]/).pop();
|
|
901
990
|
return `<div class="folder-item" data-path="${esc(d)}">
|
|
902
991
|
<span class="folder-icon">📁</span>
|
|
903
992
|
<span class="folder-name">${esc(name)}</span>
|
|
@@ -944,6 +1033,47 @@
|
|
|
944
1033
|
loadSessions();
|
|
945
1034
|
setInterval(loadSessions, 3000);
|
|
946
1035
|
|
|
1036
|
+
// Share button
|
|
1037
|
+
function copyToClipboardFallback(text) {
|
|
1038
|
+
const ta = document.createElement('textarea');
|
|
1039
|
+
ta.value = text;
|
|
1040
|
+
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
|
|
1041
|
+
document.body.appendChild(ta);
|
|
1042
|
+
ta.select();
|
|
1043
|
+
try { document.execCommand('copy'); } catch {}
|
|
1044
|
+
document.body.removeChild(ta);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function showShareToast(msg) {
|
|
1048
|
+
const toast = document.createElement('div');
|
|
1049
|
+
toast.textContent = msg;
|
|
1050
|
+
toast.style.cssText = 'position:fixed;top:16px;left:50%;transform:translateX(-50%);background:var(--surface);color:var(--text);border:1px solid var(--border);padding:6px 16px;border-radius:8px;font-size:13px;font-weight:600;z-index:200;';
|
|
1051
|
+
document.body.appendChild(toast);
|
|
1052
|
+
setTimeout(() => toast.remove(), 1500);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
document.getElementById('share-btn').addEventListener('click', async () => {
|
|
1056
|
+
const url = location.href;
|
|
1057
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1058
|
+
try { await navigator.clipboard.writeText(url); showShareToast('Link copied!'); return; } catch {}
|
|
1059
|
+
}
|
|
1060
|
+
copyToClipboardFallback(url);
|
|
1061
|
+
showShareToast('Link copied!');
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// Refresh button: clear SW cache and reload
|
|
1065
|
+
document.getElementById('refresh-btn').addEventListener('click', async () => {
|
|
1066
|
+
if ('caches' in window) {
|
|
1067
|
+
const keys = await caches.keys();
|
|
1068
|
+
await Promise.all(keys.map(k => caches.delete(k)));
|
|
1069
|
+
}
|
|
1070
|
+
if (navigator.serviceWorker) {
|
|
1071
|
+
const reg = await navigator.serviceWorker.getRegistration();
|
|
1072
|
+
if (reg) await reg.update();
|
|
1073
|
+
}
|
|
1074
|
+
location.reload();
|
|
1075
|
+
});
|
|
1076
|
+
|
|
947
1077
|
if ('serviceWorker' in navigator) {
|
|
948
1078
|
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
|
949
1079
|
}
|
package/public/sw.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const CACHE_NAME = 'termbeam-
|
|
1
|
+
const CACHE_NAME = 'termbeam-v2';
|
|
2
2
|
const SHELL_URLS = ['/', '/terminal'];
|
|
3
3
|
|
|
4
4
|
self.addEventListener('install', (event) => {
|
|
@@ -20,16 +20,34 @@ self.addEventListener('activate', (event) => {
|
|
|
20
20
|
self.addEventListener('fetch', (event) => {
|
|
21
21
|
const url = new URL(event.request.url);
|
|
22
22
|
|
|
23
|
-
// Don't cache WebSocket upgrades
|
|
23
|
+
// Don't cache WebSocket upgrades
|
|
24
24
|
if (
|
|
25
25
|
event.request.mode === 'websocket' ||
|
|
26
26
|
url.protocol === 'ws:' ||
|
|
27
|
-
url.protocol === 'wss:'
|
|
28
|
-
url.origin !== self.location.origin
|
|
27
|
+
url.protocol === 'wss:'
|
|
29
28
|
) {
|
|
30
29
|
return;
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
// Cache-first for CDN font files (NerdFont from jsdelivr)
|
|
33
|
+
if (url.origin !== self.location.origin) {
|
|
34
|
+
if (url.hostname === 'cdn.jsdelivr.net' && url.pathname.endsWith('.ttf')) {
|
|
35
|
+
event.respondWith(
|
|
36
|
+
caches.match(event.request).then((cached) => {
|
|
37
|
+
if (cached) return cached;
|
|
38
|
+
return fetch(event.request).then((response) => {
|
|
39
|
+
if (response.ok) {
|
|
40
|
+
const clone = response.clone();
|
|
41
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
42
|
+
}
|
|
43
|
+
return response;
|
|
44
|
+
});
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
// Network-first for API calls
|
|
34
52
|
if (url.pathname.startsWith('/api/')) {
|
|
35
53
|
event.respondWith(
|
|
@@ -49,6 +67,6 @@ self.addEventListener('fetch', (event) => {
|
|
|
49
67
|
}
|
|
50
68
|
return response;
|
|
51
69
|
});
|
|
52
|
-
})
|
|
70
|
+
}).catch(() => new Response('Offline', { status: 503, statusText: 'Service Unavailable' }))
|
|
53
71
|
);
|
|
54
72
|
});
|