pinokiod 3.290.0 → 3.292.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/kernel/bin/xcode-tools.js +2 -0
- package/kernel/util.js +1 -0
- package/package.json +1 -1
- package/server/index.js +105 -3
- package/server/public/common.js +310 -6
|
@@ -100,6 +100,8 @@ async function readXcodeSelectVersion(exec) {
|
|
|
100
100
|
return { valid: false, reason: 'xcode-select --version failed' }
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
console.log('xcode-select --version', result)
|
|
104
|
+
|
|
103
105
|
const match = result && result.stdout && /xcode-select version\s+(\d+)/i.exec(result.stdout)
|
|
104
106
|
if (!match) {
|
|
105
107
|
return { valid: false, reason: 'unable to parse xcode-select version' }
|
package/kernel/util.js
CHANGED
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -128,6 +128,110 @@ class Server {
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
// process.env.CONDA_LIBMAMBA_SOLVER_DEBUG_LIBSOLV = 1
|
|
131
|
+
this.installFatalHandlers()
|
|
132
|
+
}
|
|
133
|
+
installFatalHandlers() {
|
|
134
|
+
if (this.fatalHandlersInstalled) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
const normalizeError = (value, origin) => {
|
|
138
|
+
if (value instanceof Error) {
|
|
139
|
+
return value
|
|
140
|
+
}
|
|
141
|
+
if (value && typeof value === 'object') {
|
|
142
|
+
try {
|
|
143
|
+
return new Error(`${origin || 'error'}: ${JSON.stringify(value)}`)
|
|
144
|
+
} catch (_) {
|
|
145
|
+
return new Error(String(value))
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === 'string') {
|
|
149
|
+
return new Error(value)
|
|
150
|
+
}
|
|
151
|
+
return new Error(`${origin || 'error'}: ${String(value)}`)
|
|
152
|
+
}
|
|
153
|
+
const invoke = (value, origin) => {
|
|
154
|
+
const error = normalizeError(value, origin)
|
|
155
|
+
try {
|
|
156
|
+
const maybePromise = this.handleFatalError(error, origin)
|
|
157
|
+
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
158
|
+
maybePromise.catch((fatalErr) => {
|
|
159
|
+
console.error('Fatal handler rejection:', fatalErr)
|
|
160
|
+
try {
|
|
161
|
+
process.exit(1)
|
|
162
|
+
} catch (_) {
|
|
163
|
+
// ignore
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
} catch (fatalErr) {
|
|
168
|
+
console.error('Fatal handler threw:', fatalErr)
|
|
169
|
+
try {
|
|
170
|
+
process.exit(1)
|
|
171
|
+
} catch (_) {
|
|
172
|
+
// ignore
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
process.on('uncaughtException', (error) => invoke(error, 'uncaughtException'))
|
|
177
|
+
process.on('unhandledRejection', (reason) => invoke(reason, 'unhandledRejection'))
|
|
178
|
+
this.fatalHandlersInstalled = true
|
|
179
|
+
}
|
|
180
|
+
async handleFatalError(error, origin) {
|
|
181
|
+
if (this.handlingFatalError) {
|
|
182
|
+
console.error(`[Pinokiod] Additional fatal (${origin})`, (error && error.stack) ? error.stack : error)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
this.handlingFatalError = true
|
|
186
|
+
const timestamp = Date.now()
|
|
187
|
+
const message = (error && error.message) ? error.message : 'Unexpected fatal error'
|
|
188
|
+
const stack = (error && error.stack) ? error.stack : String(error || 'Unknown fatal error')
|
|
189
|
+
console.error(`[Pinokiod] Fatal (${origin})`, stack)
|
|
190
|
+
const fallbackHome = path.resolve(os.homedir(), 'pinokio')
|
|
191
|
+
const homeDir = (this.kernel && this.kernel.homedir) ? this.kernel.homedir : fallbackHome
|
|
192
|
+
const fatalFile = path.resolve(homeDir, 'logs', 'fatal.json')
|
|
193
|
+
const payload = {
|
|
194
|
+
id: `fatal-${timestamp}`,
|
|
195
|
+
type: 'kernel.fatal',
|
|
196
|
+
severity: 'fatal',
|
|
197
|
+
title: 'Pinokio crashed',
|
|
198
|
+
message,
|
|
199
|
+
stack,
|
|
200
|
+
origin,
|
|
201
|
+
timestamp,
|
|
202
|
+
version: this.version,
|
|
203
|
+
pid: process.pid,
|
|
204
|
+
logPath: fatalFile,
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
await fs.promises.mkdir(path.dirname(fatalFile), { recursive: true })
|
|
208
|
+
await fs.promises.writeFile(fatalFile, JSON.stringify(payload, null, 2))
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error('Failed to persist fatal error details:', err)
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
if (typeof Util.emitPushEvent === 'function') {
|
|
214
|
+
Util.emitPushEvent(payload)
|
|
215
|
+
} else {
|
|
216
|
+
Util.push({ title: 'Pinokio crashed', message })
|
|
217
|
+
}
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error('Failed to emit fatal notification:', err)
|
|
220
|
+
}
|
|
221
|
+
if (!this.fatalExitTimer) {
|
|
222
|
+
this.fatalExitTimer = setTimeout(() => {
|
|
223
|
+
try {
|
|
224
|
+
this.shutdown('Fatal Error')
|
|
225
|
+
} catch (shutdownErr) {
|
|
226
|
+
console.error('Failed to shutdown after fatal error:', shutdownErr)
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
process.exit(1)
|
|
230
|
+
} catch (_) {
|
|
231
|
+
// ignore
|
|
232
|
+
}
|
|
233
|
+
}, 500)
|
|
234
|
+
}
|
|
131
235
|
}
|
|
132
236
|
stop() {
|
|
133
237
|
this.server.close()
|
|
@@ -3474,9 +3578,6 @@ class Server {
|
|
|
3474
3578
|
if (!this.log) {
|
|
3475
3579
|
this.log = fs.createWriteStream(path.resolve(homedir, "logs/stdout.txt"))
|
|
3476
3580
|
process.stdout.write = process.stderr.write = this.log.write.bind(this.log)
|
|
3477
|
-
process.on('uncaughtException', (err) => {
|
|
3478
|
-
console.error((err && err.stack) ? err.stack : err);
|
|
3479
|
-
});
|
|
3480
3581
|
this.logInterval = setInterval(async () => {
|
|
3481
3582
|
try {
|
|
3482
3583
|
let file = path.resolve(homedir, "logs/stdout.txt")
|
|
@@ -8737,6 +8838,7 @@ class Server {
|
|
|
8737
8838
|
// })
|
|
8738
8839
|
|
|
8739
8840
|
|
|
8841
|
+
|
|
8740
8842
|
// install
|
|
8741
8843
|
this.server = httpserver.createServer(this.app);
|
|
8742
8844
|
this.socket = new Socket(this)
|
package/server/public/common.js
CHANGED
|
@@ -1662,6 +1662,12 @@ if (typeof hotkeys === 'function') {
|
|
|
1662
1662
|
let currentSocket = null;
|
|
1663
1663
|
let reconnectTimeout = null;
|
|
1664
1664
|
let activeAudio = null;
|
|
1665
|
+
const fatalStorageKey = 'pinokio.kernel.fatal';
|
|
1666
|
+
const fatalStaleMs = 15 * 60 * 1000;
|
|
1667
|
+
let lastFatalPayload = null;
|
|
1668
|
+
let fatalOverlayEl = null;
|
|
1669
|
+
let fatalStyleInjected = false;
|
|
1670
|
+
let pendingFatalRender = null;
|
|
1665
1671
|
|
|
1666
1672
|
// Lightweight visual indicator to confirm notification receipt (mobile-friendly)
|
|
1667
1673
|
let notifyIndicatorEl = null;
|
|
@@ -1721,6 +1727,284 @@ if (typeof hotkeys === 'function') {
|
|
|
1721
1727
|
} catch (_) {}
|
|
1722
1728
|
};
|
|
1723
1729
|
|
|
1730
|
+
const runWhenDomReady = (fn) => {
|
|
1731
|
+
if (typeof fn !== 'function') {
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
if (document.readyState === 'loading') {
|
|
1735
|
+
document.addEventListener('DOMContentLoaded', () => fn(), { once: true });
|
|
1736
|
+
} else {
|
|
1737
|
+
fn();
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
|
|
1741
|
+
const ensureFatalOverlay = () => {
|
|
1742
|
+
if (!fatalStyleInjected) {
|
|
1743
|
+
try {
|
|
1744
|
+
const style = document.createElement('style');
|
|
1745
|
+
style.textContent = `
|
|
1746
|
+
.pinokio-fatal-overlay{position:fixed;inset:0;background:rgba(15,23,42,0.94);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;z-index:2147483646;opacity:0;pointer-events:none;transition:opacity .25s ease}
|
|
1747
|
+
.pinokio-fatal-overlay.show{opacity:1;pointer-events:auto}
|
|
1748
|
+
.pinokio-fatal-panel{max-width:960px;width:100%;background:#0f172a;color:#f8fafc;border-radius:18px;box-shadow:0 40px 120px rgba(0,0,0,.55);padding:24px;display:flex;flex-direction:column;gap:16px;border:1px solid rgba(148,163,184,.35);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif}
|
|
1749
|
+
.pinokio-fatal-header{display:flex;flex-wrap:wrap;justify-content:space-between;gap:12px;align-items:flex-start}
|
|
1750
|
+
.pinokio-fatal-header h2{margin:0;font-size:20px;line-height:1.3;font-weight:700}
|
|
1751
|
+
.pinokio-fatal-header small{display:block;margin-top:4px;color:rgba(226,232,240,.85);font-size:13px}
|
|
1752
|
+
.pinokio-fatal-message{font-size:15px;line-height:1.6;margin:0;color:#cbd5f5}
|
|
1753
|
+
.pinokio-fatal-stack{background:#020617;color:#f1f5f9;font-family:ui-monospace,SFMono-Regular,Consolas,Menlo,monospace;font-size:13px;line-height:1.45;border-radius:12px;padding:16px;max-height:320px;overflow:auto;border:1px solid rgba(15,118,110,.4)}
|
|
1754
|
+
.pinokio-fatal-meta{font-size:13px;color:#cbd5f5;display:flex;flex-wrap:wrap;gap:12px}
|
|
1755
|
+
.pinokio-fatal-actions{display:flex;flex-wrap:wrap;gap:10px}
|
|
1756
|
+
.pinokio-fatal-actions button{border:none;border-radius:999px;padding:10px 18px;font-weight:600;font-size:14px;cursor:pointer;transition:opacity .2s ease}
|
|
1757
|
+
.pinokio-fatal-actions button.primary{background:#f97316;color:#0f172a}
|
|
1758
|
+
.pinokio-fatal-actions button.secondary{background:rgba(148,163,184,.2);color:#e2e8f0}
|
|
1759
|
+
.pinokio-fatal-actions button:hover{opacity:.9}
|
|
1760
|
+
.pinokio-fatal-close{background:none;border:none;color:#e2e8f0;font-size:24px;line-height:1;cursor:pointer;padding:2px 6px;border-radius:8px}
|
|
1761
|
+
.pinokio-fatal-close:hover{background:rgba(148,163,184,.15)}
|
|
1762
|
+
@media (max-width:720px){.pinokio-fatal-panel{padding:18px}.pinokio-fatal-stack{max-height:220px;font-size:12px}}
|
|
1763
|
+
`;
|
|
1764
|
+
document.head.appendChild(style);
|
|
1765
|
+
fatalStyleInjected = true;
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
console.warn('Failed to inject fatal overlay styles:', err);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (!fatalOverlayEl) {
|
|
1771
|
+
try {
|
|
1772
|
+
const wrapper = document.createElement('div');
|
|
1773
|
+
wrapper.className = 'pinokio-fatal-overlay';
|
|
1774
|
+
wrapper.setAttribute('role', 'alertdialog');
|
|
1775
|
+
wrapper.setAttribute('aria-live', 'assertive');
|
|
1776
|
+
wrapper.innerHTML = `
|
|
1777
|
+
<div class="pinokio-fatal-panel">
|
|
1778
|
+
<div class="pinokio-fatal-header">
|
|
1779
|
+
<div>
|
|
1780
|
+
<h2>Pinokio crashed</h2>
|
|
1781
|
+
<small data-field="subtitle"></small>
|
|
1782
|
+
</div>
|
|
1783
|
+
<button class="pinokio-fatal-close" type="button" aria-label="Dismiss crash message" data-action="fatal-dismiss">×</button>
|
|
1784
|
+
</div>
|
|
1785
|
+
<p class="pinokio-fatal-message" data-field="message"></p>
|
|
1786
|
+
<pre class="pinokio-fatal-stack" data-field="stack"></pre>
|
|
1787
|
+
<div class="pinokio-fatal-meta">
|
|
1788
|
+
<span data-field="timestamp"></span>
|
|
1789
|
+
<span data-field="logPath"></span>
|
|
1790
|
+
</div>
|
|
1791
|
+
<div class="pinokio-fatal-actions">
|
|
1792
|
+
<button type="button" class="secondary" data-action="fatal-copy">Copy stack</button>
|
|
1793
|
+
<button type="button" class="secondary" data-action="fatal-dismiss">Dismiss</button>
|
|
1794
|
+
<button type="button" class="primary" data-action="fatal-reload">Reload</button>
|
|
1795
|
+
</div>
|
|
1796
|
+
</div>`;
|
|
1797
|
+
document.body.appendChild(wrapper);
|
|
1798
|
+
wrapper.addEventListener('click', (event) => {
|
|
1799
|
+
const action = (event.target && event.target.getAttribute) ? event.target.getAttribute('data-action') : null;
|
|
1800
|
+
if (!action) {
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
if (action === 'fatal-copy') {
|
|
1804
|
+
copyFatalDetails();
|
|
1805
|
+
} else if (action === 'fatal-dismiss') {
|
|
1806
|
+
dismissFatalNotice();
|
|
1807
|
+
} else if (action === 'fatal-reload') {
|
|
1808
|
+
dismissFatalNotice();
|
|
1809
|
+
try {
|
|
1810
|
+
window.location.reload();
|
|
1811
|
+
} catch (_) {}
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
fatalOverlayEl = wrapper;
|
|
1815
|
+
} catch (err) {
|
|
1816
|
+
console.error('Failed to create fatal overlay:', err);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
const updateFatalOverlayContent = (payload) => {
|
|
1822
|
+
if (!fatalOverlayEl || !payload) {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
try {
|
|
1826
|
+
const messageNode = fatalOverlayEl.querySelector('[data-field="message"]');
|
|
1827
|
+
if (messageNode) {
|
|
1828
|
+
messageNode.textContent = payload.message || 'An unrecoverable error occurred.';
|
|
1829
|
+
}
|
|
1830
|
+
const stackNode = fatalOverlayEl.querySelector('[data-field="stack"]');
|
|
1831
|
+
if (stackNode) {
|
|
1832
|
+
stackNode.textContent = payload.stack || 'No stack trace available';
|
|
1833
|
+
}
|
|
1834
|
+
const subtitleNode = fatalOverlayEl.querySelector('[data-field="subtitle"]');
|
|
1835
|
+
if (subtitleNode) {
|
|
1836
|
+
const origin = payload.origin ? payload.origin : 'fatal error';
|
|
1837
|
+
const parts = [];
|
|
1838
|
+
if (payload.version && typeof payload.version === 'object') {
|
|
1839
|
+
const pinokiod = payload.version.pinokiod ? `pinokiod ${payload.version.pinokiod}` : null;
|
|
1840
|
+
const pinokio = payload.version.pinokio ? `pinokio ${payload.version.pinokio}` : null;
|
|
1841
|
+
if (pinokiod || pinokio) {
|
|
1842
|
+
parts.push([pinokiod, pinokio].filter(Boolean).join(' • '));
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
parts.push(origin);
|
|
1846
|
+
subtitleNode.textContent = parts.join(' • ');
|
|
1847
|
+
}
|
|
1848
|
+
const stampNode = fatalOverlayEl.querySelector('[data-field="timestamp"]');
|
|
1849
|
+
if (stampNode) {
|
|
1850
|
+
const ts = payload.timestamp ? new Date(payload.timestamp) : new Date();
|
|
1851
|
+
stampNode.textContent = `Recorded: ${ts.toLocaleString()}`;
|
|
1852
|
+
}
|
|
1853
|
+
const logNode = fatalOverlayEl.querySelector('[data-field="logPath"]');
|
|
1854
|
+
if (logNode) {
|
|
1855
|
+
logNode.textContent = payload.logPath ? `Details saved to ${payload.logPath}` : '';
|
|
1856
|
+
}
|
|
1857
|
+
} catch (err) {
|
|
1858
|
+
console.error('Failed to populate fatal overlay:', err);
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
|
|
1862
|
+
const showFatalOverlay = (payload) => {
|
|
1863
|
+
lastFatalPayload = payload;
|
|
1864
|
+
const render = (data) => {
|
|
1865
|
+
ensureFatalOverlay();
|
|
1866
|
+
if (!fatalOverlayEl) {
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const content = data || payload;
|
|
1870
|
+
updateFatalOverlayContent(content);
|
|
1871
|
+
fatalOverlayEl.classList.add('show');
|
|
1872
|
+
};
|
|
1873
|
+
if (document.readyState === 'loading' || !document.body) {
|
|
1874
|
+
pendingFatalRender = payload;
|
|
1875
|
+
runWhenDomReady(() => {
|
|
1876
|
+
if (pendingFatalRender) {
|
|
1877
|
+
const queued = pendingFatalRender;
|
|
1878
|
+
pendingFatalRender = null;
|
|
1879
|
+
render(queued);
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
} else {
|
|
1883
|
+
render();
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
const hideFatalOverlay = () => {
|
|
1888
|
+
if (fatalOverlayEl) {
|
|
1889
|
+
fatalOverlayEl.classList.remove('show');
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
|
|
1893
|
+
const dismissFatalNotice = () => {
|
|
1894
|
+
hideFatalOverlay();
|
|
1895
|
+
try {
|
|
1896
|
+
if (storageEnabled) {
|
|
1897
|
+
localStorage.removeItem(fatalStorageKey);
|
|
1898
|
+
}
|
|
1899
|
+
} catch (_) {}
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
const copyFatalDetails = () => {
|
|
1903
|
+
if (!lastFatalPayload) {
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const lines = [];
|
|
1907
|
+
lines.push(lastFatalPayload.title || 'Pinokio crashed');
|
|
1908
|
+
lines.push(`When: ${new Date(lastFatalPayload.timestamp || Date.now()).toLocaleString()}`);
|
|
1909
|
+
if (lastFatalPayload.origin) {
|
|
1910
|
+
lines.push(`Origin: ${lastFatalPayload.origin}`);
|
|
1911
|
+
}
|
|
1912
|
+
if (lastFatalPayload.logPath) {
|
|
1913
|
+
lines.push(`Saved at: ${lastFatalPayload.logPath}`);
|
|
1914
|
+
}
|
|
1915
|
+
lines.push('');
|
|
1916
|
+
lines.push(lastFatalPayload.message || '');
|
|
1917
|
+
lines.push('');
|
|
1918
|
+
lines.push(lastFatalPayload.stack || '');
|
|
1919
|
+
const text = lines.join('\n');
|
|
1920
|
+
const fallbackCopy = () => {
|
|
1921
|
+
try {
|
|
1922
|
+
const textarea = document.createElement('textarea');
|
|
1923
|
+
textarea.value = text;
|
|
1924
|
+
textarea.style.position = 'fixed';
|
|
1925
|
+
textarea.style.opacity = '0';
|
|
1926
|
+
document.body.appendChild(textarea);
|
|
1927
|
+
textarea.focus();
|
|
1928
|
+
textarea.select();
|
|
1929
|
+
document.execCommand('copy');
|
|
1930
|
+
document.body.removeChild(textarea);
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
console.warn('Failed to copy crash log:', err);
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1936
|
+
navigator.clipboard.writeText(text).catch(fallbackCopy);
|
|
1937
|
+
} else {
|
|
1938
|
+
fallbackCopy();
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
const sanitizeFatalPayload = (payload) => {
|
|
1943
|
+
if (!payload || typeof payload !== 'object') {
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
const safeTimestamp = typeof payload.timestamp === 'number' ? payload.timestamp : Date.now();
|
|
1947
|
+
const sanitized = {
|
|
1948
|
+
id: typeof payload.id === 'string' ? payload.id : `fatal-${safeTimestamp}`,
|
|
1949
|
+
type: 'kernel.fatal',
|
|
1950
|
+
title: typeof payload.title === 'string' ? payload.title : 'Pinokio crashed',
|
|
1951
|
+
message: typeof payload.message === 'string' ? payload.message : 'Pinokio encountered a fatal error.',
|
|
1952
|
+
stack: typeof payload.stack === 'string' ? payload.stack : '',
|
|
1953
|
+
origin: typeof payload.origin === 'string' ? payload.origin : null,
|
|
1954
|
+
timestamp: safeTimestamp,
|
|
1955
|
+
version: (payload.version && typeof payload.version === 'object') ? payload.version : null,
|
|
1956
|
+
logPath: typeof payload.logPath === 'string' ? payload.logPath : null,
|
|
1957
|
+
severity: typeof payload.severity === 'string' ? payload.severity : 'fatal',
|
|
1958
|
+
};
|
|
1959
|
+
return sanitized;
|
|
1960
|
+
};
|
|
1961
|
+
|
|
1962
|
+
const persistFatalPayload = (payload) => {
|
|
1963
|
+
if (!storageEnabled || !payload) {
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
try {
|
|
1967
|
+
localStorage.setItem(fatalStorageKey, JSON.stringify(payload));
|
|
1968
|
+
} catch (err) {
|
|
1969
|
+
console.warn('Failed to persist fatal payload:', err);
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
const parseFatalValue = (value) => {
|
|
1974
|
+
if (!value) {
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
try {
|
|
1978
|
+
const parsed = JSON.parse(value);
|
|
1979
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
const sanitized = sanitizeFatalPayload(parsed);
|
|
1983
|
+
if (!sanitized) {
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
if (sanitized.timestamp && (Date.now() - sanitized.timestamp) > fatalStaleMs) {
|
|
1987
|
+
return null;
|
|
1988
|
+
}
|
|
1989
|
+
return sanitized;
|
|
1990
|
+
} catch (_) {
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
|
|
1995
|
+
const handleFatalPayload = (payload, options) => {
|
|
1996
|
+
const sanitized = sanitizeFatalPayload(payload);
|
|
1997
|
+
if (!sanitized) {
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
if (!lastFatalPayload || lastFatalPayload.id !== sanitized.id) {
|
|
2001
|
+
showFatalOverlay(sanitized);
|
|
2002
|
+
}
|
|
2003
|
+
if (!options || options.persist !== false) {
|
|
2004
|
+
persistFatalPayload(sanitized);
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
|
|
1724
2008
|
const leaderStorageKey = 'pinokio.notification.leader';
|
|
1725
2009
|
const leaderHeartbeatMs = 5000;
|
|
1726
2010
|
const leaderStaleMs = 15000;
|
|
@@ -1884,6 +2168,9 @@ if (typeof hotkeys === 'function') {
|
|
|
1884
2168
|
}
|
|
1885
2169
|
}
|
|
1886
2170
|
} catch (_) {}
|
|
2171
|
+
if (payload && payload.type === 'kernel.fatal') {
|
|
2172
|
+
handleFatalPayload(payload);
|
|
2173
|
+
}
|
|
1887
2174
|
// Visual confirmation regardless of audio outcome (useful on mobile)
|
|
1888
2175
|
flashNotifyIndicator(payload);
|
|
1889
2176
|
if (typeof payload.sound === 'string' && payload.sound) {
|
|
@@ -2004,14 +2291,25 @@ if (typeof hotkeys === 'function') {
|
|
|
2004
2291
|
}
|
|
2005
2292
|
|
|
2006
2293
|
window.addEventListener('storage', (event) => {
|
|
2007
|
-
if (event.key !==
|
|
2294
|
+
if (!event || typeof event.key !== 'string') {
|
|
2008
2295
|
return;
|
|
2009
2296
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2297
|
+
if (event.key === leaderStorageKey) {
|
|
2298
|
+
const data = parseLeaderValue(event.newValue);
|
|
2299
|
+
if (data && data.id === tabId) {
|
|
2300
|
+
startLeadership();
|
|
2301
|
+
} else {
|
|
2302
|
+
resignLeadership();
|
|
2303
|
+
}
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
if (event.key === fatalStorageKey) {
|
|
2307
|
+
const data = parseFatalValue(event.newValue);
|
|
2308
|
+
if (data) {
|
|
2309
|
+
handleFatalPayload(data, { persist: false });
|
|
2310
|
+
} else {
|
|
2311
|
+
hideFatalOverlay();
|
|
2312
|
+
}
|
|
2015
2313
|
}
|
|
2016
2314
|
});
|
|
2017
2315
|
|
|
@@ -2025,6 +2323,12 @@ if (typeof hotkeys === 'function') {
|
|
|
2025
2323
|
|
|
2026
2324
|
// Attempt to become leader immediately on load.
|
|
2027
2325
|
attemptLeadership();
|
|
2326
|
+
if (storageEnabled) {
|
|
2327
|
+
const storedFatal = parseFatalValue(localStorage.getItem(fatalStorageKey));
|
|
2328
|
+
if (storedFatal) {
|
|
2329
|
+
handleFatalPayload(storedFatal, { persist: false });
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2028
2332
|
})();
|
|
2029
2333
|
|
|
2030
2334
|
// Mobile "Tap to connect" curtain to prime audio on the top-level page
|