Repository: CodeClimbersIO/ebb-app
Pull request #85: Onboarding improvements and fixes
Author: login: nathancovey
Created: 2025-04-24T14:44:14Z | Merged: 2025-04-24T16:46:09Z

- Added custom dmg background (if you do any colored background they almost always make the app's text black so I had to get creative there :sweat_smile:)
- Blocking not working on the first go is because I had failed to transition the blocking to our new "get preferences from the state" paradigm. The code I pushed up fixes this.
- Added sensible defaults for 1st time users. CMD+E is now default shortcut. Social media, entertainment added by default. 30 min default session time.
- Increased error toast duration


Diff:
diff --git a/src-tauri/images/dmg-background.png b/src-tauri/images/dmg-background.png
new file mode 100644
index 00000000..b5d0ef62
Binary files /dev/null and b/src-tauri/images/dmg-background.png differ
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 308812ad..f6f085ec 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -69,7 +69,10 @@
       "icons/icon.ico"
     ],
     "macOS": {
-      "frameworks": ["./target/libMacMonitor.dylib"]
+      "frameworks": ["./target/libMacMonitor.dylib"],
+      "dmg": {
+        "background": "./images/dmg-background.png"
+      }
     },
     "createUpdaterArtifacts": true,
     "publisher": "Ebb"
diff --git a/src/api/ebbApi/blockingPreferenceApi.ts b/src/api/ebbApi/blockingPreferenceApi.ts
index e043a444..c18abb97 100644
--- a/src/api/ebbApi/blockingPreferenceApi.ts
+++ b/src/api/ebbApi/blockingPreferenceApi.ts
@@ -1,8 +1,9 @@
 import { SearchOption } from '@/components/AppSelector'
 import { BlockingPreferenceRepo, BlockingPreferenceDb } from '@/db/ebb/blockingPreferenceRepo'
-import { App, AppRepo } from '@/db/monitor/appRepo'
+import { AppRepo, App } from '@/db/monitor/appRepo'
 import { TagRepo } from '@/db/monitor/tagRepo'
 import { AppCategory } from '../../lib/app-directory/apps-types'
+import type { Tag, TagWithAppCount } from '@/db/monitor/tagRepo'
 
 const saveWorkflowBlockingPreferences = async (
   workflowId: string,
@@ -35,22 +36,6 @@ const getWorkflowBlockingPreferencesAsSearchOptions = async (workflowId: string)
   return await convertToSearchOptions(preferences)
 }
 
