diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3df50bec4..09f5da1a6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,7 +33,7 @@ jobs: VITE_APP_GIT_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} run: | export VITE_APP_GIT_TIMESTAMP="$(git show -s --format=%cI)" - args=(--output ./dist --head-sha "$VITE_APP_GIT_SHA") + args=(--output ./dist --artifact-output "$RUNNER_TEMP/connect-gallery.html" --head-sha "$VITE_APP_GIT_SHA") if [[ -n "$BASELINE_URL" ]]; then args+=(--baseline-url "$BASELINE_URL") fi @@ -45,3 +45,15 @@ jobs: path: ./dist retention-days: 1 name: build-artifacts-${{ github.run_id }} + + - name: Upload directly viewable gallery report + id: gallery-artifact + uses: actions/upload-artifact@v7 + with: + path: ${{ runner.temp }}/connect-gallery.html + archive: false + if-no-files-found: error + retention-days: 14 + + - name: Add gallery link to build summary + run: echo "### [Open connect gallery](${{ steps.gallery-artifact.outputs.artifact-url }})" >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/build-gallery.mjs b/scripts/build-gallery.mjs index 80a2b5977..064f3545d 100644 --- a/scripts/build-gallery.mjs +++ b/scripts/build-gallery.mjs @@ -156,12 +156,20 @@ async function downloadBaseline(baselineUrl, destination) { item.state === state.name && item.viewport === viewport.name )); if (!capture?.assets?.current) return false; - const response = await getResponse(new URL(capture.assets.current, root)); - await writeFile( - resolve(destination, captureFilename(state.name, viewport.name)), - Buffer.from(await response.arrayBuffer()), - ); - return true; + const captureUrl = new URL(capture.assets.current, root); + try { + const response = await getResponse(captureUrl); + const buffer = Buffer.from(await response.arrayBuffer()); + readPng(buffer, `Baseline capture ${state.name}/${viewport.name} from ${captureUrl}`); + await writeFile( + resolve(destination, captureFilename(state.name, viewport.name)), + buffer, + ); + return true; + } catch (error) { + console.warn(`Skipping unavailable baseline capture ${state.name}/${viewport.name}: ${error.message}`); + return false; + } })); const downloaded = (await Promise.all(downloads)).filter(Boolean).length; if (downloaded === 0) throw new Error(`Gallery manifest at ${manifestUrl} has no compatible captures`); @@ -432,8 +440,16 @@ async function serveDirectory(directory) { }; } +function readPng(buffer, label) { + try { + return PNG.sync.read(buffer); + } catch (error) { + throw new Error(`${label} is not a valid PNG: ${error.message}`, { cause: error }); + } +} + function assertNotBlank(buffer, name) { - const png = PNG.sync.read(buffer); + const png = readPng(buffer, name); const first = [png.data[0], png.data[1], png.data[2], png.data[3]]; for (let offset = 4; offset < png.data.length; offset += 4) { if ( @@ -729,8 +745,8 @@ async function captureRenderers(renderers, output, fixtures) { async function compareImages(basePath, currentPath, diffPath) { const [baseBuffer, currentBuffer] = await Promise.all([readFile(basePath), readFile(currentPath)]); - const baseline = PNG.sync.read(baseBuffer); - const current = PNG.sync.read(currentBuffer); + const baseline = readPng(baseBuffer, `Baseline image ${basePath}`); + const current = readPng(currentBuffer, `Current image ${currentPath}`); if (baseline.width !== current.width || baseline.height !== current.height) { throw new Error(`Image dimensions differ: ${baseline.width}x${baseline.height} vs ${current.width}x${current.height}`); } @@ -764,7 +780,7 @@ function imageMarkup(path, alt, viewport) { return `${alt}`; } -function renderReport(manifest) { +function renderReport(manifest, { showPreviewLink = true } = {}) { const hasBaseline = Boolean(manifest.baseSha); const count = (status) => manifest.captures.filter((capture) => capture.status === status).length; const renderRows = (viewportName) => manifest.captures.filter((capture) => capture.viewport === viewportName).map((capture) => { @@ -792,7 +808,7 @@ function renderReport(manifest) { const tables = GALLERY_VIEWPORTS.map((viewport) => `

${viewport.name[0].toUpperCase()}${viewport.name.slice(1)} (${viewport.width} × ${viewport.height})

- +
${hasBaseline ? '' : ''} @@ -814,13 +830,16 @@ function renderReport(manifest) { connect ${hasBaseline ? 'visual regression report' : 'gallery'}
-

Open interactive preview

+ ${showPreviewLink ? '

Open interactive preview

' : ''}

${hasBaseline ? 'Visual regression report' : 'Gallery'}

${hasBaseline ? `Base: ${manifest.baseSha}
` : ''}Head: ${manifest.headSha}
Generated:

@@ -843,7 +862,19 @@ function renderReport(manifest) { \n`; } -async function buildReport(captures, output, headSha, baseSha, baselineUrl) { +async function renderSelfContainedReport(manifest, output) { + const inlineManifest = JSON.parse(JSON.stringify(manifest)); + await Promise.all(inlineManifest.captures.flatMap((capture) => ( + Object.entries(capture.assets).map(async ([name, asset]) => { + if (!asset) return; + const contents = await readFile(resolve(output, asset.replace(/^\/+/, ''))); + capture.assets[name] = `data:image/png;base64,${contents.toString('base64')}`; + }) + ))); + return renderReport(inlineManifest, { showPreviewLink: false }); +} + +async function buildReport(captures, output, headSha, baseSha, baselineUrl, artifactOutput) { const currentDirectory = resolve(captures, 'current'); const baseDirectory = resolve(captures, 'base'); const hasBaseline = Boolean(baseSha); @@ -863,7 +894,7 @@ async function buildReport(captures, output, headSha, baseSha, baselineUrl) { for (const viewport of GALLERY_VIEWPORTS) { const filename = captureFilename(state.name, viewport.name); const currentSource = resolve(currentDirectory, filename); - const currentAsset = `/connect-gallery-assets/current/${filename}`; + const currentAsset = `./connect-gallery-assets/current/${filename}`; let status = 'unavailable'; let changedPixels = null; let changedRatio = null; @@ -871,7 +902,7 @@ async function buildReport(captures, output, headSha, baseSha, baselineUrl) { let diffAsset = null; const baselineSource = resolve(baseDirectory, filename); if (hasBaseline && await fileExists(baselineSource)) { - baselineAsset = `/connect-gallery-assets/baseline/${filename}`; + baselineAsset = `./connect-gallery-assets/baseline/${filename}`; let hasDiff; ({ changedPixels, changedRatio, hasDiff } = await compareImages( baselineSource, @@ -879,7 +910,7 @@ async function buildReport(captures, output, headSha, baseSha, baselineUrl) { resolve(assetsDirectory, 'diff', filename), )); status = changedRatio > CHANGE_THRESHOLD ? 'changed' : 'unchanged'; - if (hasDiff) diffAsset = `/connect-gallery-assets/diff/${filename}`; + if (hasDiff) diffAsset = `./connect-gallery-assets/diff/${filename}`; } results.push({ state: state.name, @@ -904,11 +935,19 @@ async function buildReport(captures, output, headSha, baseSha, baselineUrl) { captures: results, }; await mkdir(output, { recursive: true }); - await Promise.all([ + const writes = [ writeFile(resolve(assetsDirectory, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`), writeFile(resolve(output, 'connect-gallery.html'), renderReport(manifest)), - ]); + ]; + if (artifactOutput) { + await mkdir(dirname(artifactOutput), { recursive: true }); + writes.push(renderSelfContainedReport(manifest, output).then((report) => ( + writeFile(artifactOutput, report) + ))); + } + await Promise.all(writes); console.log(`Gallery report written to ${resolve(output, 'connect-gallery.html')}`); + if (artifactOutput) console.log(`Self-contained gallery artifact written to ${artifactOutput}`); } async function main() { @@ -948,6 +987,7 @@ async function main() { args['head-sha'] ?? process.env.GITHUB_SHA ?? await gitSha(source), baseSha, baselineUrl, + args['artifact-output'] ? resolve(args['artifact-output']) : null, ); } finally { await rm(temporary, { recursive: true, force: true }); diff --git a/src/App.jsx b/src/App.jsx index 6a0fa3c78..02fd93d74 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,8 +6,6 @@ import qs from 'query-string'; import localforage from 'localforage'; import * as Sentry from '@sentry/react'; -import { CircularProgress, Grid } from '@material-ui/core'; - import MyCommaAuth, { config as AuthConfig, storage as AuthStorage } from '@commaai/my-comma-auth'; import { athena as Athena, auth as Auth, billing as Billing, request as Request } from './api'; @@ -121,11 +119,9 @@ class App extends Component { renderLoading() { return ( - - - - - +
+
+
); } diff --git a/src/colors.js b/src/colors.js index beb491844..668791006 100644 --- a/src/colors.js +++ b/src/colors.js @@ -1,20 +1,9 @@ const Colors = { - transparent: 'transparent', white: '#fff', - darken10: 'rgba(0, 0, 0, 0.1)', - darken20: 'rgba(0, 0, 0, 0.2)', - darken30: 'rgba(0, 0, 0, 0.3)', - darken40: 'rgba(0, 0, 0, 0.4)', - darken50: 'rgba(0, 0, 0, 0.5)', - darken60: 'rgba(0, 0, 0, 0.6)', - darken70: 'rgba(0, 0, 0, 0.7)', - darken80: 'rgba(0, 0, 0, 0.8)', - darken90: 'rgba(0, 0, 0, 0.9)', white03: 'rgba(255, 255, 255, 0.03)', white05: 'rgba(255, 255, 255, 0.05)', white08: 'rgba(255, 255, 255, 0.08)', white10: 'rgba(255, 255, 255, 0.1)', - white12: 'rgba(255, 255, 255, 0.12)', white20: 'rgba(255, 255, 255, 0.2)', white30: 'rgba(255, 255, 255, 0.3)', white40: 'rgba(255, 255, 255, 0.4)', @@ -23,112 +12,20 @@ const Colors = { white70: 'rgba(255, 255, 255, 0.7)', white80: 'rgba(255, 255, 255, 0.8)', white90: 'rgba(255, 255, 255, 0.9)', - black: '#030404', - blue50: '#258FDA', blue100: '#2284c9', - blue200: '#1f79b8', - blue300: '#1c6ea8', - blue400: '#1a6397', - blue500: '#175886', - blue600: '#144d75', - blue700: '#114265', - blue800: '#0e3754', - blue900: '#0b2c43', - blue950: '#061622', - blue999: '#030b11', - lightBlue50: '#eef6fc', - lightBlue100: '#ddeef9', - lightBlue200: '#cde5f6', - lightBlue300: '#bcddf4', - lightBlue400: '#abd4f1', - lightBlue500: '#9acbee', - lightBlue600: '#8ac3eb', lightBlue700: '#79bae8', - lightBlue800: '#68b1e5', lightBlue900: '#57a9e3', - desatBlue800: '#657f92', primeBlue50: '#5e8bff', - primeBlue100: '#5984F2', primeBlue200: '#547de6', - primeBlue300: '#4b6fcc', - primeBlue400: '#4261b3', - primeBlue500: '#385399', - primeBlue600: '#2f4680', - primeBlue700: '#263866', - primeBlue800: '#1c2a4d', - primeBlue900: '#131c33', - red50: '#da2535', - red100: '#c92231', - red200: '#b81f2d', red300: '#971a25', red400: '#861721', red500: '#75141d', - red600: '#651118', - red700: '#540e14', - red800: '#430b10', - red900: '#32090c', - lightRed50: '#fceeef', - lightRed100: '#f9dde0', - lightRed200: '#f6cdd0', - lightRed300: '#f4bcc1', - lightRed400: '#f1abb1', - lightRed500: '#ee9aa2', - lightRed600: '#eb8a92', - lightRed700: '#e87983', - lightRed800: '#e56873', - lightRed900: '#e35764', - lightRed950: '#e04754', - lightRed999: '#dd3645', - green50: '#22c967', green100: '#20b85f', green200: '#1da756', green300: '#1a974e', green400: '#178645', green500: '#14753c', - green600: '#116534', - green700: '#0e542b', - green800: '#0c4323', - green900: '#09321a', - lightGreen50: '#eefcf4', - lightGreen100: '#def9e9', - lightGreen200: '#cdf6de', - lightGreen300: '#bcf4d3', - lightGreen400: '#abf1c8', - lightGreen500: '#9beebd', - lightGreen600: '#8aebb2', - lightGreen700: '#79e8a7', - lightGreen800: '#68e59c', - lightGreen900: '#25da70', - orange50: '#da6f25', - orange100: '#c96722', orange200: '#b85e1f', - orange300: '#a7551c', - orange400: '#964d19', - orange500: '#864417', - orange600: '#753c14', - orange700: '#643311', - orange800: '#532b0e', - orange900: '#42220b', - lightOrange50: '#f9e8dd', - lightOrange100: '#f6ddcc', - lightOrange200: '#f4d2bb', - lightOrange300: '#f1c7aa', - lightOrange400: '#eebc9a', - lightOrange500: '#ebb189', - lightOrange600: '#e8a678', - lightOrange700: '#e59b67', - lightOrange800: '#e08546', - lightOrange900: '#dd7a35', - yellow50: '#f1ebaa', - yellow100: '#eee79a', - yellow200: '#ebe389', - yellow300: '#e8df78', - yellow400: '#e5db67', - yellow500: '#e3d756', - yellow600: '#e0d346', - yellow700: '#ddcf35', - yellow800: '#daca25', - yellow900: '#c9bb22', grey50: '#6e7d84', grey100: '#65737a', grey200: '#5c696f', @@ -140,19 +37,7 @@ const Colors = { grey800: '#272c2f', grey900: '#1e2224', grey950: '#151819', - grey999: '#0c0e0f', - lightGrey50: '#f8f9f9', - lightGrey100: '#eeeff0', - lightGrey200: '#e3e6e8', - lightGrey300: '#d8dcdf', - lightGrey400: '#cdd3d6', - lightGrey500: '#c3c9cd', lightGrey600: '#b8c0c4', - lightGrey700: '#adb6bb', - lightGrey800: '#a3adb2', - lightGrey900: '#98a3a9', - lightGrey950: '#8d9aa1', - lightGrey999: '#77878f', }; export default Colors; diff --git a/src/components/AppDrawer/index.jsx b/src/components/AppDrawer/index.jsx index fe1f639b4..9ba52da81 100644 --- a/src/components/AppDrawer/index.jsx +++ b/src/components/AppDrawer/index.jsx @@ -38,9 +38,9 @@ const AppDrawer = ({ open={isPermanent || drawerIsOpen} onClose={toggleDrawerOff} variant={isPermanent ? 'permanent' : 'temporary'} - PaperProps={{ style: { width, top: 'auto' } }} + PaperProps={{ style: { width, top: 'auto', borderRight: 'none' } }} > -
+
{!isPermanent && ( diff --git a/src/components/AppHeader/AccountMenu.jsx b/src/components/AppHeader/AccountMenu.jsx index af0d41746..3cde1f494 100644 --- a/src/components/AppHeader/AccountMenu.jsx +++ b/src/components/AppHeader/AccountMenu.jsx @@ -20,7 +20,7 @@ const Version = () => { if (sha) { const commitUrl = `https://github.com/commaai/connect/commit/${sha}`; - content.push({sha.substring(0, 7)}); + content.push({sha.substring(0, 7)}); if (timestamp) { const buildDate = dayjs(timestamp).fromNow(); @@ -30,7 +30,7 @@ const Version = () => { content.push('dev'); } - return {content} + return {content} }; const AccountMenu = ({ profile, open, onClose }) => { @@ -48,15 +48,15 @@ const AccountMenu = ({ profile, open, onClose }) => { return ( <>
-
+
- {profile.email} - {profile.user_id} + {profile.email} + {profile.user_id} {version}
-
+
{ Manage Account ))}
- Camera + Camera
)}
@@ -99,7 +99,7 @@ const ControlsBar = ({ >
- Snapshot + Snapshot
); diff --git a/src/components/BodyTeleop/Joystick.jsx b/src/components/BodyTeleop/Joystick.jsx index bcea115e8..7dd92b488 100644 --- a/src/components/BodyTeleop/Joystick.jsx +++ b/src/components/BodyTeleop/Joystick.jsx @@ -3,28 +3,31 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { getOrientationSource } from '../../hooks/window'; const TriggerGroup = ({ bumperActive, bumperLabel, bumperKey, cameraActive, triggerValue, triggerColor, triggerKey, directionLabel }) => { - const activeStyle = cameraActive ? { background: 'rgba(59,130,246,0.35)', borderColor: 'rgba(59,130,246,0.5)' } : undefined; + const activeStyle = cameraActive ? { + background: 'color-mix(in srgb, var(--color-action-hover) 35%, transparent)', + borderColor: 'color-mix(in srgb, var(--color-action-hover) 50%, transparent)', + } : undefined; return (
- + {bumperLabel}
- + {bumperKey}
-
+
- + {triggerKey}
- + {directionLabel}
@@ -39,17 +42,17 @@ const ControllerOverlay = ({ gamepadSteering, gamepadGas, gamepadBrake, gamepadL
- L Stick — Steering -
-
- {'\u25C0'} - {'\u25B6'} + L Stick — Steering +
+
+ {'\u25C0'} + {'\u25B6'}
@@ -58,7 +61,7 @@ const ControllerOverlay = ({ gamepadSteering, gamepadGas, gamepadBrake, gamepadL
); @@ -72,7 +75,7 @@ const TouchJoystick = ({ className, thumbPos, joystickAreaRef, onTouchStart, onT return (
e.preventDefault()} > -
-
-
+
+
+
diff --git a/src/components/BodyTeleop/SettingsMenu.jsx b/src/components/BodyTeleop/SettingsMenu.jsx index bed124b57..9421ec8fd 100644 --- a/src/components/BodyTeleop/SettingsMenu.jsx +++ b/src/components/BodyTeleop/SettingsMenu.jsx @@ -14,7 +14,7 @@ const QUALITY_OPTIONS = [ { key: 'low', label: 'low', bitrate: '500 kbps' }, ]; -const rowClass = 'flex items-center h-9 px-3.5 gap-3 cursor-pointer select-none text-[13px] text-white/85 hover:bg-white/10 transition-colors whitespace-nowrap'; +const rowClass = 'flex items-center h-9 px-3.5 gap-3 cursor-pointer select-none text-[13px] text-content/85 hover:bg-content/10 transition-colors whitespace-nowrap'; const pageClass = 'absolute top-0 left-0 w-max min-w-[200px] py-1.5 transition-all duration-200 ease-out'; const SettingsMenu = ({ onQualityChange, options = QUALITY_OPTIONS }) => { @@ -58,7 +58,7 @@ const SettingsMenu = ({ onQualityChange, options = QUALITY_OPTIONS }) => { return (
@@ -89,7 +89,7 @@ const SettingsMenu = ({ onQualityChange, options = QUALITY_OPTIONS }) => { >
setView('quality')}> Quality - + {selected?.label} @@ -106,18 +106,18 @@ const SettingsMenu = ({ onQualityChange, options = QUALITY_OPTIONS }) => { pointerEvents: open && view === 'quality' ? 'auto' : 'none', }} > -
setView('main')}> - +
setView('main')}> + Quality (Bitrate)
-
+
{options.map((opt) => (
selectQuality(opt.key)}> - {opt.key === quality && } + {opt.key === quality && } {opt.label} - {opt.bitrate && {opt.bitrate}} + {opt.bitrate && {opt.bitrate}}
))}
diff --git a/src/components/BodyTeleop/StatusBar.jsx b/src/components/BodyTeleop/StatusBar.jsx index 5288643a9..c4287748f 100644 --- a/src/components/BodyTeleop/StatusBar.jsx +++ b/src/components/BodyTeleop/StatusBar.jsx @@ -13,8 +13,8 @@ const PACKET_LOSS_POOR = 0.02; const RTT_POOR_MS = 250; const QUALITY_INDICATOR = { - good: { color: '#22c967', label: 'connected' }, - poor: { color: '#f5c542', label: 'poor connection' }, + good: { color: 'var(--color-success)', label: 'connected' }, + poor: { color: 'var(--color-warning)', label: 'poor connection' }, }; const STATS_ROWS = [ @@ -273,28 +273,28 @@ export const StatsPanel = ({ isLandscape, stats, latency, latencyHistory }) => { if (showInCompact == false && compact) return; return (
- {label} - {stats?.[key] ?? '--'} + {label} + {stats?.[key] ?? '--'}
) })}
- {!compact &&
} + {!compact &&
}
- {!compact &&
{"FRAME LATENCY"}
} + {!compact &&
{"FRAME LATENCY"}
} {LATENCY_LAYERS.map(({ label, key, labelColor }) => (
{label} - {fmtMs(latency?.[key])} + {fmtMs(latency?.[key])}
))}
- Frame Latency - {fmtMs(latency?.totalMs)} + Frame Latency + {fmtMs(latency?.totalMs)}
- - {!compact &&
} + + {!compact &&
}
); }; @@ -309,11 +309,11 @@ const StatsMenu = ({ return (
- + stats
@@ -341,12 +341,12 @@ const StatusBar = ({ style={{ backgroundColor: indicator.color }} title={indicator.label} /> - {stats?.rtt ?? '--'} + {stats?.rtt ?? '--'}
{battery && (
- - {battery.level}% + + {battery.level}%
)} {
{connecting ? ( <> - - Connecting... + + Connecting... ) : canRetry ? ( ) : null} {error && ( -
+
{error}
)} @@ -70,7 +70,7 @@ const Video = ({ }, [connectionTimeLabel]); return ( -
+
PageBaselinePRDiffResult
PageScreenshot