restyling andalytics

This commit is contained in:
legop3
2026-06-19 03:00:42 -04:00
parent c7f5cb68f4
commit db8dc04b8c
8 changed files with 56 additions and 37 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -63,7 +63,7 @@
window.roverAnalytics = { window.roverAnalytics = {
track: function (name, data) { track: function (name, data) {
enqueue('track', [name, data || {}]); enqueue('track', typeof data === 'undefined' ? [name] : [name, data]);
}, },
identify: function (data) { identify: function (data) {
enqueue('identify', [data || {}]); enqueue('identify', [data || {}]);
@@ -78,8 +78,8 @@
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script> <script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script> <script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
<title>Roomba Rover</title> <title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-DZGFVNgK.js"></script> <script type="module" crossorigin src="/assets/index-Do2lWMxe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C6VExJdW.css"> <link rel="stylesheet" crossorigin href="/assets/index-C-OVNLHz.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+11 -1
View File
@@ -63,6 +63,7 @@ export function trackAnalyticsEvent(name, payload = {}) {
if (!eventName) return; if (!eventName) return;
const adapter = getAdapter(); const adapter = getAdapter();
if (!adapter || typeof adapter.track !== 'function') return; if (!adapter || typeof adapter.track !== 'function') return;
const cleanPayload = normalizePayload(payload);
try { try {
/* /*
@@ -70,7 +71,16 @@ export function trackAnalyticsEvent(name, payload = {}) {
broken or blocked third-party script from interfering with rover driving, broken or blocked third-party script from interfering with rover driving,
scanner input, chat, or any other live-control UI. scanner input, chat, or any other live-control UI.
*/ */
adapter.track(eventName, normalizePayload(payload)); /*
Umami accepts an event name without a data object. Avoid forwarding `{}` so
events that only need counting do not create noisy empty-property requests
in the dashboard or the network payload.
*/
if (Object.keys(cleanPayload).length) {
adapter.track(eventName, cleanPayload);
} else {
adapter.track(eventName);
}
} catch (error) { } catch (error) {
console.warn('Analytics event failed', error); console.warn('Analytics event failed', error);
} }
@@ -47,10 +47,11 @@ function getGameTheme(themeColor) {
selectedButtonStyle: { selectedButtonStyle: {
borderColor: rgba(rgb, 0.86), borderColor: rgba(rgb, 0.86),
borderLeftColor: rgba(rgb, 1), borderLeftColor: rgba(rgb, 1),
backgroundColor: rgba(rgb, 0.16), backgroundColor: rgba(rgb, 0.14),
}, },
titleBoxStyle: { titleBoxStyle: {
borderLeftColor: rgba(rgb, 0.88), borderLeftColor: rgba(rgb, 0.88),
backgroundColor: rgba(rgb, 0.06),
}, },
boxStyle: { boxStyle: {
borderColor: rgba(rgb, 0.62), borderColor: rgba(rgb, 0.62),
@@ -88,7 +89,7 @@ function GameChoice({ game, disabled, onVote }) {
type="button" type="button"
disabled={disabled || game.active} disabled={disabled || game.active}
onClick={() => onVote(game.id)} onClick={() => onVote(game.id)}
className="button-dark flex min-h-[4.25rem] flex-col items-start justify-between border-l-4 px-1.5 py-1 text-left disabled:opacity-70" className="button-dark flex min-h-[3.5rem] flex-col items-start justify-between border-l-4 border-neutral-600 bg-neutral-800 px-1 py-0.75 text-left hover:bg-neutral-700 disabled:opacity-70"
style={style} style={style}
> >
<span className="flex w-full items-start justify-between gap-1"> <span className="flex w-full items-start justify-between gap-1">
@@ -106,9 +107,9 @@ function GameChoice({ game, disabled, onVote }) {
function StatGrid({ stats, theme }) { function StatGrid({ stats, theme }) {
if (!Array.isArray(stats) || !stats.length) return null; if (!Array.isArray(stats) || !stats.length) return null;
return ( return (
<div className="grid gap-1 sm:grid-cols-3"> <div className="grid gap-0.5 sm:grid-cols-3">
{stats.slice(0, 6).map((stat) => ( {stats.slice(0, 6).map((stat) => (
<div key={stat.label} className="border border-neutral-700 px-1.5 py-1" style={theme.boxStyle}> <div key={stat.label} className="surface border border-neutral-700 px-1 py-0.75" style={theme.boxStyle}>
<p className="truncate text-[0.68rem] text-neutral-400">{stat.label}</p> <p className="truncate text-[0.68rem] text-neutral-400">{stat.label}</p>
<p className="truncate text-sm font-semibold text-neutral-100">{stat.value}</p> <p className="truncate text-sm font-semibold text-neutral-100">{stat.value}</p>
</div> </div>
@@ -127,9 +128,9 @@ function ParticipantsBlock({ participants, theme }) {
.filter(Boolean); .filter(Boolean);
return ( return (
<div className="min-w-0 border border-neutral-700 px-2 py-1.5" style={theme.boxStyle}> <div className="surface min-w-0 border border-neutral-700 px-1 py-0.75" style={theme.boxStyle}>
<p className="text-sm font-semibold text-neutral-400">Participants</p> <p className="text-sm font-semibold text-neutral-400">Participants</p>
<p className="font-mono text-2xl font-semibold leading-tight text-neutral-50"> <p className="font-mono text-xl font-semibold leading-tight text-neutral-50">
{knownParticipants.length} {knownParticipants.length}
</p> </p>
<p className="truncate text-sm leading-tight text-neutral-300"> <p className="truncate text-sm leading-tight text-neutral-300">
@@ -160,15 +161,15 @@ function Leaderboard({ players, ownPlayer }) {
const hasOwnPoints = ownPlayer && Number.isFinite(ownPlayer.totalPoints); const hasOwnPoints = ownPlayer && Number.isFinite(ownPlayer.totalPoints);
return ( return (
<div className="space-y-1"> <div className="space-y-1">
<div className="grid gap-1 sm:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]"> <div className="grid gap-0.5 sm:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
<div className="border border-neutral-700 px-1.5 py-1"> <div className="surface border border-neutral-700 px-1 py-0.75">
<p className="text-xs text-neutral-400">Your points</p> <p className="text-xs text-neutral-400">Your points</p>
<p className="text-lg font-semibold leading-tight text-neutral-50"> <p className="text-lg font-semibold leading-tight text-neutral-50">
{hasOwnPoints ? ownPlayer.totalPoints : 0} {hasOwnPoints ? ownPlayer.totalPoints : 0}
</p> </p>
{ownPlayer?.rank ? <p className="text-[0.7rem] text-neutral-400">Rank {ownPlayer.rank}</p> : null} {ownPlayer?.rank ? <p className="text-[0.7rem] text-neutral-400">Rank {ownPlayer.rank}</p> : null}
</div> </div>
<div className="border border-neutral-700 px-1.5 py-1"> <div className="surface border border-neutral-700 px-1 py-0.75">
<p className="mb-0.5 text-xs font-semibold text-neutral-300">Leaderboard</p> <p className="mb-0.5 text-xs font-semibold text-neutral-300">Leaderboard</p>
{Array.isArray(players) && players.length ? ( {Array.isArray(players) && players.length ? (
<div className="space-y-0.5"> <div className="space-y-0.5">
@@ -284,8 +285,8 @@ export default function BarcodeGamesPanel() {
}; };
return ( return (
<CardFrame title="Barcode games" bodyClassName="space-y-2 p-1.5 text-sm"> <CardFrame title="Barcode games" bodyClassName="space-y-1 p-0.5 text-sm">
<div className="grid gap-1 sm:grid-cols-2"> <div className="grid gap-0.5 sm:grid-cols-2">
{games.map((game) => ( {games.map((game) => (
<GameChoice <GameChoice
key={game.id} key={game.id}
@@ -296,25 +297,25 @@ export default function BarcodeGamesPanel() {
))} ))}
</div> </div>
<section className="space-y-2 border-t border-neutral-700 pt-2"> <section className="space-y-1 border-t border-neutral-700 pt-1">
<div className="grid items-start gap-2 lg:grid-cols-[minmax(0,1fr)_15rem_15rem]"> <div className="grid items-stretch gap-0.5 lg:grid-cols-[minmax(0,1fr)_9rem_10rem]">
<div className="min-w-0 border-l-4 border-neutral-700 pl-2" style={activeTheme.titleBoxStyle}> <div className="surface min-w-0 border-l-4 border-neutral-700 px-1 py-0.75" style={activeTheme.titleBoxStyle}>
<p className="break-words text-2xl font-bold leading-tight text-neutral-50" style={activeTheme.textStyle}> <p className="break-words text-xl font-bold leading-tight text-neutral-50" style={activeTheme.textStyle}>
{display.title || activeGame?.title || 'No game active'} {display.title || activeGame?.title || 'No game active'}
</p> </p>
<p className="mt-0.5 break-words text-lg font-semibold leading-tight text-neutral-200"> <p className="mt-0.5 break-words text-sm font-semibold leading-tight text-neutral-200">
{display.primary || 'Vote for a game to start'} {display.primary || 'Vote for a game to start'}
</p> </p>
{display.secondary ? ( {display.secondary ? (
<p className="mt-1 break-words text-base leading-snug text-neutral-300">{display.secondary}</p> <p className="mt-0.5 break-words text-xs leading-snug text-neutral-300">{display.secondary}</p>
) : null} ) : null}
</div> </div>
<div className="border border-neutral-700 px-2 py-1.5 text-left lg:text-right" style={activeTheme.boxStyle}> <div className="surface border border-neutral-700 px-1 py-0.75 text-left lg:text-right" style={activeTheme.boxStyle}>
<p className="text-sm font-semibold text-neutral-400">{display.timer?.label || 'Time'}</p> <p className="text-xs font-semibold text-neutral-400">{display.timer?.label || 'Time'}</p>
<p className="font-mono text-2xl font-semibold leading-tight text-neutral-50"> <p className="font-mono text-xl font-semibold leading-tight text-neutral-50">
{timerText || '--'} {timerText || '--'}
</p> </p>
<p className="text-sm leading-tight text-neutral-500"> <p className="text-xs leading-tight text-neutral-500">
{timerText ? 'active timer' : 'no timer'} {timerText ? 'active timer' : 'no timer'}
</p> </p>
</div> </div>
@@ -325,10 +326,14 @@ export default function BarcodeGamesPanel() {
<Leaderboard players={state.leaderboard} ownPlayer={state.ownPlayer} /> <Leaderboard players={state.leaderboard} ownPlayer={state.ownPlayer} />
<div className="grid gap-2 border-t border-neutral-700 pt-1.5 sm:grid-cols-2"> <div className="grid gap-0.5 border-t border-neutral-700 pt-1 sm:grid-cols-2">
<div className="surface px-1 py-0.75">
<CounterList title="Most scanned objects" entries={state.counters?.objects} /> <CounterList title="Most scanned objects" entries={state.counters?.objects} />
</div>
<div className="surface px-1 py-0.75">
<CounterList title="Most scanned rovers" entries={state.counters?.rovers} /> <CounterList title="Most scanned rovers" entries={state.counters?.rovers} />
</div> </div>
</div>
</CardFrame> </CardFrame>
); );
} }
@@ -42,7 +42,7 @@ export default function SocketConnectionPill() {
setLastReason(''); setLastReason('');
show(); show();
scheduleFade(); scheduleFade();
trackAnalyticsEvent('socket_connect'); trackAnalyticsEvent('socket_connect', { connected: true });
}; };
const onDisconnect = (reason) => { const onDisconnect = (reason) => {
@@ -70,7 +70,11 @@ export default function SocketConnectionPill() {
const onReconnectAttempt = () => { const onReconnectAttempt = () => {
setConnected(false); setConnected(false);
show(); show();
trackAnalyticsEventThrottled('socket_reconnect_attempt', {}, { key: 'socket_reconnect_attempt', throttleMs: 30 * 1000 }); trackAnalyticsEventThrottled(
'socket_reconnect_attempt',
{ connected: false },
{ key: 'socket_reconnect_attempt', throttleMs: 30 * 1000 },
);
}; };
socket.on('connect', onConnect); socket.on('connect', onConnect);
+1 -1
View File
@@ -40,7 +40,7 @@
window.roverAnalytics = { window.roverAnalytics = {
track: function (name, data) { track: function (name, data) {
enqueue('track', [name, data || {}]); enqueue('track', typeof data === 'undefined' ? [name] : [name, data]);
}, },
identify: function (data) { identify: function (data) {
enqueue('identify', [data || {}]); enqueue('identify', [data || {}]);