Skip to content

Instantly share code, notes, and snippets.

@monperrus
Created August 26, 2026 06:20
Show Gist options
  • Select an option

  • Save monperrus/5a4e746f8fbce259d9d66a9decc85647 to your computer and use it in GitHub Desktop.

Select an option

Save monperrus/5a4e746f8fbce259d9d66a9decc85647 to your computer and use it in GitHub Desktop.
Martin's Super Webapp tab: Run
{
"1": {
"tab": {
"id": "run",
"label": "Run",
"html": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n<title>Run Pace</title>\n<style>\n html, body { margin: 0; height: 100%; background: #0d1117; color: #c9d1d9;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }\n * { box-sizing: border-box; }\n \n .app { display: flex; flex-direction: column; height: 100vh; overflow: hidden; padding: 12px; gap: 8px; }\n \n .header { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }\n .header h1 { margin: 0; font-size: 16px; font-weight: 700; display: flex; align-items: center; gap: 6px; }\n .status-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }\n .status-dot.idle { background: #8b949e; }\n .status-dot.running { background: #3fb950; box-shadow: 0 0 12px rgba(63,185,80,0.5); animation: pulse 1.5s infinite; }\n .status-dot.error { background: #f85149; box-shadow: 0 0 12px rgba(248,81,73,0.5); }\n @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }\n .header .badge { font-size: 10px; background: rgba(88,166,255,0.15); color: #58a6ff; padding: 3px 10px; border-radius: 20px; font-weight: 600; }\n \n .pace-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n min-height: 0;\n background: rgba(22,27,34,0.5);\n border: 1px solid #30363d;\n border-radius: 20px;\n padding: 16px;\n position: relative;\n overflow: hidden;\n }\n .pace-container::before {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: 20px;\n opacity: 0;\n transition: opacity 0.5s;\n pointer-events: none;\n }\n .pace-container.warning::before {\n background: radial-gradient(ellipse at center, rgba(248,81,73,0.08), transparent 70%);\n opacity: 1;\n }\n .pace-container.danger::before {\n background: radial-gradient(ellipse at center, rgba(248,81,73,0.15), transparent 70%);\n opacity: 1;\n }\n \n .pace-label { font-size: 13px; color: #8b949e; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 4px; z-index: 1; }\n .pace-value {\n font-size: clamp(64px, 18vw, 120px);\n font-weight: 900;\n color: #f0f6fc;\n line-height: 1;\n z-index: 1;\n transition: color 0.3s;\n letter-spacing: -1px;\n font-variant-numeric: tabular-nums;\n }\n .pace-value.fast { color: #3fb950; }\n .pace-value.slow { color: #d29922; }\n .pace-value.too-slow { color: #f85149; }\n .pace-unit { font-size: 14px; color: #8b949e; margin-top: 4px; z-index: 1; }\n \n .pace-bar-wrapper { width: 100%; max-width: 320px; height: 6px; background: #21262d; border-radius: 3px; margin-top: 16px; overflow: hidden; z-index: 1; }\n .pace-bar { height: 100%; border-radius: 3px; width: 0%; transition: width 0.5s ease, background 0.5s; }\n .pace-bar.good { background: #3fb950; }\n .pace-bar.warning { background: #d29922; }\n .pace-bar.bad { background: #f85149; }\n \n .stats-row { display: flex; gap: 8px; flex-shrink: 0; }\n .stat-card {\n flex: 1;\n background: rgba(22,27,34,0.6);\n border: 1px solid #30363d;\n border-radius: 12px;\n padding: 10px 8px;\n text-align: center;\n min-height: 44px;\n }\n .stat-card .stat-val { font-size: 18px; font-weight: 700; color: #f0f6fc; font-variant-numeric: tabular-nums; }\n .stat-card .stat-label { font-size: 9px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }\n \n .controls { display: flex; gap: 6px; flex-shrink: 0; }\n .controls button {\n min-height: 50px;\n border: 1px solid #30363d;\n background: #21262d;\n color: #e6edf3;\n border-radius: 12px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 0 16px;\n transition: all 0.15s;\n flex: 1;\n font-family: inherit;\n }\n .controls button:active { transform: scale(0.96); }\n .controls button.primary {\n background: linear-gradient(135deg, #238636, #2ea043);\n border-color: transparent;\n color: #fff;\n flex: 2;\n }\n .controls button.primary.running {\n background: linear-gradient(135deg, #f85149, #da3633);\n box-shadow: 0 2px 8px rgba(248,81,73,0.3);\n }\n .controls button.danger { border-color: #f85149; color: #f85149; }\n \n .sound-meter { display: flex; align-items: center; gap: 8px; flex-shrink: 0; padding: 6px 0; }\n .sound-meter .meter-label { font-size: 11px; color: #8b949e; flex-shrink: 0; }\n .sound-meter .meter-track { flex: 1; height: 4px; background: #21262d; border-radius: 2px; overflow: hidden; }\n .sound-meter .meter-fill { height: 100%; border-radius: 2px; width: 0%; background: #f85149; transition: width 0.3s; }\n .sound-meter .meter-val { font-size: 12px; color: #8b949e; min-width: 30px; text-align: right; font-variant-numeric: tabular-nums; }\n \n .log-toggle {\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 5;\n min-width: 40px;\n min-height: 40px;\n background: rgba(22,27,34,0.8);\n backdrop-filter: blur(8px);\n border: 1px solid #30363d;\n border-radius: 10px;\n color: #8b949e;\n font-size: 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.15s;\n }\n .log-toggle:active { transform: scale(0.92); }\n .log-panel {\n position: absolute; inset: 0; z-index: 10;\n background: rgba(13,17,23,0.96);\n backdrop-filter: blur(12px);\n display: none;\n flex-direction: column;\n border-radius: 20px;\n }\n .log-panel.open { display: flex; }\n .log-panel .log-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid #30363d; }\n .log-panel .log-header h3 { margin: 0; font-size: 14px; color: #e6edf3; }\n .log-panel .log-header button { min-height: 40px; padding: 0 14px; background: transparent; border: 1px solid #30363d; border-radius: 8px; color: #8b949e; font-size: 13px; cursor: pointer; font-family: inherit; }\n .log-entries { flex: 1; overflow-y: auto; padding: 8px 12px; }\n .log-entries::-webkit-scrollbar { width: 3px; }\n .log-entries::-webkit-scrollbar-thumb { background: #30363d; border-radius: 2px; }\n .log-entry { font-size: 12px; color: #8b949e; padding: 6px 0; border-bottom: 1px solid #21262d; line-height: 1.5; display: flex; gap: 6px; font-variant-numeric: tabular-nums; }\n .log-entry .log-time { color: #484f58; min-width: 60px; }\n .log-entry .log-pace { font-weight: 600; }\n .log-entry .log-pace.fast { color: #3fb950; }\n .log-entry .log-pace.slow { color: #d29922; }\n .log-entry .log-pace.too-slow { color: #f85149; }\n .log-entry .log-dist { color: #58a6ff; }\n \n @media (max-width: 380px) {\n .pace-value { font-size: clamp(48px, 15vw, 80px); }\n .stat-card .stat-val { font-size: 15px; }\n .controls button { font-size: 12px; padding: 0 10px; }\n }\n</style>\n</head>\n<body>\n<div class=\"app\">\n <div class=\"header\">\n <h1><span class=\"status-dot idle\" id=\"statusDot\"></span> Run Pace</h1>\n <span class=\"badge\" id=\"freqBadge\">Ready</span>\n </div>\n\n <div class=\"pace-container\" id=\"paceContainer\">\n <div class=\"pace-label\">Pace</div>\n <div class=\"pace-value\" id=\"paceValue\">--:--</div>\n <div class=\"pace-unit\">min / km</div>\n <div class=\"pace-bar-wrapper\">\n <div class=\"pace-bar\" id=\"paceBar\" style=\"width: 0%\"></div>\n </div>\n <button class=\"log-toggle\" id=\"logToggleBtn\">πŸ“‹</button>\n <div class=\"log-panel\" id=\"logPanel\">\n <div class=\"log-header\">\n <h3>πŸ“‹ Lap Log</h3>\n <button id=\"logCloseBtn\">βœ• Close</button>\n </div>\n <div class=\"log-entries\" id=\"logEntries\"></div>\n </div>\n </div>\n\n <div class=\"sound-meter\">\n <span class=\"meter-label\">πŸ”Š Beep</span>\n <div class=\"meter-track\">\n <div class=\"meter-fill\" id=\"soundMeterFill\"></div>\n </div>\n <span class=\"meter-val\" id=\"soundMeterVal\">0%</span>\n </div>\n\n <div class=\"stats-row\">\n <div class=\"stat-card\"><div class=\"stat-val\" id=\"statDistance\">0.00</div><div class=\"stat-label\">km</div></div>\n <div class=\"stat-card\"><div class=\"stat-val\" id=\"statDuration\">00:00</div><div class=\"stat-label\">Time</div></div>\n <div class=\"stat-card\"><div class=\"stat-val\" id=\"statAvgPace\">--:--</div><div class=\"stat-label\">Avg Pace</div></div>\n </div>\n\n <div class=\"controls\">\n <button class=\"primary\" id=\"startBtn\">β–Ά Start Run</button>\n <button class=\"danger\" id=\"resetBtn\">β†Ί</button>\n </div>\n</div>\n\n<script>\n(function() {\n 'use strict';\n \n const PACE_THRESHOLD = 5.0;\n const MAX_PACE = 10.0;\n const STORAGE_KEY = 'msw_tab_run_log';\n \n // Smoothing: 15-second rolling window, plus exponential smoothing\n const WINDOW_SECONDS = 15;\n const WINDOW_MIN_DIST = 8; // meters\n const SMOOTHING_FACTOR = 0.25; // how much new raw value affects display (0.25 = responsive but smooth)\n \n let audioCtx = null;\n let oscillator = null;\n let gainNode = null;\n let isSoundPlaying = false;\n \n function initAudio() {\n if (audioCtx) return;\n try {\n audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n gainNode = audioCtx.createGain();\n gainNode.connect(audioCtx.destination);\n gainNode.gain.value = 0;\n } catch(e) { console.warn('Audio N/A', e); }\n }\n \n function startBeep(volume) {\n if (!audioCtx) initAudio();\n if (!audioCtx || isSoundPlaying) return;\n try {\n if (audioCtx.state === 'suspended') audioCtx.resume();\n oscillator = audioCtx.createOscillator();\n oscillator.type = 'sine';\n oscillator.frequency.value = 880;\n oscillator.connect(gainNode);\n oscillator.start();\n isSoundPlaying = true;\n setBeepVolume(volume);\n } catch(e) { console.warn('Audio err', e); }\n }\n \n function setBeepVolume(volume) {\n if (!gainNode) return;\n const clamped = Math.min(1, Math.max(0, volume));\n const curved = Math.pow(clamped, 1.5);\n gainNode.gain.setTargetAtTime(curved * 0.3, audioCtx.currentTime, 0.05);\n }\n \n function stopBeep() {\n if (!isSoundPlaying || !oscillator) return;\n try {\n if (gainNode) gainNode.gain.setTargetAtTime(0, audioCtx.currentTime, 0.05);\n setTimeout(() => {\n try { oscillator.stop(); oscillator.disconnect(); } catch(e) {}\n isSoundPlaying = false; oscillator = null;\n }, 100);\n } catch(e) { isSoundPlaying = false; oscillator = null; }\n }\n \n function updateSound(pace) {\n if (!pace || pace <= PACE_THRESHOLD || pace === Infinity) {\n if (isSoundPlaying) stopBeep();\n document.getElementById('soundMeterFill').style.width = '0%';\n document.getElementById('soundMeterVal').textContent = '0%';\n return;\n }\n const rawVol = Math.min(1, Math.max(0, (pace - PACE_THRESHOLD) / (MAX_PACE - PACE_THRESHOLD)));\n document.getElementById('soundMeterFill').style.width = (rawVol * 100) + '%';\n document.getElementById('soundMeterVal').textContent = Math.round(rawVol * 100) + '%';\n if (!isSoundPlaying) startBeep(rawVol);\n else setBeepVolume(rawVol);\n }\n \n let points = [];\n let isRunning = false;\n let watchId = null;\n let intervalId = null;\n let startTime = null;\n let currentPos = null;\n let lapLog = [];\n let lastLogDist = 0;\n let smoothedPace = null; // ← exponential moving average\n \n const paceValue = document.getElementById('paceValue');\n const paceContainer = document.getElementById('paceContainer');\n const paceBar = document.getElementById('paceBar');\n const statusDot = document.getElementById('statusDot');\n const freqBadge = document.getElementById('freqBadge');\n const statDistance = document.getElementById('statDistance');\n const statDuration = document.getElementById('statDuration');\n const statAvgPace = document.getElementById('statAvgPace');\n const startBtn = document.getElementById('startBtn');\n const resetBtn = document.getElementById('resetBtn');\n const logToggleBtn = document.getElementById('logToggleBtn');\n const logCloseBtn = document.getElementById('logCloseBtn');\n const logPanel = document.getElementById('logPanel');\n const logEntries = document.getElementById('logEntries');\n \n function toRad(deg) { return deg * Math.PI / 180; }\n function haversine(lat1, lng1, lat2, lng2) {\n const R = 6371000;\n const dLat = toRad(lat2 - lat1);\n const dLng = toRad(lng2 - lng1);\n const a = Math.sin(dLat/2)**2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng/2)**2;\n return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));\n }\n \n function totalDistance(pts) {\n let d = 0;\n for (let i = 1; i < pts.length; i++) d += haversine(pts[i-1].lat, pts[i-1].lng, pts[i].lat, pts[i].lng);\n return d;\n }\n \n function formatPace(p) {\n if (!p || p === Infinity || p <= 0) return '--:--';\n const min = Math.floor(p);\n const sec = Math.round((p - min) * 60);\n if (sec >= 60) return (min+1) + ':00';\n return min + ':' + String(sec).padStart(2, '0');\n }\n \n function formatDuration(ms) {\n if (!ms) return '00:00';\n const totalSec = Math.floor(ms / 1000);\n const m = Math.floor(totalSec / 60);\n const s = totalSec % 60;\n return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');\n }\n \n function formatKM(m) { return (m / 1000).toFixed(2); }\n \n // ── Stable rolling window pace ──\n // Looks back ~15 seconds to get a stable pace reading\n function calcRawPace() {\n if (points.length < 2) return null;\n \n const now = points[points.length - 1].time;\n const cutoff = now - WINDOW_SECONDS * 1000;\n \n // Find the earliest point within our window\n let startIdx = 0;\n for (let i = 0; i < points.length; i++) {\n if (points[i].time >= cutoff) {\n startIdx = i;\n break;\n }\n }\n \n // Make sure we have at least 2 points in the window\n if (points.length - startIdx < 2) startIdx = points.length - 2;\n \n const pStart = points[startIdx];\n const pEnd = points[points.length - 1];\n \n const dist = haversine(pStart.lat, pStart.lng, pEnd.lat, pEnd.lng);\n const time = pEnd.time - pStart.time;\n \n if (dist < WINDOW_MIN_DIST || time < 4000) return null;\n \n return (time / 60000) / (dist / 1000);\n }\n \n function calcAvgPace() {\n if (points.length < 2) return null;\n const dist = totalDistance(points);\n if (dist < 3) return null;\n const elapsed = points[points.length - 1].time - points[0].time;\n if (elapsed < 2000) return null;\n return (elapsed / 60000) / (dist / 1000);\n }\n \n function updateUI() {\n const rawPace = calcRawPace();\n const avgPace = calcAvgPace();\n const dist = points.length > 1 ? totalDistance(points) : 0;\n const duration = startTime ? (Date.now() - startTime) : 0;\n \n // Apply exponential smoothing\n let displayPace = null;\n if (rawPace && rawPace > 0 && rawPace !== Infinity && isFinite(rawPace)) {\n if (smoothedPace === null) {\n smoothedPace = rawPace;\n } else {\n // Blend: new raw value contributes SMOOTHING_FACTOR, old smoothed contributes rest\n smoothedPace = rawPace * SMOOTHING_FACTOR + smoothedPace * (1 - SMOOTHING_FACTOR);\n }\n displayPace = smoothedPace;\n } else {\n // If we lose GPS signal, keep the last known pace for a bit\n if (smoothedPace !== null) {\n // Slowly drift towards average if available\n if (avgPace && avgPace > 0) {\n smoothedPace = smoothedPace * 0.9 + avgPace * 0.1;\n }\n displayPace = smoothedPace;\n }\n }\n \n if (displayPace && displayPace > 0 && displayPace !== Infinity && isFinite(displayPace)) {\n paceValue.textContent = formatPace(displayPace);\n paceValue.className = 'pace-value';\n paceContainer.className = 'pace-container';\n if (displayPace <= PACE_THRESHOLD) {\n paceValue.classList.add('fast');\n paceBar.className = 'pace-bar good';\n } else if (displayPace <= MAX_PACE) {\n paceValue.classList.add('slow');\n paceContainer.classList.add('warning');\n paceBar.className = 'pace-bar warning';\n } else {\n paceValue.classList.add('too-slow');\n paceContainer.classList.add('danger');\n paceBar.className = 'pace-bar bad';\n }\n const barPct = Math.min(100, (displayPace / MAX_PACE) * 100);\n paceBar.style.width = barPct + '%';\n } else {\n paceValue.textContent = '--:--';\n paceValue.className = 'pace-value';\n paceContainer.className = 'pace-container';\n paceBar.style.width = '0%';\n paceBar.className = 'pace-bar';\n }\n \n statDistance.textContent = formatKM(dist);\n statDuration.textContent = formatDuration(duration);\n statAvgPace.textContent = avgPace ? formatPace(avgPace) : '--:--';\n \n updateSound(displayPace);\n \n if (isRunning) freqBadge.textContent = formatDuration(duration);\n }\n \n function recordPoint(position) {\n const pos = {\n lat: position.coords.latitude,\n lng: position.coords.longitude,\n acc: Math.round(position.coords.accuracy),\n time: Date.now()\n };\n currentPos = pos;\n points.push(pos);\n \n // Log lap every ~100m\n const dist = points.length > 1 ? totalDistance(points) : 0;\n if (dist - lastLogDist >= 100 || (lastLogDist === 0 && dist > 0)) {\n lastLogDist = dist;\n const pace = smoothedPace || calcRawPace() || calcAvgPace() || 0;\n lapLog.push({\n time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }),\n pace: pace,\n dist: dist,\n duration: Date.now() - startTime\n });\n updateLog();\n }\n \n updateUI();\n statusDot.className = 'status-dot running';\n saveState();\n }\n \n function recordError(err) {\n console.warn('GPS error:', err.message);\n statusDot.className = 'status-dot error';\n }\n \n function startRun() {\n if (isRunning) return;\n if (!navigator.geolocation) { alert('GPS not available.'); return; }\n \n initAudio();\n isRunning = true;\n startTime = Date.now();\n startBtn.textContent = '⏹ Stop Run';\n startBtn.classList.add('running');\n freqBadge.textContent = '00:00';\n lastLogDist = 0;\n smoothedPace = null;\n \n navigator.geolocation.getCurrentPosition(\n p => recordPoint(p),\n recordError,\n { enableHighAccuracy: true, timeout: 10000, maximumAge: 3000 }\n );\n \n watchId = navigator.geolocation.watchPosition(\n recordPoint,\n recordError,\n { enableHighAccuracy: true, timeout: 10000, maximumAge: 3000 }\n );\n \n intervalId = setInterval(() => { if (isRunning) updateUI(); }, 1000);\n statusDot.className = 'status-dot running';\n }\n \n function stopRun() {\n isRunning = false;\n startBtn.textContent = 'β–Ά Start Run';\n startBtn.classList.remove('running');\n if (watchId !== null) { navigator.geolocation.clearWatch(watchId); watchId = null; }\n if (intervalId !== null) { clearInterval(intervalId); intervalId = null; }\n stopBeep();\n statusDot.className = 'status-dot idle';\n saveState();\n }\n \n function toggleRun() { if (isRunning) stopRun(); else startRun(); }\n \n function resetRun() {\n if (isRunning) stopRun();\n points = []; currentPos = null; startTime = null; lapLog = []; lastLogDist = 0; smoothedPace = null;\n paceValue.textContent = '--:--'; paceValue.className = 'pace-value';\n paceContainer.className = 'pace-container';\n paceBar.style.width = '0%'; paceBar.className = 'pace-bar';\n document.getElementById('soundMeterFill').style.width = '0%';\n document.getElementById('soundMeterVal').textContent = '0%';\n stopBeep();\n statDistance.textContent = '0.00'; statDuration.textContent = '00:00'; statAvgPace.textContent = '--:--';\n statusDot.className = 'status-dot idle';\n freqBadge.textContent = 'Ready';\n updateLog();\n localStorage.removeItem(STORAGE_KEY);\n }\n \n function saveState() {\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify({ points, startTime, lapLog, lastLogDist }));\n } catch(e) {}\n }\n \n function loadState() {\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (raw) {\n const s = JSON.parse(raw);\n if (s.points && s.points.length > 0) {\n points = s.points; startTime = s.startTime; lapLog = s.lapLog || []; lastLogDist = s.lastLogDist || 0;\n return true;\n }\n }\n } catch(e) {}\n return false;\n }\n \n function updateLog() {\n logEntries.innerHTML = '';\n if (lapLog.length === 0) {\n logEntries.innerHTML = '<div style=\"text-align:center;color:#484f58;padding:20px;font-size:13px;\">No data yet β€” start running!</div>';\n return;\n }\n const reversed = [...lapLog].reverse().slice(0, 200);\n for (const lap of reversed) {\n const entry = document.createElement('div');\n entry.className = 'log-entry';\n const pc = lap.pace <= PACE_THRESHOLD ? 'fast' : (lap.pace <= MAX_PACE ? 'slow' : 'too-slow');\n entry.innerHTML = `<span class=\\\"log-time\\\">${lap.time}</span><span class=\\\"log-pace ${pc}\\\">${formatPace(lap.pace)}</span><span class=\\\"log-dist\\\">${formatKM(lap.dist)}km</span>`;\n logEntries.appendChild(entry);\n }\n }\n \n startBtn.addEventListener('click', toggleRun);\n resetBtn.addEventListener('click', resetRun);\n logToggleBtn.addEventListener('click', () => { logPanel.classList.toggle('open'); updateLog(); });\n logCloseBtn.addEventListener('click', () => logPanel.classList.remove('open'));\n \n const hasSaved = loadState();\n if (hasSaved) {\n currentPos = points[points.length - 1];\n updateUI(); updateLog();\n statusDot.className = 'status-dot idle';\n freqBadge.textContent = 'Paused';\n }\n \n console.log('πŸƒ Run tab ready β€” ' + points.length + ' saved points');\n})();\n</script>\n</body>\n</html>"
},
"currentHash": "8eda68f4a90ae6e37f037ea90f7a8c95a748d3e0384f666f82702fd2378892ae",
"previousHash": null,
"prompt": "Published shared tab",
"publishedAt": "2026-08-26T06:20:07.939Z",
"appId": "dfda7e57-b0ee-4ab3-aa45-5005bd0d4af3"
},
"appId": "dfda7e57-b0ee-4ab3-aa45-5005bd0d4af3"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment