clay-server 2.7.2 → 2.8.2
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/bin/cli.js +31 -17
- package/lib/config.js +7 -4
- package/lib/project.js +343 -15
- package/lib/public/app.js +1039 -134
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/clay-logo.png +0 -0
- package/lib/public/css/base.css +18 -1
- package/lib/public/css/filebrowser.css +1 -0
- package/lib/public/css/home-hub.css +455 -0
- package/lib/public/css/icon-strip.css +6 -5
- package/lib/public/css/loop.css +141 -23
- package/lib/public/css/messages.css +2 -0
- package/lib/public/css/mobile-nav.css +38 -12
- package/lib/public/css/overlays.css +205 -169
- package/lib/public/css/playbook.css +264 -0
- package/lib/public/css/profile.css +268 -0
- package/lib/public/css/scheduler-modal.css +1429 -0
- package/lib/public/css/scheduler.css +1305 -0
- package/lib/public/css/sidebar.css +305 -11
- package/lib/public/css/sticky-notes.css +23 -19
- package/lib/public/css/stt.css +155 -0
- package/lib/public/css/title-bar.css +14 -6
- package/lib/public/favicon-banded-32.png +0 -0
- package/lib/public/favicon-banded.png +0 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-banded-76.png +0 -0
- package/lib/public/icon-banded-96.png +0 -0
- package/lib/public/index.html +336 -44
- package/lib/public/modules/ascii-logo.js +442 -0
- package/lib/public/modules/markdown.js +18 -0
- package/lib/public/modules/notifications.js +50 -63
- package/lib/public/modules/playbook.js +578 -0
- package/lib/public/modules/profile.js +357 -0
- package/lib/public/modules/project-settings.js +1 -9
- package/lib/public/modules/scheduler.js +2826 -0
- package/lib/public/modules/server-settings.js +1 -1
- package/lib/public/modules/sidebar.js +376 -32
- package/lib/public/modules/stt.js +272 -0
- package/lib/public/modules/terminal.js +32 -0
- package/lib/public/modules/theme.js +3 -10
- package/lib/public/style.css +6 -0
- package/lib/public/sw.js +82 -3
- package/lib/public/wordmark-banded-20.png +0 -0
- package/lib/public/wordmark-banded-32.png +0 -0
- package/lib/public/wordmark-banded-64.png +0 -0
- package/lib/public/wordmark-banded-80.png +0 -0
- package/lib/scheduler.js +402 -0
- package/lib/sdk-bridge.js +3 -2
- package/lib/server.js +124 -3
- package/lib/sessions.js +35 -2
- package/package.json +1 -1
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// Speech-to-Text module using Web Speech API
|
|
2
|
+
// Uses browser's built-in speech recognition (Chrome/Edge/Safari → Google servers)
|
|
3
|
+
|
|
4
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
5
|
+
import { autoResize } from './input.js';
|
|
6
|
+
|
|
7
|
+
var ctx;
|
|
8
|
+
|
|
9
|
+
// --- State ---
|
|
10
|
+
var recording = false;
|
|
11
|
+
var recognition = null;
|
|
12
|
+
var selectedLang = null;
|
|
13
|
+
var textBeforeSTT = '';
|
|
14
|
+
var interimText = '';
|
|
15
|
+
|
|
16
|
+
// DOM refs
|
|
17
|
+
var sttBtn = null;
|
|
18
|
+
var langPopover = null;
|
|
19
|
+
|
|
20
|
+
// --- Language options ---
|
|
21
|
+
// Web Speech API uses BCP-47 language tags
|
|
22
|
+
var LANGUAGES = [
|
|
23
|
+
{ code: 'en-US', name: 'English' },
|
|
24
|
+
{ code: 'ko-KR', name: 'Korean' },
|
|
25
|
+
{ code: 'ja-JP', name: 'Japanese' },
|
|
26
|
+
{ code: 'zh-CN', name: 'Chinese' },
|
|
27
|
+
{ code: 'es-ES', name: 'Spanish' },
|
|
28
|
+
{ code: 'fr-FR', name: 'French' },
|
|
29
|
+
{ code: 'de-DE', name: 'German' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// --- Persist language choice ---
|
|
33
|
+
function saveLang(code) {
|
|
34
|
+
try { localStorage.setItem('stt-lang', code); } catch (e) { /* ignore */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadLang() {
|
|
38
|
+
try { return localStorage.getItem('stt-lang'); } catch (e) { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Check browser support ---
|
|
42
|
+
function getSpeechRecognition() {
|
|
43
|
+
return window.SpeechRecognition || window.webkitSpeechRecognition || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Init ---
|
|
47
|
+
export function initSTT(_ctx) {
|
|
48
|
+
ctx = _ctx;
|
|
49
|
+
|
|
50
|
+
sttBtn = document.getElementById('stt-btn');
|
|
51
|
+
if (!sttBtn) return;
|
|
52
|
+
|
|
53
|
+
if (!getSpeechRecognition()) {
|
|
54
|
+
sttBtn.style.display = 'none';
|
|
55
|
+
console.warn('[STT] Web Speech API not supported in this browser');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Restore saved language
|
|
60
|
+
selectedLang = loadLang();
|
|
61
|
+
|
|
62
|
+
sttBtn.addEventListener('click', function(e) {
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
|
|
65
|
+
if (recording) {
|
|
66
|
+
stopRecording();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!selectedLang) {
|
|
71
|
+
showLangPopover();
|
|
72
|
+
} else {
|
|
73
|
+
startRecording();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Right-click to change language
|
|
78
|
+
sttBtn.addEventListener('contextmenu', function(e) {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
if (recording) stopRecording();
|
|
82
|
+
showLangPopover();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Language popover ---
|
|
87
|
+
function showLangPopover() {
|
|
88
|
+
if (langPopover) {
|
|
89
|
+
hideLangPopover();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
langPopover = document.createElement('div');
|
|
94
|
+
langPopover.className = 'stt-lang-popover';
|
|
95
|
+
|
|
96
|
+
var html = '<div class="stt-lang-title">Voice Input Language</div>';
|
|
97
|
+
for (var i = 0; i < LANGUAGES.length; i++) {
|
|
98
|
+
var l = LANGUAGES[i];
|
|
99
|
+
var activeClass = (selectedLang === l.code) ? ' stt-lang-active' : '';
|
|
100
|
+
html += '<button class="stt-lang-option' + activeClass + '" data-lang="' + l.code + '">' +
|
|
101
|
+
'<span class="stt-lang-name">' + l.name + '</span>' +
|
|
102
|
+
'</button>';
|
|
103
|
+
}
|
|
104
|
+
langPopover.innerHTML = html;
|
|
105
|
+
|
|
106
|
+
langPopover.querySelectorAll('.stt-lang-option').forEach(function(btn) {
|
|
107
|
+
btn.addEventListener('click', function() {
|
|
108
|
+
onLangSelected(btn.dataset.lang);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
var wrapper = document.getElementById('input-wrapper');
|
|
113
|
+
wrapper.appendChild(langPopover);
|
|
114
|
+
|
|
115
|
+
setTimeout(function() {
|
|
116
|
+
document.addEventListener('click', closeLangOnOutside);
|
|
117
|
+
}, 0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function closeLangOnOutside(e) {
|
|
121
|
+
if (langPopover && !langPopover.contains(e.target) && e.target !== sttBtn && !sttBtn.contains(e.target)) {
|
|
122
|
+
hideLangPopover();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hideLangPopover() {
|
|
127
|
+
if (langPopover) {
|
|
128
|
+
langPopover.remove();
|
|
129
|
+
langPopover = null;
|
|
130
|
+
}
|
|
131
|
+
document.removeEventListener('click', closeLangOnOutside);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function onLangSelected(code) {
|
|
135
|
+
selectedLang = code;
|
|
136
|
+
saveLang(code);
|
|
137
|
+
hideLangPopover();
|
|
138
|
+
startRecording();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Recording ---
|
|
142
|
+
function startRecording() {
|
|
143
|
+
if (recording) return;
|
|
144
|
+
|
|
145
|
+
var SpeechRecognition = getSpeechRecognition();
|
|
146
|
+
if (!SpeechRecognition) return;
|
|
147
|
+
|
|
148
|
+
recognition = new SpeechRecognition();
|
|
149
|
+
recognition.lang = selectedLang || 'en-US';
|
|
150
|
+
recognition.continuous = true;
|
|
151
|
+
recognition.interimResults = true;
|
|
152
|
+
|
|
153
|
+
textBeforeSTT = ctx.inputEl.value;
|
|
154
|
+
interimText = '';
|
|
155
|
+
|
|
156
|
+
recognition.onresult = function(e) {
|
|
157
|
+
var final = '';
|
|
158
|
+
var interim = '';
|
|
159
|
+
|
|
160
|
+
for (var i = 0; i < e.results.length; i++) {
|
|
161
|
+
var result = e.results[i];
|
|
162
|
+
if (result.isFinal) {
|
|
163
|
+
final += result[0].transcript;
|
|
164
|
+
} else {
|
|
165
|
+
interim += result[0].transcript;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
var text = textBeforeSTT;
|
|
170
|
+
if (final) {
|
|
171
|
+
if (text && text.length > 0 && text[text.length - 1] !== ' ' && text[text.length - 1] !== '\n') {
|
|
172
|
+
text += ' ';
|
|
173
|
+
}
|
|
174
|
+
text += final;
|
|
175
|
+
}
|
|
176
|
+
if (interim) {
|
|
177
|
+
if (text && text.length > 0 && text[text.length - 1] !== ' ' && text[text.length - 1] !== '\n') {
|
|
178
|
+
text += ' ';
|
|
179
|
+
}
|
|
180
|
+
text += interim;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
ctx.inputEl.value = text;
|
|
184
|
+
autoResize();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
recognition.onerror = function(e) {
|
|
188
|
+
console.error('[STT] Recognition error:', e.error);
|
|
189
|
+
if (e.error === 'not-allowed') {
|
|
190
|
+
if (ctx.addSystemMessage) {
|
|
191
|
+
ctx.addSystemMessage('Microphone access denied.\n\nTo fix: click the lock icon in the address bar → Site settings → Microphone → Allow, then reload.', true);
|
|
192
|
+
}
|
|
193
|
+
stopRecording();
|
|
194
|
+
} else if (e.error === 'no-speech') {
|
|
195
|
+
// Silence — just keep listening
|
|
196
|
+
} else if (e.error === 'network') {
|
|
197
|
+
if (ctx.addSystemMessage) {
|
|
198
|
+
ctx.addSystemMessage('Speech recognition unavailable.\n\nWeb Speech API sends audio to Google servers for recognition. Some Chromium forks (Arc, Brave) block this connection.\n\nSupported: Chrome, Edge, Safari 14.1+, Samsung Internet\nNot supported: Arc, Brave, Firefox', true);
|
|
199
|
+
}
|
|
200
|
+
stopRecording();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
recognition.onend = function() {
|
|
205
|
+
// Auto-restart if still recording (browser may stop after silence)
|
|
206
|
+
if (recording) {
|
|
207
|
+
// Save confirmed text so far
|
|
208
|
+
textBeforeSTT = ctx.inputEl.value;
|
|
209
|
+
try {
|
|
210
|
+
recognition.start();
|
|
211
|
+
} catch (e) {
|
|
212
|
+
// Already started or other error
|
|
213
|
+
stopRecording();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
recognition.start();
|
|
220
|
+
recording = true;
|
|
221
|
+
sttBtn.classList.add('stt-active');
|
|
222
|
+
sttBtn.innerHTML =
|
|
223
|
+
'<span class="stt-wave">' +
|
|
224
|
+
'<span class="stt-wave-bar"></span>' +
|
|
225
|
+
'<span class="stt-wave-bar"></span>' +
|
|
226
|
+
'<span class="stt-wave-bar"></span>' +
|
|
227
|
+
'<span class="stt-wave-bar"></span>' +
|
|
228
|
+
'<span class="stt-wave-bar"></span>' +
|
|
229
|
+
'</span>' +
|
|
230
|
+
'<span class="stt-stop-label">Stop</span>';
|
|
231
|
+
ctx.inputEl.setAttribute('placeholder', 'Listening...');
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error('[STT] Failed to start:', err);
|
|
234
|
+
if (ctx.addSystemMessage) {
|
|
235
|
+
ctx.addSystemMessage('Failed to start voice input: ' + err.message, true);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function stopRecording() {
|
|
241
|
+
if (!recording) return;
|
|
242
|
+
recording = false;
|
|
243
|
+
|
|
244
|
+
if (recognition) {
|
|
245
|
+
try { recognition.stop(); } catch (e) { /* ignore */ }
|
|
246
|
+
recognition = null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
sttBtn.classList.remove('stt-active');
|
|
250
|
+
sttBtn.innerHTML = iconHtml('mic');
|
|
251
|
+
refreshIcons();
|
|
252
|
+
ctx.inputEl.setAttribute('placeholder', 'Message Claude Code...');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- External lang setter (used by profile module) ---
|
|
256
|
+
export function setSTTLang(code) {
|
|
257
|
+
selectedLang = code;
|
|
258
|
+
saveLang(code);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function getSTTLang() {
|
|
262
|
+
return selectedLang;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- Exports ---
|
|
266
|
+
export function isSTTRecording() {
|
|
267
|
+
return recording;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function isSTTInitializing() {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
@@ -239,6 +239,20 @@ function createXtermForTab(tab) {
|
|
|
239
239
|
}
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
+
// Ctrl+V paste for Firefox (Firefox blocks xterm.js clipboard access)
|
|
243
|
+
bodyEl.addEventListener("keydown", function (e) {
|
|
244
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
if (navigator.clipboard && navigator.clipboard.readText) {
|
|
247
|
+
navigator.clipboard.readText().then(function (text) {
|
|
248
|
+
if (text && ctx.ws && ctx.connected) {
|
|
249
|
+
ctx.ws.send(JSON.stringify({ type: "term_input", id: tab.id, data: text }));
|
|
250
|
+
}
|
|
251
|
+
}).catch(function () { /* permission denied or not available */ });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
242
256
|
// Right-click context menu
|
|
243
257
|
bodyEl.addEventListener("contextmenu", function (e) {
|
|
244
258
|
showTermCtxMenu(e, tab);
|
|
@@ -599,6 +613,24 @@ function showTermCtxMenu(e, tab) {
|
|
|
599
613
|
});
|
|
600
614
|
menu.appendChild(copyItem);
|
|
601
615
|
|
|
616
|
+
// Paste
|
|
617
|
+
var pasteItem = document.createElement("button");
|
|
618
|
+
pasteItem.className = "term-ctx-item";
|
|
619
|
+
pasteItem.innerHTML = iconHtml("clipboard-paste") + " <span>Paste</span>";
|
|
620
|
+
pasteItem.addEventListener("click", function (ev) {
|
|
621
|
+
ev.stopPropagation();
|
|
622
|
+
closeTermCtxMenu();
|
|
623
|
+
if (!tab.xterm) return;
|
|
624
|
+
if (navigator.clipboard && navigator.clipboard.readText) {
|
|
625
|
+
navigator.clipboard.readText().then(function (text) {
|
|
626
|
+
if (text && ctx.ws && ctx.connected) {
|
|
627
|
+
ctx.ws.send(JSON.stringify({ type: "term_input", id: tab.id, data: text }));
|
|
628
|
+
}
|
|
629
|
+
}).catch(function () { /* permission denied or not available */ });
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
menu.appendChild(pasteItem);
|
|
633
|
+
|
|
602
634
|
// Clear
|
|
603
635
|
var clearItem = document.createElement("button");
|
|
604
636
|
clearItem.className = "term-ctx-item";
|
|
@@ -370,17 +370,10 @@ export function applyTheme(themeId, fromPicker) {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
// ---
|
|
373
|
+
// --- Favicon update on theme change ---
|
|
374
374
|
function updateMascotSvgs(vars, isLight) {
|
|
375
|
-
var
|
|
376
|
-
|
|
377
|
-
var src = isLight ? lightSrc : darkSrc;
|
|
378
|
-
var mascots = document.querySelectorAll("img.footer-mascot");
|
|
379
|
-
for (var i = 0; i < mascots.length; i++) {
|
|
380
|
-
mascots[i].setAttribute("src", src);
|
|
381
|
-
}
|
|
382
|
-
var faviconEl = document.querySelector('link[rel="icon"][type="image/svg+xml"]');
|
|
383
|
-
if (faviconEl) faviconEl.setAttribute("href", src);
|
|
375
|
+
var faviconEl = document.querySelector('link[rel="icon"][type="image/png"]');
|
|
376
|
+
if (faviconEl) faviconEl.setAttribute("href", "favicon-banded.png");
|
|
384
377
|
}
|
|
385
378
|
|
|
386
379
|
// --- Theme loading from server ---
|
package/lib/public/style.css
CHANGED
|
@@ -15,3 +15,9 @@
|
|
|
15
15
|
@import url("css/skills.css");
|
|
16
16
|
@import url("css/mobile-nav.css");
|
|
17
17
|
@import url("css/loop.css");
|
|
18
|
+
@import url("css/scheduler.css");
|
|
19
|
+
@import url("css/scheduler-modal.css");
|
|
20
|
+
@import url("css/home-hub.css");
|
|
21
|
+
@import url("css/playbook.css");
|
|
22
|
+
@import url("css/stt.css");
|
|
23
|
+
@import url("css/profile.css");
|
package/lib/public/sw.js
CHANGED
|
@@ -1,11 +1,88 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
var CACHE_NAME = "clay-offline-v1";
|
|
2
|
+
|
|
3
|
+
self.addEventListener("install", function (event) {
|
|
4
|
+
event.waitUntil(self.skipWaiting());
|
|
3
5
|
});
|
|
4
6
|
|
|
5
7
|
self.addEventListener("activate", function (event) {
|
|
6
|
-
|
|
8
|
+
// Clean up old cache versions
|
|
9
|
+
event.waitUntil(
|
|
10
|
+
caches.keys().then(function (names) {
|
|
11
|
+
return Promise.all(
|
|
12
|
+
names.filter(function (n) { return n !== CACHE_NAME; })
|
|
13
|
+
.map(function (n) { return caches.delete(n); })
|
|
14
|
+
);
|
|
15
|
+
}).then(function () {
|
|
16
|
+
return self.clients.claim();
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// --- Offline cache: network-first, cache-fallback ---
|
|
22
|
+
|
|
23
|
+
function shouldCache(request, response) {
|
|
24
|
+
if (request.method !== "GET") return false;
|
|
25
|
+
if (!response || !response.ok) return false;
|
|
26
|
+
// Cache same-origin static assets and CDN resources (jsdelivr, fonts)
|
|
27
|
+
var url = new URL(request.url);
|
|
28
|
+
if (url.origin === self.location.origin) return true;
|
|
29
|
+
if (url.hostname === "cdn.jsdelivr.net") return true;
|
|
30
|
+
if (url.hostname === "fonts.googleapis.com") return true;
|
|
31
|
+
if (url.hostname === "fonts.gstatic.com") return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
self.addEventListener("fetch", function (event) {
|
|
36
|
+
var request = event.request;
|
|
37
|
+
|
|
38
|
+
// Only handle GET requests
|
|
39
|
+
if (request.method !== "GET") return;
|
|
40
|
+
|
|
41
|
+
// Skip WebSocket upgrade requests and API/data endpoints
|
|
42
|
+
var url = new URL(request.url);
|
|
43
|
+
if (url.pathname.indexOf("/ws") !== -1) return;
|
|
44
|
+
if (url.pathname.indexOf("/api/") !== -1) return;
|
|
45
|
+
|
|
46
|
+
event.respondWith(
|
|
47
|
+
fetch(request).then(function (response) {
|
|
48
|
+
// Network succeeded: cache a clone for offline use
|
|
49
|
+
if (shouldCache(request, response)) {
|
|
50
|
+
var clone = response.clone();
|
|
51
|
+
caches.open(CACHE_NAME).then(function (cache) {
|
|
52
|
+
cache.put(request, clone);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return response;
|
|
56
|
+
}).catch(function () {
|
|
57
|
+
// Network failed: serve from cache
|
|
58
|
+
return caches.match(request).then(function (cached) {
|
|
59
|
+
if (cached) return cached;
|
|
60
|
+
|
|
61
|
+
// For navigation requests, serve cached index.html as fallback
|
|
62
|
+
// (handles /p/slug/ routes that all serve the same SPA shell)
|
|
63
|
+
if (request.mode === "navigate") {
|
|
64
|
+
return caches.match("/index.html").then(function (indexCached) {
|
|
65
|
+
// If even the index page is not cached, show a minimal offline page
|
|
66
|
+
if (indexCached) return indexCached;
|
|
67
|
+
return new Response(
|
|
68
|
+
"<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Clay</title>" +
|
|
69
|
+
"<style>body{margin:0;background:#000;color:#888;display:flex;" +
|
|
70
|
+
"align-items:center;justify-content:center;height:100vh;" +
|
|
71
|
+
"font-family:monospace;font-size:1.2em}</style></head>" +
|
|
72
|
+
"<body><p>Waiting for server…</p></body></html>",
|
|
73
|
+
{ headers: { "Content-Type": "text/html" } }
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Non-navigation request with no cache: return network error
|
|
78
|
+
return new Response("", { status: 503, statusText: "Offline" });
|
|
79
|
+
});
|
|
80
|
+
})
|
|
81
|
+
);
|
|
7
82
|
});
|
|
8
83
|
|
|
84
|
+
// --- Push notifications ---
|
|
85
|
+
|
|
9
86
|
self.addEventListener("push", function (event) {
|
|
10
87
|
var data = {};
|
|
11
88
|
try { data = event.data.json(); } catch (e) { return; }
|
|
@@ -46,6 +123,8 @@ self.addEventListener("push", function (event) {
|
|
|
46
123
|
);
|
|
47
124
|
});
|
|
48
125
|
|
|
126
|
+
// --- Notification click ---
|
|
127
|
+
|
|
49
128
|
self.addEventListener("notificationclick", function (event) {
|
|
50
129
|
var data = event.notification.data || {};
|
|
51
130
|
event.notification.close();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|