-const getWorkflowBlockedApps = async (workflowId: string): Promise<App[]> => {
-  const { preferences } = await BlockingPreferenceRepo.getWorkflowBlockingPreferences(workflowId)
-  // get all app ids from preferences
-  const prefAppIds = preferences
-    .filter(pref => pref.app_id)
-    .map(pref => pref.app_id as string)
-  const prefTagIds = preferences
-    .filter(pref => pref.tag_id)
-    .map(pref => pref.tag_id as string)
-
-  // get all apps that belong to the tags of type category
-  const categoryApps = await AppRepo.getAppsByCategoryTags(prefTagIds)
-  const apps = await AppRepo.getAppsByIds([...prefAppIds])
-  return [...apps, ...categoryApps]
-}
-
 const convertToSearchOptions = async (
   preferences: BlockingPreferenceDb[]
 ): Promise<SearchOption[]> => {
@@ -93,9 +78,71 @@ const convertToSearchOptions = async (
   return searchOptions
 }
 
+interface BlockingAppConfig {
+  external_id: string | null
+  is_browser: boolean
+}
+
+const getWorkflowBlockedApps = async (workflowId: string): Promise<BlockingAppConfig[]> => {
+  const selectedApps = await getWorkflowBlockingPreferencesAsSearchOptions(workflowId)
+
+  const directAppSelections: App[] = selectedApps
+    .filter((option): option is Extract<SearchOption, { type: 'app' }> => option.type === 'app' && !!option.app)
+    .map(option => option.app)
+
+  const categorySelections: Tag[] = selectedApps
+    .filter((option): option is Extract<SearchOption, { type: 'category' }> => option.type === 'category' && !!option.tag)
+    .map(option => option.tag)
+
+  const categoryTagIds = categorySelections.map(tag => tag.id).filter(id => !!id) as string[]
+
+  const appsFromCategories = categoryTagIds.length > 0
+    ? await AppRepo.getAppsByCategoryTags(categoryTagIds)
+    : []
+
+  const allAppsToConsider = [...directAppSelections, ...appsFromCategories]
+
+  const uniqueAppsMap = new Map<string, App>()
+  allAppsToConsider.forEach(app => {
+    if (app && app.id) {
+      uniqueAppsMap.set(app.id, app)
+    }
+  })
+  const uniqueApps = Array.from(uniqueAppsMap.values())
+
+  const blockingAppConfig = uniqueApps.map((app: App) => ({
+    external_id: app.app_external_id,
+    is_browser: app.is_browser === 1
+  }))
+
+  return blockingAppConfig
+}
+
+const getDefaultSearchOptions = async (): Promise<SearchOption[]> => {
+  const defaultCategoryNames = ['social media', 'entertainment']
+  const allCategoryTags = await TagRepo.getTagsByType('category')
+  const defaultTags = allCategoryTags.filter((tag: Tag) => 
+    defaultCategoryNames.includes(tag.name)
+  )
+  const defaultTagIds = defaultTags.map((tag: Tag) => tag.id).filter((id): id is string => !!id)
+  
+  let defaultSearchOptions: SearchOption[] = []
+  if (defaultTagIds.length > 0) {
+    const categoriesWithCounts = await TagRepo.getCategoriesWithAppCounts(defaultTagIds)
+    defaultSearchOptions = categoriesWithCounts.map((catInfo: TagWithAppCount): SearchOption => ({
+      type: 'category',
+      tag: catInfo, 
+      category: catInfo.name as AppCategory,
+      count: catInfo.count
+    }))
+  }
+  return defaultSearchOptions
+}
+
 export const BlockingPreferenceApi = {
   getWorkflowBlockingPreferencesAsSearchOptions,
   saveWorkflowBlockingPreferences,
-  getWorkflowBlockedApps
+  getWorkflowBlockedApps,
+  getDefaultSearchOptions,
 }
 
diff --git a/src/api/ebbApi/shortcutApi.ts b/src/api/ebbApi/shortcutApi.ts
index 5b62b9ec..e3dec714 100644
--- a/src/api/ebbApi/shortcutApi.ts
+++ b/src/api/ebbApi/shortcutApi.ts
@@ -59,7 +59,9 @@ export const updateGlobalShortcut = async (newShortcut: string): Promise<void> =
           }
         })
       } catch (registrationError) {
-        logAndToastError(`Explicit error during shortcut update registration: ${registrationError}`)
+        if (newShortcut !== DEFAULT_SHORTCUT) {
+          logAndToastError(`Explicit error during shortcut update registration: ${registrationError}`)
+        }
       }
     }
     
diff --git a/src/components/ShortcutInput.tsx b/src/components/ShortcutInput.tsx
index c2ebc402..3dc58478 100644
--- a/src/components/ShortcutInput.tsx
+++ b/src/components/ShortcutInput.tsx
@@ -5,7 +5,6 @@ import { Hotkey } from '@/components/ui/hotkey'
 import { X, Check } from 'lucide-react'
 import {
   updateGlobalShortcut,
-  loadShortcut as loadInitialShortcut,
 } from '../api/ebbApi/shortcutApi'
 import { useShortcutStore } from '@/lib/stores/shortcutStore'
 import { logAndToastError } from '@/lib/utils/logAndToastError'
