diff --git a/src/components/pages/OverviewPage.tsx b/src/components/pages/OverviewPage.tsx
new file mode 100644
index 0000000..57982d6
--- /dev/null
+++ b/src/components/pages/OverviewPage.tsx
@@ -0,0 +1,176 @@
+import { useState, useEffect, ReactNode } from 'react'
+import { invoke } from '@tauri-apps/api/core'
+import { useAppContext } from '@/contexts/useAppContext'
+import { Icon, IconName } from '@/components/icons'
+
+interface LocalReport {
+ game_id: string
+ unlocker: string
+ worked: boolean
+}
+
+interface SystemInfo {
+ os_name: string
+ cpu_model: string
+ cpu_cores: number
+ gpu_name: string
+}
+
+interface StatChipProps {
+ label: string
+ value: ReactNode
+ icon: IconName | string
+ variant?: string
+}
+
+const StatChip = ({ label, value, icon, variant }: StatChipProps) => (
+
+
+ {value}
+ {label}
+
+)
+
+/**
+ * Overview page - the default landing view. Leads with the two library
+ * totals as hero numbers, then a Native/Proton composition bar for the
+ * Steam library, then secondary stats as compact chips, plus app info.
+ */
+const OverviewPage = () => {
+ const { games, epicGames, epicLoading, loadEpicGames, activityFeed } = useAppContext()
+ const [localReports, setLocalReports] = useState([])
+ const [systemInfo, setSystemInfo] = useState(null)
+
+ useEffect(() => {
+ invoke('get_local_reports')
+ .then(setLocalReports)
+ .catch(() => setLocalReports([]))
+ }, [])
+
+ useEffect(() => {
+ invoke('get_system_info')
+ .then(setSystemInfo)
+ .catch(() => setSystemInfo(null))
+ }, [])
+
+ // Overview is the default landing page, so kick off the Epic scan here too
+ // otherwise the Epic count would show 0 until the user visits that tab.
+ useEffect(() => {
+ if (epicGames.length === 0 && !epicLoading) {
+ loadEpicGames()
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const nativeCount = games.filter((g) => g.native).length
+ const protonCount = games.length - nativeCount
+ const creamCount = games.filter((g) => g.cream_installed).length
+ const smokeCount = games.filter((g) => g.smoke_installed).length
+ const nativePct = games.length ? (nativeCount / games.length) * 100 : 0
+ const protonPct = games.length ? (protonCount / games.length) * 100 : 0
+
+ return (
+
+
Overview
+
+
+
+ {games.length}
+ Steam Games
+
+
+
+ {epicLoading ? '—' : epicGames.length}
+ Epic Games
+
+
+
+ {games.length > 0 && (
+
+
Steam Library Composition
+
+
+
+
+ Native
+ {nativeCount}
+
+
+
+ Proton
+ {protonCount}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
Recent Activity
+
+ {activityFeed.length === 0 ? (
+
+ Nothing yet this session - install or uninstall something and it'll show up here.
+
+ ) : (
+
+ {activityFeed.map((item) => (
+ -
+
+ {item.message}
+
+ {new Date(item.timestamp).toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+ ))}
+
+ )}
+
+
+
+
System
+
+ {systemInfo ? (
+
+
+ Operating System
+
+ {systemInfo.os_name}
+
+
+
+ Processor
+
+ {systemInfo.cpu_model}
+
+ {systemInfo.cpu_cores} threads
+
+
+ Graphics
+
+ {systemInfo.gpu_name}
+
+
+
+ ) : (
+
Reading system info...
+ )}
+
+
+
+ )
+}
+
+export default OverviewPage
diff --git a/src/components/pages/SettingsPage.tsx b/src/components/pages/SettingsPage.tsx
new file mode 100644
index 0000000..001d12b
--- /dev/null
+++ b/src/components/pages/SettingsPage.tsx
@@ -0,0 +1,236 @@
+import { useState, useEffect, useCallback } from 'react'
+import { invoke } from '@tauri-apps/api/core'
+import { open } from '@tauri-apps/plugin-dialog'
+import { EntryList } from '@/components/common'
+import { AnimatedCheckbox, Button } from '@/components/buttons'
+import { useAppContext } from '@/contexts/useAppContext'
+import { Config } from '@/types/Config'
+
+/**
+ * Settings page - Steam library paths, the compatibility reporting toggle, and a danger
+ * zone for resetting app data on disk.
+ */
+const SettingsPage = () => {
+ const [libraryPaths, setLibraryPaths] = useState([])
+ const [reportingOptedIn, setReportingOptedIn] = useState(false)
+ const [isBusy, setIsBusy] = useState(false)
+ const [pathError, setPathError] = useState(null)
+ const [isResetting, setIsResetting] = useState(false)
+ const [isClearingCache, setIsClearingCache] = useState(false)
+
+ const { loadGames, showToast } = useAppContext()
+
+ const loadSettingsConfig = useCallback(async () => {
+ try {
+ const config = await invoke('load_config')
+ setLibraryPaths(config.custom_steam_paths)
+ setReportingOptedIn(config.reporting_opted_in)
+ } catch (error) {
+ console.error('Failed to load config:', error)
+ }
+ }, [])
+
+ useEffect(() => {
+ loadSettingsConfig()
+ }, [loadSettingsConfig])
+
+ const addPath = async (path: string) => {
+ setPathError(null)
+ setIsBusy(true)
+ try {
+ const config = await invoke('add_custom_steam_path', { path })
+ setLibraryPaths(config.custom_steam_paths)
+ loadGames()
+ showToast('Library path added - rescanning for games...', 'info')
+ } catch (error) {
+ setPathError(`${error}`)
+ } finally {
+ setIsBusy(false)
+ }
+ }
+
+ const handleBrowse = async () => {
+ setPathError(null)
+
+ let selected: string | null
+ try {
+ selected = await open({ directory: true, multiple: false, title: 'Select Steam Library Folder' })
+ } catch (error) {
+ console.error('Failed to open folder picker:', error)
+ return
+ }
+ if (!selected) return
+
+ addPath(selected)
+ }
+
+ const handleRemovePath = async (path: string) => {
+ setIsBusy(true)
+ setPathError(null)
+ try {
+ const config = await invoke('remove_custom_steam_path', { path })
+ setLibraryPaths(config.custom_steam_paths)
+ loadGames()
+ showToast('Library path removed - rescanning for games...', 'info')
+ } catch (error) {
+ setPathError(`${error}`)
+ } finally {
+ setIsBusy(false)
+ }
+ }
+
+ const handleToggleReporting = async () => {
+ const next = !reportingOptedIn
+ setReportingOptedIn(next)
+ try {
+ await invoke('set_reporting_opt_in', { optedIn: next })
+ showToast(
+ next ? 'Compatibility reporting enabled' : 'Compatibility reporting disabled',
+ 'info'
+ )
+ } catch (error) {
+ setReportingOptedIn(!next)
+ showToast(`Failed to update reporting preference: ${error}`, 'error')
+ }
+ }
+
+ const handleResetConfig = async () => {
+ if (
+ !window.confirm(
+ 'Reset all settings to their defaults? This clears your custom Steam library paths and reporting preference, and brings back the startup disclaimer.'
+ )
+ ) {
+ return
+ }
+
+ setIsResetting(true)
+ try {
+ const config = await invoke('reset_config')
+ setLibraryPaths(config.custom_steam_paths)
+ setReportingOptedIn(config.reporting_opted_in)
+ loadGames()
+ showToast('Settings reset to defaults', 'success')
+ } catch (error) {
+ showToast(`Failed to reset settings: ${error}`, 'error')
+ } finally {
+ setIsResetting(false)
+ }
+ }
+
+ const handleClearCache = async () => {
+ if (
+ !window.confirm(
+ "Clear cached unlocker downloads? They'll be re-downloaded automatically the next time they're needed."
+ )
+ ) {
+ return
+ }
+
+ setIsClearingCache(true)
+ try {
+ await invoke('clear_caches')
+ showToast('Unlocker cache cleared', 'success')
+ } catch (error) {
+ showToast(`Failed to clear cache: ${error}`, 'error')
+ } finally {
+ setIsClearingCache(false)
+ }
+ }
+
+ const handleOpenConfigFolder = async () => {
+ try {
+ await invoke('open_config_folder')
+ } catch (error) {
+ showToast(`Failed to open config folder: ${error}`, 'error')
+ }
+ }
+
+ return (
+
+
Settings
+
+
+
Steam Library Paths
+
+ Add extra folders to scan for Steam games - useful if a library isn't auto-detected.
+
+
+ {pathError &&
{pathError}
}
+
+
+
+
+
+
Compatibility Reporting
+
+ Anonymously report whether an unlocker worked for a game, and see how it's worked for
+ others before you install.
+
+
+
+
+
+
+
Danger Zone
+
+ These affect app data on disk - not your games or any unlockers already installed on
+ them.
+
+
+
+
+ Reset all settings
+
+ Restores defaults, including custom library paths and the startup disclaimer.
+
+
+
+
+
+
+
+ Clear unlocker cache
+
+ Deletes downloaded CreamLinux/SmokeAPI/ScreamAPI/Koaloader files.
+
+
+
+
+
+
+
+ Open config folder
+ ~/.config/creamlinux
+
+
+
+
+
+ )
+}
+
+export default SettingsPage