Created
August 26, 2026 06:20
-
-
Save monperrus/5a4e746f8fbce259d9d66a9decc85647 to your computer and use it in GitHub Desktop.
Martin's Super Webapp tab: Run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "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