@@ -38,23 +37,10 @@ export function ShortcutInput({ popoverAlign = 'center' }: ShortcutInputProps) {
   const [isOpen, setIsOpen] = useState(false)
   const [activeModifiers, setActiveModifiers] = useState<ModifierKey[]>([])
   const [activeKey, setActiveKey] = useState<string | null>(null)
-  const [currentShortcut, setCurrentShortcut] = useState('')
   const [recordingOpacity, setRecordingOpacity] = useState(1)
   const [snapshot, setSnapshot] = useState<KeySnapshot | null>(null)
   const loadShortcutFromStore = useShortcutStore((state) => state.loadShortcutFromStorage)
-
-  useEffect(() => {
-    const loadAndSetShortcut = async () => {
-      try {
-        const initialShortcut = await loadInitialShortcut()
-        setCurrentShortcut(initialShortcut)
-        await loadShortcutFromStore()
-      } catch (error) {
-        logAndToastError(`Failed to load initial shortcut: ${error}`)
-      }
-    }
-    void loadAndSetShortcut()
-  }, [loadShortcutFromStore])
+  const shortcutParts = useShortcutStore((state) => state.shortcutParts)
 
   const handleOpenChange = (open: boolean) => {
     setIsOpen(open)
@@ -93,7 +79,7 @@ export function ShortcutInput({ popoverAlign = 'center' }: ShortcutInputProps) {
       } else if (keyCodeMap[e.key]) {
         unmodifiedKey = keyCodeMap[e.key]
       } else if (e.key === ' ') {
-         unmodifiedKey = 'SPACE'
+        unmodifiedKey = 'SPACE'
       }
 
       if (keyCodeMap[unmodifiedKey]) {
@@ -115,7 +101,6 @@ export function ShortcutInput({ popoverAlign = 'center' }: ShortcutInputProps) {
             setRecordingOpacity(0)
             setSnapshot({ modifiers: currentModifiers, key: unmodifiedKey, isClosing: true })
             await updateGlobalShortcut(newShortcut)
-            setCurrentShortcut(newShortcut)
             await loadShortcutFromStore()
             
             setTimeout(() => {
@@ -164,7 +149,6 @@ export function ShortcutInput({ popoverAlign = 'center' }: ShortcutInputProps) {
     e.stopPropagation()
     try {
       await updateGlobalShortcut('')
-      setCurrentShortcut('')
       await loadShortcutFromStore()
     } catch (error) {
       logAndToastError(`Failed to clear shortcut: ${error}`)
@@ -172,20 +156,12 @@ export function ShortcutInput({ popoverAlign = 'center' }: ShortcutInputProps) {
   }
 
   const renderHotkeyPart = (part: string) => {
-    const content = part === 'CommandOrControl' ? '⌘' : 
-                   part === 'Control' ? '⌃' :
-                   part === 'Option' ? '⌥' :
-                   part === 'Shift' ? '⇧' :
-                   part === 'ENTER' ? '↵' :
-                   part === 'SPACE' ? '⎵' : part
-    return <Hotkey size="lg">{content}</Hotkey>
+    return <Hotkey size="lg">{part}</Hotkey>
   }
 
   const displayModifiers = snapshot?.isClosing ? snapshot.modifiers : activeModifiers
   const displayKey = snapshot?.isClosing ? snapshot.key : activeKey
-  const formattedDisplayKey = displayKey === 'SPACE' ? '⎵' : 
-                             displayKey === 'ENTER' ? '↵' : 
-                             displayKey
+  const formattedDisplayKey = displayKey === 'SPACE' ? '⎵' : displayKey === 'ENTER' ? '↵' : displayKey
 
   return (
     <Popover open={isOpen} onOpenChange={handleOpenChange}>
@@ -194,9 +170,9 @@ export function ShortcutInput({ popoverAlign = 'center' }: ShortcutInputProps) {
           variant='outline' 
           className='min-w-[140px] h-12 text-lg font-mono flex items-center gap-2 group relative'
         >
-          {currentShortcut ? (
+          {shortcutParts.length > 0 ? (
             <>
-              {currentShortcut.split('+').map((part, index) => (
+              {shortcutParts.map((part, index) => (
                 <span key={index}>{renderHotkeyPart(part)}</span>
               ))}
               <div 
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
index 0755216f..ba1b171c 100644
--- a/src/components/ui/sonner.tsx
+++ b/src/components/ui/sonner.tsx
@@ -49,6 +49,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
 
 const toastError = (errorMessage: string) => {
   toast(<ErrorToastContent errorMessage={errorMessage} />, {
+    duration: 8000,
   })
 }
 
diff --git a/src/lib/stores/shortcutStore.ts b/src/lib/stores/shortcutStore.ts
index 21cd99d6..d6bb9dee 100644
--- a/src/lib/stores/shortcutStore.ts
+++ b/src/lib/stores/shortcutStore.ts
@@ -8,11 +8,11 @@ interface ShortcutState {
 }
 
 export const useShortcutStore = create<ShortcutState>((set) => ({
-  shortcutParts: ['⌘', 'E'],
+  shortcutParts: [],
   setShortcutParts: (parts) => set({ shortcutParts: parts }),
   loadShortcutFromStorage: async () => {
     const shortcut = await loadShortcut()
-    const parts = shortcut.split('+')
+    const parts = shortcut ? shortcut.split('+') : []
     const displayParts = parts.map(part => {
       if (part === 'CommandOrControl') return '⌘'
       if (part === 'Control') return '⌃'
diff --git a/src/pages/ShortcutTutorialPage.tsx b/src/pages/ShortcutTutorialPage.tsx
index 6a1b5d98..25ce0997 100644
--- a/src/pages/ShortcutTutorialPage.tsx
+++ b/src/pages/ShortcutTutorialPage.tsx
@@ -3,9 +3,28 @@ import { useNavigate } from 'react-router-dom'
 import { OnboardingUtils } from '@/lib/utils/onboarding'
 import { ShortcutInput } from '@/components/ShortcutInput'
 import { logAndToastError } from '@/lib/utils/logAndToastError'
+import { useEffect } from 'react'
+import { loadShortcut, updateGlobalShortcut, DEFAULT_SHORTCUT } from '@/api/ebbApi/shortcutApi'
+import { useShortcutStore } from '@/lib/stores/shortcutStore'
 
 export const ShortcutTutorialPage = () => {
   const navigate = useNavigate()
+  const loadShortcutFromStore = useShortcutStore((state) => state.loadShortcutFromStorage)
+
+  useEffect(() => {
+    const initializeShortcut = async () => {
+      try {
+        const currentShortcut = await loadShortcut()
+        if (!currentShortcut) {
+          await updateGlobalShortcut(DEFAULT_SHORTCUT)
+        }
+        await loadShortcutFromStore()
+      } catch (error) {
+        logAndToastError(`Failed to initialize shortcut on tutorial page: ${error}`)
+      }
+    }
+    void initializeShortcut()
+  }, [loadShortcutFromStore])
 
   const handleComplete = async () => {
     try {
diff --git a/src/pages/StartFlowPage.tsx b/src/pages/StartFlowPage.tsx
index 7d80da56..83caefdb 100644
--- a/src/pages/StartFlowPage.tsx
+++ b/src/pages/StartFlowPage.tsx
@@ -11,16 +11,15 @@ import { WorkflowApi, type Workflow } from '@/api/ebbApi/workflowApi'
 import { invoke } from '@tauri-apps/api/core'
 import { DateTime, Duration } from 'luxon'
 import { startFlowTimer } from '../lib/tray'
-import { getDurationFromDefault, useFlowTimer } from '../lib/stores/flowTimer'
+import { useFlowTimer, getDurationFromDefault } from '../lib/stores/flowTimer'
 import { MusicSelector } from '@/components/MusicSelector'
 import { AppSelector, type SearchOption } from '@/components/AppSelector'
-import { BlockingPreferenceApi } from '../api/ebbApi/blockingPreferenceApi'
-import { App } from '../db/monitor/appRepo'
 import { AlertCircle } from 'lucide-react'
 import { Alert, AlertDescription } from '@/components/ui/alert'
 import { SpotifyApiService } from '@/lib/integrations/spotify/spotifyApi'
 import { TypewriterModeToggle } from '@/components/TypewriterModeToggle'
 import { logAndToastError } from '@/lib/utils/logAndToastError'
+import { BlockingPreferenceApi } from '@/api/ebbApi/blockingPreferenceApi'
 
 export const StartFlowPage = () => {
   const { duration, setDuration } = useFlowTimer()
@@ -47,7 +46,19 @@ export const StartFlowPage = () => {
         const loadedWorkflows = await WorkflowApi.getWorkflows()
         setWorkflows(loadedWorkflows)
         
-        if (loadedWorkflows.length > 0) {
+        if (loadedWorkflows.length === 0) {
+          // First session defaults
+          setDuration(Duration.fromObject({ minutes: 30 }))
+
+          try {
+            const defaultSearchOptions = await BlockingPreferenceApi.getDefaultSearchOptions()
+            setSelectedApps(defaultSearchOptions) 
+          } catch (tagError) {
+            logAndToastError(`Failed to load default categories: ${tagError}`)
+            setSelectedApps([]) 
+          }
+
+        } else {
           const mostRecentWorkflow = loadedWorkflows.reduce((prev, current) => {
             return (prev.lastSelected || 0) > (current.lastSelected || 0) ? prev : current
           }, loadedWorkflows[0])
@@ -166,16 +177,17 @@ export const StartFlowPage = () => {
 
   const handleBegin = async () => {
     try {
-      const workflowId = selectedWorkflowId
-      const workflowName = selectedWorkflow?.name || 'Focus Session'
+      let workflowId = selectedWorkflowId
+      let currentWorkflow = selectedWorkflow
+      const workflowName = currentWorkflow?.name || 'Focus Session'
 
-      // If this is the first session (no workflows), create one from current settings
+      // If first session (no workflows), create one from current settings
       if (workflows.length === 0) {
         const newWorkflow: Workflow = {
           name: 'New Profile',
           selectedApps,
           selectedPlaylist,
-          selectedPlaylistName: null,
+          selectedPlaylistName: currentWorkflow?.selectedPlaylistName,
           lastSelected: Date.now(),
           settings: {
             defaultDuration: duration?.as('minutes') ?? null,
@@ -190,27 +202,22 @@ export const StartFlowPage = () => {
         try {
           const savedWorkflow = await WorkflowApi.saveWorkflow(newWorkflow)
           setWorkflows([savedWorkflow])
-          setSelectedWorkflowId(savedWorkflow.id || null)
-          setSelectedWorkflow(savedWorkflow)
+          workflowId = savedWorkflow.id || null
+          currentWorkflow = savedWorkflow
+          setSelectedWorkflowId(workflowId)
+          setSelectedWorkflow(currentWorkflow)
         } catch (error) {
           logAndToastError(`Failed to save first workflow: ${error}`)
+          return
         }
-      }
+      } else if (workflowId) {
 
-      const blockedApps = workflowId ? 
-        await BlockingPreferenceApi.getWorkflowBlockedApps(workflowId) :
-        []
-      
-      const blockingApps = blockedApps.map((app: App) => ({
-        external_id: app.app_external_id,
-        is_browser: app.is_browser === 1
-      }))
-      const isBlockList = !isAllowList
-
-      if (workflowId) {
         await WorkflowApi.updateLastSelected(workflowId)
       }
 
+      const blockingApps = workflowId ? await BlockingPreferenceApi.getWorkflowBlockedApps(workflowId) : []
+      const isBlockList = !isAllowList
+
       const sessionId = await FlowSessionApi.startFlowSession(
         workflowName,
         duration ? duration.as('minutes') : undefined
@@ -231,11 +238,11 @@ export const StartFlowPage = () => {
         objective: workflowName,
         sessionId,
         duration: duration ? duration.as('minutes') : undefined,
-        workflowId,
+        workflowId, 
         hasBreathing,
         hasMusic,
         selectedPlaylist,
-        selectedPlaylistName: selectedWorkflow?.selectedPlaylistName,
+        selectedPlaylistName: currentWorkflow?.selectedPlaylistName, 
         difficulty
       }
 


Reviews:
user: login: rphovley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T15:50:52Z
user: login: rphovley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T15:52:24Z
user: login: rphovley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T15:56:50Z
user: login: rphovley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T16:00:18Z
user: login: nathancovey
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T16:11:23Z
user: login: nathancovey
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T16:12:40Z
user: login: nathancovey
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T16:15:00Z

Review comments:
diff_hunk: @@ -38,23 +37,10 @@ export function ShortcutInput({ popoverAlign = 'center' }: ShortcutInputProps) {
   const [isOpen, setIsOpen] = useState(false)
   const [activeModifiers, setActiveModifiers] = useState<ModifierKey[]>([])
   const [activeKey, setActiveKey] = useState<string | null>(null)
-  const [currentShortcut, setCurrentShortcut] = useState('')
   const [recordingOpacity, setRecordingOpacity] = useState(1)
   const [snapshot, setSnapshot] = useState<KeySnapshot | null>(null)
   const loadShortcutFromStore = useShortcutStore((state) => state.loadShortcutFromStorage)
-
-  useEffect(() => {
path: src/components/ShortcutInput.tsx
user: login: rphovley
body: Do we not need to load up the shortcut anymore or is this being done in the shortcutStore?
created_at: 2025-04-24T15:50:52Z
author_association: MEMBER
diff_hunk: @@ -8,11 +8,11 @@ interface ShortcutState {
 }
 
 export const useShortcutStore = create<ShortcutState>((set) => ({
-  shortcutParts: ['⌘', 'E'],
+  shortcutParts: [],
path: src/lib/stores/shortcutStore.ts
user: login: rphovley
body: how does this make the shortcut default to "command" + "E"? 
created_at: 2025-04-24T15:52:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,47 @@
+import type { SearchOption } from '@/components/AppSelector'
+import { AppRepo, type App } from '@/db/monitor/appRepo'
+import type { Tag } from '@/db/monitor/tagRepo'
+
+interface BlockingAppConfig {
+  external_id: string | null
+  is_browser: boolean
+}
+
+export const prepareBlockingConfig = async (
path: src/lib/utils/blocking.util.ts
user: login: rphovley
body: This looks like something that would belong in the blockingPreferenceApi. I thought we'd already have something like this with BlockingPreferenceApi.getWorkflowBlockedApps? 

If that function isn't working, we should probably adjust that one instead of creating a different one
created_at: 2025-04-24T15:56:49Z
author_association: MEMBER
diff_hunk: @@ -47,7 +49,36 @@ export const StartFlowPage = () => {
         const loadedWorkflows = await WorkflowApi.getWorkflows()
         setWorkflows(loadedWorkflows)
         
-        if (loadedWorkflows.length > 0) {
+        if (loadedWorkflows.length === 0) {
+          // First session defaults
+          setDuration(Duration.fromObject({ minutes: 30 }))
+
+          try {
+            const defaultCategoryNames = ['social media', 'entertainment']
path: src/pages/StartFlowPage.tsx
user: login: rphovley
body: I'd put the code for this try block in the BlockingPreferenceApi as `getDefaultSearchOptions`and do something like 
```
let defaultSearchOptions = BlockingPreferenceApi.getDefaultSearchOptions(params)
```
here. Should help with readability
created_at: 2025-04-24T16:00:18Z
author_association: MEMBER
diff_hunk: @@ -38,23 +37,10 @@ export function ShortcutInput({ popoverAlign = 'center' }: ShortcutInputProps) {
   const [isOpen, setIsOpen] = useState(false)
   const [activeModifiers, setActiveModifiers] = useState<ModifierKey[]>([])
   const [activeKey, setActiveKey] = useState<string | null>(null)
-  const [currentShortcut, setCurrentShortcut] = useState('')
   const [recordingOpacity, setRecordingOpacity] = useState(1)
   const [snapshot, setSnapshot] = useState<KeySnapshot | null>(null)
   const loadShortcutFromStore = useShortcutStore((state) => state.loadShortcutFromStorage)
-
-  useEffect(() => {
path: src/components/ShortcutInput.tsx
user: login: nathancovey
body: It's being done in the store
created_at: 2025-04-24T16:11:23Z
author_association: MEMBER
diff_hunk: @@ -8,11 +8,11 @@ interface ShortcutState {
 }
 
 export const useShortcutStore = create<ShortcutState>((set) => ({
-  shortcutParts: ['⌘', 'E'],
+  shortcutParts: [],
path: src/lib/stores/shortcutStore.ts
user: login: nathancovey
body: Setting the default happens on `shortcutTutorialPage.tsx`, during onboarding
created_at: 2025-04-24T16:12:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,47 @@
+import type { SearchOption } from '@/components/AppSelector'
+import { AppRepo, type App } from '@/db/monitor/appRepo'
+import type { Tag } from '@/db/monitor/tagRepo'
+
+interface BlockingAppConfig {
+  external_id: string | null
+  is_browser: boolean
+}
+
+export const prepareBlockingConfig = async (
path: src/lib/utils/blocking.util.ts
user: login: nathancovey
body: okay I will change this
created_at: 2025-04-24T16:15:00Z
author_association: MEMBER

PR comments:
user: login: nathancovey
created_at: 2025-04-24T16:23:25Z
body: Okay just refactored based on all your suggestions 👍 @rphovley 
author_association: MEMBER
user: login: nathancovey
created_at: 2025-04-24T16:24:22Z
body: You might want to pull and test just to make sure I didn't miss anything
author_association: MEMBER