daedalus-cli 3.75.0 → 3.76.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/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/dist/agents/roles.d.ts.map +1 -1
- package/dist/agents/roles.js +4 -1
- package/dist/agents/roles.js.map +1 -1
- package/dist/commands/webui.js +5 -5
- package/dist/commands/webui.js.map +1 -1
- package/dist/marathon/engine.js +1 -1
- package/dist/marathon/engine.js.map +1 -1
- package/dist/marathon/evaluator.d.ts +1 -0
- package/dist/marathon/evaluator.d.ts.map +1 -1
- package/dist/marathon/evaluator.js +33 -5
- package/dist/marathon/evaluator.js.map +1 -1
- package/dist/marathon/planner.d.ts.map +1 -1
- package/dist/marathon/planner.js +2 -1
- package/dist/marathon/planner.js.map +1 -1
- package/dist/tools/builtin/files.d.ts.map +1 -1
- package/dist/tools/builtin/files.js +11 -1
- package/dist/tools/builtin/files.js.map +1 -1
- package/dist/webui/install.test.d.ts +2 -0
- package/dist/webui/install.test.d.ts.map +1 -0
- package/dist/webui/install.test.js +43 -0
- package/dist/webui/install.test.js.map +1 -0
- package/dist/webui/public/apple-touch-icon.png +0 -0
- package/dist/webui/public/icon-192.png +0 -0
- package/dist/webui/public/icon-512.png +0 -0
- package/dist/webui/public/index.html +116 -32
- package/dist/webui/public/logo.png +0 -0
- package/dist/webui/public/manifest.json +25 -0
- package/dist/webui/public/script.js +387 -8
- package/dist/webui/public/styles.css +773 -3
- package/dist/webui/public/sw.js +81 -0
- package/dist/webui/public/types.d.ts +9 -0
- package/dist/webui/public/types.d.ts.map +1 -0
- package/dist/webui/public/types.js +2 -0
- package/dist/webui/public/types.js.map +1 -0
- package/dist/webui/public/types.ts +8 -0
- package/dist/webui/qr.d.ts +23 -0
- package/dist/webui/qr.d.ts.map +1 -0
- package/dist/webui/qr.js +60 -0
- package/dist/webui/qr.js.map +1 -0
- package/dist/webui/qr.test.d.ts +2 -0
- package/dist/webui/qr.test.d.ts.map +1 -0
- package/dist/webui/qr.test.js +34 -0
- package/dist/webui/qr.test.js.map +1 -0
- package/dist/webui/server.d.ts +3 -2
- package/dist/webui/server.d.ts.map +1 -1
- package/dist/webui/server.js +93 -36
- package/dist/webui/server.js.map +1 -1
- package/dist/webui/server.test.js +33 -1
- package/dist/webui/server.test.js.map +1 -1
- package/dist/webui/styles.test.d.ts +2 -0
- package/dist/webui/styles.test.d.ts.map +1 -0
- package/dist/webui/styles.test.js +19 -0
- package/dist/webui/styles.test.js.map +1 -0
- package/dist/webui/ws.d.ts +17 -0
- package/dist/webui/ws.d.ts.map +1 -0
- package/dist/webui/ws.js +57 -0
- package/dist/webui/ws.js.map +1 -0
- package/dist/webui/ws.test.d.ts +2 -0
- package/dist/webui/ws.test.d.ts.map +1 -0
- package/dist/webui/ws.test.js +66 -0
- package/dist/webui/ws.test.js.map +1 -0
- package/package.json +4 -1
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
// Service worker registration
|
|
2
|
+
if ('serviceWorker' in navigator) {
|
|
3
|
+
navigator.serviceWorker.register('/sw.js')
|
|
4
|
+
.then(registration => {
|
|
5
|
+
console.log('[script.js] Service worker registered successfully:', registration);
|
|
6
|
+
|
|
7
|
+
registration.addEventListener('updatefound', () => {
|
|
8
|
+
console.log('[script.js] Service worker update found');
|
|
9
|
+
const newWorker = registration.installing;
|
|
10
|
+
if (newWorker) {
|
|
11
|
+
newWorker.addEventListener('statechange', event => {
|
|
12
|
+
if (event.target.state === 'installed') {
|
|
13
|
+
if (navigator.serviceWorker.controller) {
|
|
14
|
+
console.log('[script.js] New service worker installed, page will reload on next navigation');
|
|
15
|
+
addLog('Service worker update available. Page will reload on next visit.');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
})
|
|
22
|
+
.catch(error => {
|
|
23
|
+
console.warn('[script.js] Service worker registration failed:', error);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let refreshing = false;
|
|
27
|
+
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
28
|
+
if (!refreshing) {
|
|
29
|
+
refreshing = true;
|
|
30
|
+
console.log('[script.js] Service worker controller changed, reloading page');
|
|
31
|
+
window.location.reload();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
1
36
|
const statusEl = document.getElementById('connection-status');
|
|
2
37
|
const lastUpdateEl = document.getElementById('last-update');
|
|
3
38
|
const logContainer = document.getElementById('log-container');
|
|
@@ -287,7 +322,7 @@ function renderToolStart(toolName) {
|
|
|
287
322
|
|
|
288
323
|
const icon = document.createElement('span');
|
|
289
324
|
icon.className = 'tool-tree-icon';
|
|
290
|
-
icon.
|
|
325
|
+
icon.innerHTML = '<svg style="width: 12px; height: 12px; display: inline-block; vertical-align: -1px;" viewBox="0 0 24 24" fill="currentColor"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>';
|
|
291
326
|
|
|
292
327
|
const label = document.createElement('span');
|
|
293
328
|
label.className = 'tool-tree-label';
|
|
@@ -323,16 +358,15 @@ function renderToolStart(toolName) {
|
|
|
323
358
|
details.className = 'tool-tree-details hidden';
|
|
324
359
|
|
|
325
360
|
header.addEventListener('click', () => {
|
|
326
|
-
|
|
327
|
-
|
|
361
|
+
const isHidden = details.classList.toggle('hidden');
|
|
362
|
+
chevron.style.transform = isHidden ? 'rotate(0deg)' : 'rotate(90deg)';
|
|
328
363
|
});
|
|
329
364
|
|
|
330
365
|
tree.appendChild(header);
|
|
331
366
|
tree.appendChild(details);
|
|
332
367
|
chatMessages.appendChild(tree);
|
|
333
|
-
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
334
|
-
|
|
335
368
|
currentToolTreeEl = tree;
|
|
369
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
336
370
|
}
|
|
337
371
|
|
|
338
372
|
const label = currentToolTreeEl.querySelector('.tool-tree-label');
|
|
@@ -365,7 +399,7 @@ function renderToolResult(toolName, resultSummary) {
|
|
|
365
399
|
|
|
366
400
|
const summary = document.createElement('span');
|
|
367
401
|
summary.className = 'tool-tree-item-summary';
|
|
368
|
-
summary.textContent = resultSummary ? `(${resultSummary})` : '
|
|
402
|
+
summary.textContent = resultSummary ? `(${resultSummary})` : 'completed';
|
|
369
403
|
|
|
370
404
|
item.appendChild(rail);
|
|
371
405
|
item.appendChild(name);
|
|
@@ -381,7 +415,7 @@ function renderToolResult(toolName, resultSummary) {
|
|
|
381
415
|
const badge = currentToolTreeEl.querySelector('.tool-tree-badge');
|
|
382
416
|
if (badge) {
|
|
383
417
|
badge.className = 'tool-tree-badge completed';
|
|
384
|
-
badge.textContent = '
|
|
418
|
+
badge.textContent = 'DONE';
|
|
385
419
|
}
|
|
386
420
|
|
|
387
421
|
currentToolTreeEl.classList.remove('running');
|
|
@@ -1217,6 +1251,58 @@ if (modelModal) {
|
|
|
1217
1251
|
});
|
|
1218
1252
|
}
|
|
1219
1253
|
|
|
1254
|
+
// ─────────────────────────────────────────────────────────────
|
|
1255
|
+
// QR Code Pairing Modal — M-5 / M-6
|
|
1256
|
+
// ─────────────────────────────────────────────────────────────
|
|
1257
|
+
const qrPairBtn = document.getElementById('qr-pair-btn');
|
|
1258
|
+
const qrModal = document.getElementById('qr-modal');
|
|
1259
|
+
const qrModalClose = document.getElementById('qr-modal-close');
|
|
1260
|
+
const qrImage = document.getElementById('qr-image');
|
|
1261
|
+
const qrWsUrl = document.getElementById('qr-ws-url');
|
|
1262
|
+
|
|
1263
|
+
async function openQrModal() {
|
|
1264
|
+
if (qrModal) {
|
|
1265
|
+
qrModal.classList.remove('hidden');
|
|
1266
|
+
let webUrl = window.location.origin;
|
|
1267
|
+
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
|
1268
|
+
try {
|
|
1269
|
+
const res = await fetch('/api/pairing-url');
|
|
1270
|
+
if (res.ok) {
|
|
1271
|
+
const data = await res.json();
|
|
1272
|
+
if (data.url) webUrl = data.url;
|
|
1273
|
+
}
|
|
1274
|
+
} catch {}
|
|
1275
|
+
}
|
|
1276
|
+
if (qrWsUrl) {
|
|
1277
|
+
qrWsUrl.textContent = webUrl;
|
|
1278
|
+
}
|
|
1279
|
+
if (qrImage) {
|
|
1280
|
+
qrImage.src = `/api/qr?url=${encodeURIComponent(webUrl)}&t=${Date.now()}`;
|
|
1281
|
+
}
|
|
1282
|
+
addLog(`QR pairing portal opened: <strong>${webUrl}</strong>`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function closeQrModal() {
|
|
1287
|
+
if (qrModal) {
|
|
1288
|
+
qrModal.classList.add('hidden');
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (qrPairBtn) {
|
|
1293
|
+
qrPairBtn.addEventListener('click', openQrModal);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (qrModalClose) {
|
|
1297
|
+
qrModalClose.addEventListener('click', closeQrModal);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if (qrModal) {
|
|
1301
|
+
qrModal.addEventListener('click', (e) => {
|
|
1302
|
+
if (e.target === qrModal) closeQrModal();
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1220
1306
|
async function loadUserProfile() {
|
|
1221
1307
|
try {
|
|
1222
1308
|
const res = await fetch('/api/profile');
|
|
@@ -1231,13 +1317,306 @@ async function loadUserProfile() {
|
|
|
1231
1317
|
} catch {}
|
|
1232
1318
|
}
|
|
1233
1319
|
|
|
1320
|
+
// ─────────────────────────────────────────────────────────────
|
|
1321
|
+
// Touch-Optimized UI — M-4: TouchTargetConfig interface
|
|
1322
|
+
// ─────────────────────────────────────────────────────────────
|
|
1323
|
+
/**
|
|
1324
|
+
* @typedef {Object} TouchTargetConfig
|
|
1325
|
+
* @property {string} selector - CSS selector for the interactive element
|
|
1326
|
+
* @property {number} [minSizePx=48] - Minimum width and height in pixels
|
|
1327
|
+
* @property {string} [touchAction='manipulation'] - touch-action CSS value
|
|
1328
|
+
*/
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Apply touch optimizations by injecting CSS for minimum tap targets
|
|
1332
|
+
* and touch-action on elements matching the given selectors.
|
|
1333
|
+
* @param {TouchTargetConfig[]} configs
|
|
1334
|
+
*/
|
|
1335
|
+
function applyTouchOptimizations(configs) {
|
|
1336
|
+
if (!configs || configs.length === 0) return;
|
|
1337
|
+
|
|
1338
|
+
const styleId = 'touch-optimization-styles';
|
|
1339
|
+
let styleEl = document.getElementById(styleId);
|
|
1340
|
+
|
|
1341
|
+
if (!styleEl) {
|
|
1342
|
+
styleEl = document.createElement('style');
|
|
1343
|
+
styleEl.id = styleId;
|
|
1344
|
+
document.head.appendChild(styleEl);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const rules = configs.map(function (cfg) {
|
|
1348
|
+
var minSize = cfg.minSizePx !== undefined ? cfg.minSizePx : 48;
|
|
1349
|
+
var touchAction = cfg.touchAction !== undefined ? cfg.touchAction : 'manipulation';
|
|
1350
|
+
var sel = cfg.selector;
|
|
1351
|
+
return [
|
|
1352
|
+
sel + ' {',
|
|
1353
|
+
' min-width: ' + minSize + 'px;',
|
|
1354
|
+
' min-height: ' + minSize + 'px;',
|
|
1355
|
+
' touch-action: ' + touchAction + ';',
|
|
1356
|
+
'}',
|
|
1357
|
+
].join('\n');
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
styleEl.textContent = rules.join('\n');
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// ─────────────────────────────────────────────────────────────
|
|
1364
|
+
// Critical element pointerdown listeners — M-4
|
|
1365
|
+
// ─────────────────────────────────────────────────────────────
|
|
1366
|
+
/**
|
|
1367
|
+
* Add both click and pointerdown listeners to ensure tactile responsiveness.
|
|
1368
|
+
* pointerdown fires before click and works for both touch and mouse.
|
|
1369
|
+
* @param {HTMLElement|null} el
|
|
1370
|
+
* @param {() => void} handler
|
|
1371
|
+
*/
|
|
1372
|
+
function addDualInteractionListeners(el, handler) {
|
|
1373
|
+
if (!el) return;
|
|
1374
|
+
el.addEventListener('pointerdown', function (e) {
|
|
1375
|
+
e.preventDefault();
|
|
1376
|
+
handler();
|
|
1377
|
+
});
|
|
1378
|
+
// Keep click for keyboard / accessibility
|
|
1379
|
+
if (!el.dataset._hasClickBound) {
|
|
1380
|
+
el.addEventListener('click', handler);
|
|
1381
|
+
el.dataset._hasClickBound = 'true';
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Wire up pointerdown on all critical interactive elements
|
|
1386
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
1387
|
+
// #active-model-badge — opens model selection modal
|
|
1388
|
+
addDualInteractionListeners(
|
|
1389
|
+
document.getElementById('active-model-badge'),
|
|
1390
|
+
openModelModal
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
// #clear-log — clears the log panel
|
|
1394
|
+
addDualInteractionListeners(
|
|
1395
|
+
document.getElementById('clear-log'),
|
|
1396
|
+
function () {
|
|
1397
|
+
if (logContainer) logContainer.innerHTML = '';
|
|
1398
|
+
}
|
|
1399
|
+
);
|
|
1400
|
+
|
|
1401
|
+
// #new-chat-btn — starts a new chat session
|
|
1402
|
+
addDualInteractionListeners(
|
|
1403
|
+
document.getElementById('new-chat-btn'),
|
|
1404
|
+
function () {
|
|
1405
|
+
if (chatMessages) chatMessages.innerHTML = '';
|
|
1406
|
+
addLog('New rite initiated. The cosmos awaits your query.');
|
|
1407
|
+
}
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
// #attach-btn — triggers file attachment
|
|
1411
|
+
addDualInteractionListeners(
|
|
1412
|
+
document.getElementById('attach-btn'),
|
|
1413
|
+
function () {
|
|
1414
|
+
var fileInput = document.getElementById('file-input');
|
|
1415
|
+
if (fileInput) fileInput.click();
|
|
1416
|
+
}
|
|
1417
|
+
);
|
|
1418
|
+
|
|
1419
|
+
// #send-btn — submits the chat form
|
|
1420
|
+
addDualInteractionListeners(
|
|
1421
|
+
document.getElementById('send-btn'),
|
|
1422
|
+
function () {
|
|
1423
|
+
var form = document.getElementById('chat-form');
|
|
1424
|
+
if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
1425
|
+
}
|
|
1426
|
+
);
|
|
1427
|
+
|
|
1428
|
+
// #model-modal-close — closes model selection modal
|
|
1429
|
+
addDualInteractionListeners(
|
|
1430
|
+
document.getElementById('model-modal-close'),
|
|
1431
|
+
closeModelModal
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
// #qr-pair-btn — opens QR pairing modal
|
|
1435
|
+
addDualInteractionListeners(
|
|
1436
|
+
document.getElementById('qr-pair-btn'),
|
|
1437
|
+
openQrModal
|
|
1438
|
+
);
|
|
1439
|
+
|
|
1440
|
+
// #qr-modal-close — closes QR pairing modal
|
|
1441
|
+
addDualInteractionListeners(
|
|
1442
|
+
document.getElementById('qr-modal-close'),
|
|
1443
|
+
closeQrModal
|
|
1444
|
+
);
|
|
1445
|
+
|
|
1446
|
+
// Apply global touch optimizations via CSS
|
|
1447
|
+
applyTouchOptimizations([
|
|
1448
|
+
{ selector: 'button', minSizePx: 48, touchAction: 'manipulation' },
|
|
1449
|
+
{ selector: 'a[href]', minSizePx: 48, touchAction: 'manipulation' },
|
|
1450
|
+
{ selector: '.model-pill', minSizePx: 44, touchAction: 'manipulation' },
|
|
1451
|
+
{ selector: '.clear-btn', minSizePx: 48, touchAction: 'manipulation' },
|
|
1452
|
+
{ selector: '.chat-header-btn', minSizePx: 48, touchAction: 'manipulation' },
|
|
1453
|
+
{ selector: '.attach-btn', minSizePx: 44, touchAction: 'manipulation' },
|
|
1454
|
+
{ selector: '#send-btn', minSizePx: 48, touchAction: 'manipulation' },
|
|
1455
|
+
{ selector: '.model-option-card', minSizePx: 48, touchAction: 'manipulation' },
|
|
1456
|
+
{ selector: '.code-copy-btn', minSizePx: 36, touchAction: 'manipulation' },
|
|
1457
|
+
]);
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
// ─────────────────────────────────────────────────────────────
|
|
1461
|
+
// WebSocket & Milestone Push Notifications — M-7
|
|
1462
|
+
// ─────────────────────────────────────────────────────────────
|
|
1463
|
+
let wsClient = null;
|
|
1464
|
+
let wsReconnectTimer = null;
|
|
1465
|
+
|
|
1466
|
+
function showMilestoneNotification(payload) {
|
|
1467
|
+
if (!payload || payload.type !== 'milestone') return;
|
|
1468
|
+
|
|
1469
|
+
const title = `[DAEDALUS] Milestone ${payload.id ? payload.id.toUpperCase() : ''}: ${payload.title || 'Update'}`;
|
|
1470
|
+
const options = {
|
|
1471
|
+
body: payload.summary || `Status: ${(payload.status || 'passed').toUpperCase()}${payload.score !== undefined ? ` (${payload.score}/100)` : ''}`,
|
|
1472
|
+
icon: '/favicon.svg',
|
|
1473
|
+
badge: '/favicon.svg',
|
|
1474
|
+
tag: `daedalus-milestone-${payload.id || Date.now()}`,
|
|
1475
|
+
renotify: true,
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
if ('Notification' in window) {
|
|
1479
|
+
if (Notification.permission === 'granted') {
|
|
1480
|
+
try {
|
|
1481
|
+
new Notification(title, options);
|
|
1482
|
+
} catch {}
|
|
1483
|
+
} else if (Notification.permission !== 'denied') {
|
|
1484
|
+
Notification.requestPermission().then(function (permission) {
|
|
1485
|
+
if (permission === 'granted') {
|
|
1486
|
+
try {
|
|
1487
|
+
new Notification(title, options);
|
|
1488
|
+
} catch {}
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Also log into Oracle system log
|
|
1495
|
+
addLog(`Milestone <strong>${payload.id || ''}</strong>: ${payload.title} — <em>${(payload.status || 'passed').toUpperCase()}</em>`);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function connectWebSocket() {
|
|
1499
|
+
if (wsClient && (wsClient.readyState === WebSocket.OPEN || wsClient.readyState === WebSocket.CONNECTING)) {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1504
|
+
const wsUrl = `${protocol}//${window.location.host}`;
|
|
1505
|
+
|
|
1506
|
+
try {
|
|
1507
|
+
wsClient = new WebSocket(wsUrl);
|
|
1508
|
+
|
|
1509
|
+
wsClient.onopen = function () {
|
|
1510
|
+
if (wsReconnectTimer) {
|
|
1511
|
+
clearTimeout(wsReconnectTimer);
|
|
1512
|
+
wsReconnectTimer = null;
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
wsClient.onmessage = function (event) {
|
|
1517
|
+
try {
|
|
1518
|
+
const data = JSON.parse(event.data);
|
|
1519
|
+
if (data.type === 'milestone') {
|
|
1520
|
+
showMilestoneNotification(data);
|
|
1521
|
+
}
|
|
1522
|
+
} catch {}
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
wsClient.onclose = function () {
|
|
1526
|
+
wsClient = null;
|
|
1527
|
+
if (!wsReconnectTimer) {
|
|
1528
|
+
wsReconnectTimer = setTimeout(connectWebSocket, 3000);
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
wsClient.onerror = function () {
|
|
1533
|
+
try { wsClient.close(); } catch {}
|
|
1534
|
+
};
|
|
1535
|
+
} catch {}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// ─────────────────────────────────────────────────────────────
|
|
1539
|
+
// Bootstrapping
|
|
1540
|
+
// ─────────────────────────────────────────────────────────────
|
|
1234
1541
|
connectSSE();
|
|
1542
|
+
connectWebSocket();
|
|
1235
1543
|
loadModels();
|
|
1236
1544
|
loadUserProfile();
|
|
1237
1545
|
|
|
1238
|
-
setInterval(()
|
|
1546
|
+
setInterval(function () {
|
|
1239
1547
|
if (lastUpdateEl) {
|
|
1240
1548
|
lastUpdateEl.textContent = new Date().toLocaleTimeString();
|
|
1241
1549
|
}
|
|
1242
1550
|
}, 1000);
|
|
1243
1551
|
|
|
1552
|
+
// ─────────────────────────────────────────────────────────────
|
|
1553
|
+
// PWA Install Prompt Handling — M-8
|
|
1554
|
+
// ─────────────────────────────────────────────────────────────
|
|
1555
|
+
let deferredPrompt = null;
|
|
1556
|
+
const installBanner = document.getElementById('install-banner');
|
|
1557
|
+
const installButton = document.getElementById('install-button');
|
|
1558
|
+
const installBannerCloseButtons = document.querySelectorAll('.install-banner-close, .install-banner-dismiss-btn');
|
|
1559
|
+
|
|
1560
|
+
function showInstallBanner() {
|
|
1561
|
+
if (installBanner) {
|
|
1562
|
+
installBanner.classList.remove('hidden');
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function hideInstallBanner() {
|
|
1567
|
+
if (installBanner) {
|
|
1568
|
+
installBanner.classList.add('hidden');
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
window.addEventListener('beforeinstallprompt', function (event) {
|
|
1573
|
+
event.preventDefault();
|
|
1574
|
+
deferredPrompt = event;
|
|
1575
|
+
console.log('[script.js] beforeinstallprompt captured, install banner ready');
|
|
1576
|
+
showInstallBanner();
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
if (installButton) {
|
|
1580
|
+
installButton.addEventListener('click', async function () {
|
|
1581
|
+
if (!deferredPrompt) {
|
|
1582
|
+
console.warn('[script.js] No deferred install prompt available');
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
try {
|
|
1587
|
+
const result = await deferredPrompt.prompt();
|
|
1588
|
+
const choice = result && result.outcome ? result.outcome : 'unknown';
|
|
1589
|
+
|
|
1590
|
+
if (choice === 'accepted') {
|
|
1591
|
+
console.log('[script.js] User accepted PWA install prompt');
|
|
1592
|
+
addLog('PWA install accepted. Daedalus will be added to your home screen.');
|
|
1593
|
+
} else {
|
|
1594
|
+
console.log('[script.js] User dismissed PWA install prompt:', choice);
|
|
1595
|
+
addLog('PWA install dismissed by user.');
|
|
1596
|
+
}
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
console.error('[script.js] PWA install prompt failed:', err);
|
|
1599
|
+
addLog('PWA install prompt encountered an error.');
|
|
1600
|
+
} finally {
|
|
1601
|
+
deferredPrompt = null;
|
|
1602
|
+
hideInstallBanner();
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
installBannerCloseButtons.forEach(btn => {
|
|
1608
|
+
btn.addEventListener('click', function () {
|
|
1609
|
+
console.log('[script.js] Install banner dismissed by user');
|
|
1610
|
+
deferredPrompt = null;
|
|
1611
|
+
hideInstallBanner();
|
|
1612
|
+
});
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
window.addEventListener('appinstalled', function () {
|
|
1616
|
+
console.log('[script.js] PWA installed successfully');
|
|
1617
|
+
addLog('Daedalus has been installed as an app.');
|
|
1618
|
+
deferredPrompt = null;
|
|
1619
|
+
hideInstallBanner();
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
|