1- import { createResource , createSignal , For , Show , Suspense , type VoidComponent } from 'solid-js'
1+ import { createEffect , createResource , createSignal , For , Show , Suspense , type VoidComponent } from 'solid-js'
22import { createStore } from 'solid-js/store'
33import clsx from 'clsx'
4-
54import { takeSnapshot } from '~/api/athena'
6- import { getDevice , SHARED_DEVICE } from '~/api/devices'
5+ import { getAthenaOfflineQueue , getDevice , SHARED_DEVICE } from '~/api/devices'
6+ import { getUploadQueue } from '~/api/file'
77import { DrawerToggleButton , useDrawerContext } from '~/components/material/Drawer'
88import Icon from '~/components/material/Icon'
99import IconButton from '~/components/material/IconButton'
1010import TopAppBar from '~/components/material/TopAppBar'
1111import DeviceLocation from '~/components/DeviceLocation'
1212import DeviceStatistics from '~/components/DeviceStatistics'
13- import UploadQueue from '~/components/UploadQueue'
13+ import UploadQueue , { mapOfflineQueueItems } from '~/components/UploadQueue'
1414import { dayjs } from '~/utils/format'
1515import { getDeviceName } from '~/utils/device'
16-
1716import RouteList from '../components/RouteList'
18-
1917type DeviceActivityProps = {
2018 dongleId : string
2119}
22-
2320const DeviceActivity : VoidComponent < DeviceActivityProps > = ( props ) => {
24- // TODO: device should be passed in from DeviceList
21+ const getOnlineUploadSummary = async ( dongleId : string ) => {
22+ try {
23+ return await getUploadQueue ( dongleId )
24+ } catch {
25+ return { result : [ ] }
26+ }
27+ }
28+
29+ const getOfflineUploadSummary = async ( dongleId : string ) => {
30+ try {
31+ return await getAthenaOfflineQueue ( dongleId )
32+ } catch {
33+ return [ ]
34+ }
35+ }
2536 const [ device ] = createResource ( ( ) => props . dongleId , getDevice )
26- // Resource as source of another resource blocks component initialization
2737 const deviceName = ( ) => ( device . latest ? getDeviceName ( device . latest ) : '' )
28- // TODO: remove this. if we're listing the routes for a device you should always be a user, this is for viewing public routes which are being removed
2938 const isDeviceUser = ( ) => ( device . loading ? true : device . latest ?. is_owner || device . latest ?. alias !== SHARED_DEVICE )
3039 const [ queueVisible , setQueueVisible ] = createSignal ( false )
3140 const [ snapshot , setSnapshot ] = createStore < {
@@ -37,7 +46,6 @@ const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
3746 fetching : false ,
3847 images : [ ] ,
3948 } )
40-
4149 const onClickSnapshot = async ( ) => {
4250 setSnapshot ( { error : null , fetching : true } )
4351 try {
@@ -58,7 +66,6 @@ const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
5866 setSnapshot ( 'fetching' , false )
5967 }
6068 }
61-
6269 const downloadSnapshot = ( image : string , index : number ) => {
6370 const link = document . createElement ( 'a' )
6471 link . href = `data:image/jpeg;base64,${ image } `
@@ -67,23 +74,35 @@ const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
6774 link . click ( )
6875 document . body . removeChild ( link )
6976 }
70-
71- const clearImage = ( index : number ) => {
72- const newImages = snapshot . images . filter ( ( _ , i ) => i !== index )
73- setSnapshot ( 'images' , newImages )
74- }
75-
77+ const clearImage = ( index : number ) =>
78+ setSnapshot (
79+ 'images' ,
80+ snapshot . images . filter ( ( _ , i ) => i !== index ) ,
81+ )
7682 const clearError = ( ) => setSnapshot ( 'error' , null )
77-
7883 const { modal } = useDrawerContext ( )
84+ const [ onlineQueue ] = createResource ( ( ) => props . dongleId , getOnlineUploadSummary )
85+ const [ offlineQueue ] = createResource ( ( ) => props . dongleId , getOfflineUploadSummary )
7986 const onlineStatus = ( ) => ( device . latest ?. is_online ? 'Online now' : 'Offline' )
80- const lastSeen = ( ) => {
81- const lastPing = device . latest ?. last_athena_ping
82- if ( ! lastPing ) return 'Last seen unavailable'
83- return `Last seen ${ dayjs . unix ( lastPing ) . format ( 'MMM D, h:mm A' ) } `
84- }
87+ const lastSeen = ( ) =>
88+ device . latest ?. last_athena_ping
89+ ? `Last seen ${ dayjs . unix ( device . latest . last_athena_ping ) . format ( 'MMM D, h:mm A' ) } `
90+ : 'Last seen unavailable'
8591 const versionLabel = ( ) => device . latest ?. openpilot_version || 'Version unavailable'
86-
92+ const uploadSummary = ( ) => {
93+ const onlineItems = onlineQueue . latest ?. result ?? [ ]
94+ const offlineItems = offlineQueue . latest ? mapOfflineQueueItems ( offlineQueue . latest ) : [ ]
95+ const queuedCount = onlineItems . length + offlineItems . length
96+ if ( queuedCount === 0 ) return { count : 0 , label : 'No pending uploads' }
97+ if ( onlineItems . some ( ( item ) => item . progress > 0 && item . progress < 1 ) ) {
98+ return { count : queuedCount , label : `${ queuedCount } upload${ queuedCount === 1 ? '' : 's' } in progress` }
99+ }
100+ if ( offlineItems . length > 0 && onlineItems . length === 0 ) {
101+ return { count : queuedCount , label : `${ queuedCount } queued until the device reconnects` }
102+ }
103+ return { count : queuedCount , label : `${ queuedCount } upload${ queuedCount === 1 ? '' : 's' } queued` }
104+ }
105+ createEffect ( ( ) => uploadSummary ( ) . count > 0 && setQueueVisible ( true ) )
87106 return (
88107 < >
89108 < TopAppBar
@@ -133,6 +152,7 @@ const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
133152 onClick = { ( ) => setQueueVisible ( ! queueVisible ( ) ) }
134153 >
135154 < p > { queueVisible ( ) ? 'Hide upload status' : 'Show upload status' } </ p >
155+ < span class = "rounded-full bg-surface-container-high px-2 py-1 text-xs text-on-surface-variant" > { uploadSummary ( ) . label } </ span >
136156 < Icon class = "text-zinc-500" name = { queueVisible ( ) ? 'keyboard_arrow_up' : 'keyboard_arrow_down' } />
137157 </ button >
138158 </ Show >
0 commit comments